@gtkx/cli 1.0.0-rc.1 → 1.0.0-rc.2

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.
Files changed (286) hide show
  1. package/dist/builder.d.ts +3 -2
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +2 -1
  4. package/dist/builder.js.map +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +21 -6
  8. package/dist/cli.js.map +1 -1
  9. package/dist/codegen/freshness.d.ts +5 -5
  10. package/dist/codegen/freshness.d.ts.map +1 -1
  11. package/dist/codegen/freshness.js +26 -23
  12. package/dist/codegen/freshness.js.map +1 -1
  13. package/dist/codegen/report.d.ts +2 -1
  14. package/dist/codegen/report.d.ts.map +1 -1
  15. package/dist/codegen/report.js +4 -2
  16. package/dist/codegen/report.js.map +1 -1
  17. package/dist/codegen/run-codegen.d.ts +8 -8
  18. package/dist/codegen/run-codegen.d.ts.map +1 -1
  19. package/dist/codegen/run-codegen.js +72 -43
  20. package/dist/codegen/run-codegen.js.map +1 -1
  21. package/dist/codegen/store-resolver.d.ts +8 -5
  22. package/dist/codegen/store-resolver.d.ts.map +1 -1
  23. package/dist/codegen/store-resolver.js +26 -6
  24. package/dist/codegen/store-resolver.js.map +1 -1
  25. package/dist/commands/build.d.ts +4 -3
  26. package/dist/commands/build.d.ts.map +1 -1
  27. package/dist/commands/build.js +2 -1
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/codegen.d.ts +2 -1
  30. package/dist/commands/codegen.d.ts.map +1 -1
  31. package/dist/commands/codegen.js +6 -4
  32. package/dist/commands/codegen.js.map +1 -1
  33. package/dist/commands/dev.d.ts +2 -1
  34. package/dist/commands/dev.d.ts.map +1 -1
  35. package/dist/commands/dev.js +2 -1
  36. package/dist/commands/dev.js.map +1 -1
  37. package/dist/commands/docs.d.ts +2 -1
  38. package/dist/commands/docs.d.ts.map +1 -1
  39. package/dist/commands/docs.js +22 -7
  40. package/dist/commands/docs.js.map +1 -1
  41. package/dist/dev/icon-dir.d.ts +2 -1
  42. package/dist/dev/icon-dir.d.ts.map +1 -1
  43. package/dist/dev/icon-dir.js +6 -3
  44. package/dist/dev/icon-dir.js.map +1 -1
  45. package/dist/dev/refresh-tracker.d.ts +5 -4
  46. package/dist/dev/refresh-tracker.d.ts.map +1 -1
  47. package/dist/dev/refresh-tracker.js +6 -5
  48. package/dist/dev/refresh-tracker.js.map +1 -1
  49. package/dist/dev/runner-deps.d.ts +2 -1
  50. package/dist/dev/runner-deps.d.ts.map +1 -1
  51. package/dist/dev/runner-deps.js +20 -4
  52. package/dist/dev/runner-deps.js.map +1 -1
  53. package/dist/dev/runner-main.d.ts +2 -1
  54. package/dist/dev/runner-main.d.ts.map +1 -1
  55. package/dist/dev/runner-main.js +3 -2
  56. package/dist/dev/runner-main.js.map +1 -1
  57. package/dist/dev/runner.d.ts +6 -5
  58. package/dist/dev/runner.d.ts.map +1 -1
  59. package/dist/dev/runner.js +69 -38
  60. package/dist/dev/runner.js.map +1 -1
  61. package/dist/dev/schema-dir.d.ts +2 -1
  62. package/dist/dev/schema-dir.d.ts.map +1 -1
  63. package/dist/dev/schema-dir.js +4 -2
  64. package/dist/dev/schema-dir.js.map +1 -1
  65. package/dist/dev/supervisor.d.ts +7 -6
  66. package/dist/dev/supervisor.d.ts.map +1 -1
  67. package/dist/dev/supervisor.js +107 -67
  68. package/dist/dev/supervisor.js.map +1 -1
  69. package/dist/dev/vite-dev-server.d.ts +3 -3
  70. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  71. package/dist/dev/vite-dev-server.js +2 -1
  72. package/dist/dev/vite-dev-server.js.map +1 -1
  73. package/dist/internal/banner.d.ts +2 -1
  74. package/dist/internal/banner.d.ts.map +1 -1
  75. package/dist/internal/banner.js +2 -1
  76. package/dist/internal/banner.js.map +1 -1
  77. package/dist/internal/data-dir.d.ts +3 -2
  78. package/dist/internal/data-dir.d.ts.map +1 -1
  79. package/dist/internal/data-dir.js +29 -14
  80. package/dist/internal/data-dir.js.map +1 -1
  81. package/dist/internal/entry-arg.d.ts +5 -4
  82. package/dist/internal/entry-arg.d.ts.map +1 -1
  83. package/dist/internal/entry-arg.js +10 -7
  84. package/dist/internal/entry-arg.js.map +1 -1
  85. package/dist/internal/errors.d.ts +3 -2
  86. package/dist/internal/errors.d.ts.map +1 -1
  87. package/dist/internal/errors.js +7 -5
  88. package/dist/internal/errors.js.map +1 -1
  89. package/dist/internal/list-files.d.ts +3 -2
  90. package/dist/internal/list-files.d.ts.map +1 -1
  91. package/dist/internal/list-files.js +5 -3
  92. package/dist/internal/list-files.js.map +1 -1
  93. package/dist/internal/resolve-cli-tool.d.ts +2 -1
  94. package/dist/internal/resolve-cli-tool.d.ts.map +1 -1
  95. package/dist/internal/resolve-cli-tool.js +17 -10
  96. package/dist/internal/resolve-cli-tool.js.map +1 -1
  97. package/dist/internal/staging-dir.d.ts +3 -2
  98. package/dist/internal/staging-dir.d.ts.map +1 -1
  99. package/dist/internal/staging-dir.js +3 -2
  100. package/dist/internal/staging-dir.js.map +1 -1
  101. package/dist/internal/xdg-data-dirs.d.ts +2 -1
  102. package/dist/internal/xdg-data-dirs.d.ts.map +1 -1
  103. package/dist/internal/xdg-data-dirs.js +4 -2
  104. package/dist/internal/xdg-data-dirs.js.map +1 -1
  105. package/dist/mcp/client.d.ts +9 -4
  106. package/dist/mcp/client.d.ts.map +1 -1
  107. package/dist/mcp/client.js +106 -81
  108. package/dist/mcp/client.js.map +1 -1
  109. package/dist/mcp/handlers.d.ts +3 -2
  110. package/dist/mcp/handlers.d.ts.map +1 -1
  111. package/dist/mcp/handlers.js +89 -61
  112. package/dist/mcp/handlers.js.map +1 -1
  113. package/dist/mcp/index.d.ts +2 -2
  114. package/dist/mcp/index.d.ts.map +1 -1
  115. package/dist/mcp/index.js +30 -13
  116. package/dist/mcp/index.js.map +1 -1
  117. package/dist/mcp/serialize-widget.d.ts +3 -3
  118. package/dist/mcp/serialize-widget.d.ts.map +1 -1
  119. package/dist/mcp/serialize-widget.js +9 -6
  120. package/dist/mcp/serialize-widget.js.map +1 -1
  121. package/dist/mcp/testing-loader.d.ts +3 -4
  122. package/dist/mcp/testing-loader.d.ts.map +1 -1
  123. package/dist/mcp/testing-loader.js +43 -21
  124. package/dist/mcp/testing-loader.js.map +1 -1
  125. package/dist/mcp/widget-registry.d.ts +3 -2
  126. package/dist/mcp/widget-registry.d.ts.map +1 -1
  127. package/dist/mcp/widget-registry.js +4 -3
  128. package/dist/mcp/widget-registry.js.map +1 -1
  129. package/dist/refresh-globals.d.ts +2 -0
  130. package/dist/refresh-globals.d.ts.map +1 -0
  131. package/dist/refresh-globals.js +8 -0
  132. package/dist/refresh-globals.js.map +1 -0
  133. package/dist/refresh-runtime.d.ts +5 -11
  134. package/dist/refresh-runtime.d.ts.map +1 -1
  135. package/dist/refresh-runtime.js +16 -14
  136. package/dist/refresh-runtime.js.map +1 -1
  137. package/dist/settings/compile.d.ts +2 -1
  138. package/dist/settings/compile.d.ts.map +1 -1
  139. package/dist/settings/compile.js +6 -4
  140. package/dist/settings/compile.js.map +1 -1
  141. package/dist/settings/parser.d.ts +6 -5
  142. package/dist/settings/parser.d.ts.map +1 -1
  143. package/dist/settings/parser.js +26 -22
  144. package/dist/settings/parser.js.map +1 -1
  145. package/dist/settings/render.d.ts +3 -2
  146. package/dist/settings/render.d.ts.map +1 -1
  147. package/dist/settings/render.js +38 -64
  148. package/dist/settings/render.js.map +1 -1
  149. package/dist/settings/schema.d.ts +8 -8
  150. package/dist/settings/schema.d.ts.map +1 -1
  151. package/dist/settings/schema.js +61 -43
  152. package/dist/settings/schema.js.map +1 -1
  153. package/dist/vite-plugins/asset-extensions.d.ts +4 -3
  154. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  155. package/dist/vite-plugins/asset-extensions.js +4 -3
  156. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  157. package/dist/vite-plugins/built-url.d.ts +2 -1
  158. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  159. package/dist/vite-plugins/built-url.js +19 -13
  160. package/dist/vite-plugins/built-url.js.map +1 -1
  161. package/dist/vite-plugins/css.d.ts +2 -1
  162. package/dist/vite-plugins/css.d.ts.map +1 -1
  163. package/dist/vite-plugins/css.js +23 -15
  164. package/dist/vite-plugins/css.js.map +1 -1
  165. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts +5 -4
  166. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts.map +1 -1
  167. package/dist/vite-plugins/fast-refresh/refresh-filter.js +11 -7
  168. package/dist/vite-plugins/fast-refresh/refresh-filter.js.map +1 -1
  169. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts +4 -3
  170. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts.map +1 -1
  171. package/dist/vite-plugins/fast-refresh/swc-refresh.js +45 -41
  172. package/dist/vite-plugins/fast-refresh/swc-refresh.js.map +1 -1
  173. package/dist/vite-plugins/icons.d.ts +2 -1
  174. package/dist/vite-plugins/icons.d.ts.map +1 -1
  175. package/dist/vite-plugins/icons.js +24 -20
  176. package/dist/vite-plugins/icons.js.map +1 -1
  177. package/dist/vite-plugins/index.d.ts +2 -1
  178. package/dist/vite-plugins/index.d.ts.map +1 -1
  179. package/dist/vite-plugins/index.js +3 -2
  180. package/dist/vite-plugins/index.js.map +1 -1
  181. package/dist/vite-plugins/native.d.ts +2 -1
  182. package/dist/vite-plugins/native.d.ts.map +1 -1
  183. package/dist/vite-plugins/native.js +6 -4
  184. package/dist/vite-plugins/native.js.map +1 -1
  185. package/dist/vite-plugins/react-compiler.d.ts +3 -2
  186. package/dist/vite-plugins/react-compiler.d.ts.map +1 -1
  187. package/dist/vite-plugins/react-compiler.js +26 -20
  188. package/dist/vite-plugins/react-compiler.js.map +1 -1
  189. package/dist/vite-plugins/react-dom-prebundle.d.ts +2 -1
  190. package/dist/vite-plugins/react-dom-prebundle.d.ts.map +1 -1
  191. package/dist/vite-plugins/react-dom-prebundle.js +2 -1
  192. package/dist/vite-plugins/react-dom-prebundle.js.map +1 -1
  193. package/dist/vite-plugins/resource-init-module.d.ts +2 -1
  194. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  195. package/dist/vite-plugins/resource-init-module.js +39 -38
  196. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  197. package/dist/vite-plugins/resource-shared.d.ts +7 -5
  198. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  199. package/dist/vite-plugins/resource-shared.js +17 -10
  200. package/dist/vite-plugins/resource-shared.js.map +1 -1
  201. package/dist/vite-plugins/resources.d.ts +3 -2
  202. package/dist/vite-plugins/resources.d.ts.map +1 -1
  203. package/dist/vite-plugins/resources.js +94 -54
  204. package/dist/vite-plugins/resources.js.map +1 -1
  205. package/dist/vite-plugins/settings-worker-env.d.ts +2 -1
  206. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  207. package/dist/vite-plugins/settings-worker-env.js +4 -2
  208. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  209. package/dist/vite-plugins/settings.d.ts +2 -1
  210. package/dist/vite-plugins/settings.d.ts.map +1 -1
  211. package/dist/vite-plugins/settings.js +56 -32
  212. package/dist/vite-plugins/settings.js.map +1 -1
  213. package/dist/vite-plugins/strip-query.d.ts +3 -0
  214. package/dist/vite-plugins/strip-query.d.ts.map +1 -0
  215. package/dist/vite-plugins/strip-query.js +6 -0
  216. package/dist/vite-plugins/strip-query.js.map +1 -0
  217. package/dist/vite-plugins/undeclared-library.d.ts +2 -1
  218. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
  219. package/dist/vite-plugins/undeclared-library.js +8 -5
  220. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  221. package/dist/vite-plugins/virtual-module.d.ts +2 -2
  222. package/dist/vite-plugins/virtual-module.d.ts.map +1 -1
  223. package/dist/vite-plugins/virtual-module.js +5 -3
  224. package/dist/vite-plugins/virtual-module.js.map +1 -1
  225. package/dist/vitest-plugin.d.ts.map +1 -1
  226. package/dist/vitest-plugin.js.map +1 -1
  227. package/env.d.ts +21 -61
  228. package/package.json +12 -14
  229. package/src/babel-modules.d.ts +0 -2
  230. package/src/builder.ts +7 -6
  231. package/src/cli.ts +26 -6
  232. package/src/codegen/freshness.ts +38 -25
  233. package/src/codegen/report.ts +10 -2
  234. package/src/codegen/run-codegen.ts +135 -60
  235. package/src/codegen/store-resolver.ts +56 -15
  236. package/src/commands/build.ts +3 -2
  237. package/src/commands/codegen.ts +9 -4
  238. package/src/commands/dev.ts +3 -4
  239. package/src/commands/docs.ts +36 -7
  240. package/src/dev/icon-dir.ts +13 -3
  241. package/src/dev/refresh-tracker.ts +12 -8
  242. package/src/dev/runner-deps.ts +30 -7
  243. package/src/dev/runner-main.ts +4 -3
  244. package/src/dev/runner.ts +100 -41
  245. package/src/dev/schema-dir.ts +7 -2
  246. package/src/dev/supervisor.ts +169 -71
  247. package/src/dev/vite-dev-server.ts +4 -2
  248. package/src/internal/banner.ts +5 -1
  249. package/src/internal/data-dir.ts +47 -16
  250. package/src/internal/entry-arg.ts +16 -8
  251. package/src/internal/errors.ts +15 -7
  252. package/src/internal/list-files.ts +10 -4
  253. package/src/internal/resolve-cli-tool.ts +28 -12
  254. package/src/internal/staging-dir.ts +5 -2
  255. package/src/internal/xdg-data-dirs.ts +8 -2
  256. package/src/mcp/client.ts +143 -85
  257. package/src/mcp/handlers.ts +126 -69
  258. package/src/mcp/index.ts +38 -14
  259. package/src/mcp/serialize-widget.ts +15 -8
  260. package/src/mcp/testing-loader.ts +64 -23
  261. package/src/mcp/widget-registry.ts +11 -5
  262. package/src/refresh-globals.ts +10 -0
  263. package/src/refresh-runtime.ts +20 -16
  264. package/src/settings/compile.ts +7 -4
  265. package/src/settings/parser.ts +69 -30
  266. package/src/settings/render.ts +66 -66
  267. package/src/settings/schema.ts +100 -46
  268. package/src/vite-plugins/asset-extensions.ts +4 -3
  269. package/src/vite-plugins/built-url.ts +30 -15
  270. package/src/vite-plugins/css.ts +35 -20
  271. package/src/vite-plugins/fast-refresh/refresh-filter.ts +18 -10
  272. package/src/vite-plugins/fast-refresh/swc-refresh.ts +60 -47
  273. package/src/vite-plugins/icons.ts +40 -24
  274. package/src/vite-plugins/index.ts +6 -3
  275. package/src/vite-plugins/native.ts +14 -5
  276. package/src/vite-plugins/react-compiler.ts +44 -30
  277. package/src/vite-plugins/react-dom-prebundle.ts +4 -1
  278. package/src/vite-plugins/resource-init-module.ts +41 -38
  279. package/src/vite-plugins/resource-shared.ts +27 -14
  280. package/src/vite-plugins/resources.ts +153 -59
  281. package/src/vite-plugins/settings-worker-env.ts +7 -2
  282. package/src/vite-plugins/settings.ts +107 -42
  283. package/src/vite-plugins/strip-query.ts +7 -0
  284. package/src/vite-plugins/undeclared-library.ts +21 -12
  285. package/src/vite-plugins/virtual-module.ts +9 -2
  286. package/src/vitest-plugin.ts +1 -2
@@ -1,5 +1,3 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
3
1
  import * as Gtk from "@gtkx/gi/gtk";
4
2
  import {
5
3
  invalidRequestError,
@@ -10,147 +8,206 @@ import {
10
8
  ServerRequestParamsSchemas,
11
9
  widgetNotFoundError,
12
10
  } from "@gtkx/mcp/internal";
13
- import { serializeWidget } from "./serialize-widget.js";
14
- import { loadTestingModule } from "./testing-loader.js";
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { dirname } from "node:path";
15
13
  import type { WidgetRegistry } from "./widget-registry.js";
14
+ import { serializeWidget } from "./serialize-widget.js";
15
+ import { loadTestingModule, type TestingModule } from "./testing-loader.js";
16
16
 
17
- export type HandlerContext = {
17
+ type HandlerContext = {
18
18
  app: Gtk.Application;
19
19
  registry: WidgetRegistry;
20
20
  };
21
21
 
22
22
  type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
23
+ type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
23
24
 
24
- const validated = <Params>(
25
+ const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
26
+ "app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], ({ registry }) =>
27
+ Promise.resolve({
28
+ windows: registry.toplevels().map((window) => ({
29
+ id: registry.getOrCreateId(window),
30
+ title: window.getTitle(),
31
+ })),
32
+ })),
33
+ "widget.getTree": validated(ServerRequestParamsSchemas["widget.getTree"], async ({ app, registry }, params) => {
34
+ const testing = await loadTestingModule();
35
+ const container = params.rootId === undefined ? app : requireWidget(registry, params.rootId);
36
+
37
+ return {
38
+ tree: testing.prettyWidget(container, {
39
+ getId: (w) => registry.getOrCreateId(w),
40
+ highlight: false,
41
+ ...((params.maxDepth !== undefined) && { maxDepth: params.maxDepth }),
42
+ }),
43
+ };
44
+ }),
45
+ "widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
46
+ "widget.getProps": validated(ServerRequestParamsSchemas["widget.getProps"], async ({ registry }, params) => {
47
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
48
+
49
+ return serializeWidget(widget, (target) => registry.getOrCreateId(target), testing);
50
+ }),
51
+ "widget.click": validated(ServerRequestParamsSchemas["widget.click"], async ({ registry }, params) => {
52
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
53
+ await testing.userEvent.click(widget);
54
+
55
+ return { success: true };
56
+ }),
57
+ "widget.type": validated(ServerRequestParamsSchemas["widget.type"], async ({ registry }, params) => {
58
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
59
+
60
+ if (params.clear) {
61
+ await testing.userEvent.clear(widget);
62
+ }
63
+
64
+ await testing.userEvent.type(widget, params.text);
65
+
66
+ return { success: true };
67
+ }),
68
+ "widget.fireEvent": validated(ServerRequestParamsSchemas["widget.fireEvent"], async ({ registry }, params) => {
69
+ const { testing, widget } = await widgetTarget(registry, params.widgetId);
70
+ const signalArgs = (params.args ?? []).map((arg) => extractSignalArg(arg));
71
+ await testing.fireEvent(widget, params.signal, ...signalArgs);
72
+
73
+ return { success: true };
74
+ }),
75
+ "widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
76
+ };
77
+
78
+ function validated<Params>(
25
79
  schema: ParamsSchema<Params>,
26
80
  handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
27
- ): ValidatedHandler => {
81
+ ): ValidatedHandler {
28
82
  return (ctx, params) => {
29
83
  const parsed = schema.safeParse(params ?? {});
84
+
30
85
  if (!parsed.success) {
31
86
  throw invalidRequestError(parsed.error.message);
32
87
  }
88
+
33
89
  return handler(ctx, parsed.data);
34
90
  };
35
- };
91
+ }
92
+
93
+ const widgetTarget = async (registry: WidgetRegistry, widgetId: string | undefined): Promise<WidgetTarget> => ({
94
+ testing: await loadTestingModule(),
95
+ widget: requireWidget(registry, widgetId),
96
+ });
36
97
 
37
98
  const requireWidget = (registry: WidgetRegistry, widgetId: string | undefined): Gtk.Widget => {
38
99
  if (widgetId === undefined) {
39
100
  throw widgetNotFoundError("undefined");
40
101
  }
102
+
41
103
  const widget = registry.get(widgetId);
104
+
42
105
  if (!widget) {
43
106
  throw widgetNotFoundError(widgetId);
44
107
  }
108
+
45
109
  return widget;
46
110
  };
47
111
 
48
112
  const extractSignalArg = (arg: unknown): unknown => {
49
113
  const isTypedArg = typeof arg === "object" && arg !== null && "type" in arg && "value" in arg;
114
+
50
115
  return isTypedArg ? (arg as { value: unknown }).value : arg;
51
116
  };
52
117
 
53
- const handleQuery = async (
118
+ const resolveRole = (value: string | number): Gtk.AccessibleRole | undefined => {
119
+ if (typeof value === "number") {
120
+ return value;
121
+ }
122
+
123
+ const resolved = Gtk.AccessibleRole[value.toUpperCase() as keyof typeof Gtk.AccessibleRole];
124
+
125
+ return typeof resolved === "number" ? resolved : undefined;
126
+ };
127
+
128
+ const matchesOrEmpty = async (find: () => Promise<Gtk.Widget[]>): Promise<Gtk.Widget[]> => {
129
+ try {
130
+ return await find();
131
+ } catch {
132
+ return [];
133
+ }
134
+ };
135
+
136
+ async function handleQuery(
54
137
  { app, registry }: HandlerContext,
55
138
  params: ServerRequestParams<"widget.query">,
56
- ): Promise<unknown> => {
139
+ ): Promise<unknown> {
57
140
  const testing = await loadTestingModule();
58
141
  let widgets: Gtk.Widget[] = [];
59
142
 
60
143
  switch (params.by) {
61
144
  case "role": {
62
- const roleValue =
63
- typeof params.value === "string"
64
- ? Gtk.AccessibleRole[params.value as keyof typeof Gtk.AccessibleRole]
65
- : params.value;
66
- widgets = await testing.findAllByRole(app, roleValue as Gtk.AccessibleRole, params.options);
145
+ const roleValue = resolveRole(params.value);
146
+
147
+ if (roleValue === undefined) {
148
+ throw invalidRequestError(
149
+ `Unknown accessible role "${String(params.value)}"; use the lowercase role shown in the ` +
150
+ "widget tree, e.g. \"button\", \"list\", \"list_item\", or \"checkbox\".",
151
+ );
152
+ }
153
+
154
+ widgets = await matchesOrEmpty(() => testing.findAllByRole(app, roleValue, params.options));
67
155
  break;
68
156
  }
69
- case "text":
70
- widgets = await testing.findAllByText(app, String(params.value), params.options);
157
+ case "text": {
158
+ widgets = await matchesOrEmpty(() => testing.findAllByText(app, String(params.value), params.options));
71
159
  break;
72
- case "name":
73
- widgets = await testing.findAllByName(app, String(params.value), params.options);
160
+ }
161
+ case "name": {
162
+ widgets = await matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options));
74
163
  break;
75
- case "labelText":
76
- widgets = await testing.findAllByLabelText(app, String(params.value), params.options);
164
+ }
165
+ case "labelText": {
166
+ widgets = await matchesOrEmpty(() => testing.findAllByLabelText(app, String(params.value), params.options));
77
167
  break;
168
+ }
78
169
  }
79
170
 
80
- return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.idFor(widget), testing)) };
81
- };
171
+ return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.getOrCreateId(widget), testing, 0)) };
172
+ }
82
173
 
83
174
  const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
84
175
  const [toplevel] = registry.toplevels();
176
+
85
177
  if (!toplevel) {
86
178
  throw new Error("No windows available for screenshot");
87
179
  }
180
+
88
181
  return toplevel;
89
182
  };
90
183
 
91
- const handleScreenshot = async (
184
+ async function handleScreenshot(
92
185
  { registry }: HandlerContext,
93
186
  params: ServerRequestParams<"widget.screenshot">,
94
- ): Promise<unknown> => {
187
+ ): Promise<unknown> {
95
188
  const testing = await loadTestingModule();
96
189
  const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
97
190
  const result = await testing.screenshot(target);
191
+
98
192
  if (params.path) {
99
193
  mkdirSync(dirname(params.path), { recursive: true });
100
194
  writeFileSync(params.path, Buffer.from(result.data, "base64"));
195
+
101
196
  return { data: result.data, mimeType: result.mimeType, savedPath: params.path };
102
197
  }
103
- return { data: result.data, mimeType: result.mimeType };
104
- };
105
198
 
106
- const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
107
- "app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], async ({ registry }) => ({
108
- windows: registry.toplevels().map((window) => ({
109
- id: registry.idFor(window),
110
- title: window.getTitle(),
111
- })),
112
- })),
113
- "widget.getTree": validated(ServerRequestParamsSchemas["widget.getTree"], async ({ app, registry }) => {
114
- const testing = await loadTestingModule();
115
- return { tree: testing.prettyWidget(app, { getId: (w) => registry.idFor(w), highlight: false }) };
116
- }),
117
- "widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
118
- "widget.getProps": validated(ServerRequestParamsSchemas["widget.getProps"], async ({ registry }, params) => {
119
- const testing = await loadTestingModule();
120
- const widget = requireWidget(registry, params.widgetId);
121
- return serializeWidget(widget, (target) => registry.idFor(target), testing);
122
- }),
123
- "widget.click": validated(ServerRequestParamsSchemas["widget.click"], async ({ registry }, params) => {
124
- const testing = await loadTestingModule();
125
- const widget = requireWidget(registry, params.widgetId);
126
- await testing.userEvent.click(widget);
127
- return { success: true };
128
- }),
129
- "widget.type": validated(ServerRequestParamsSchemas["widget.type"], async ({ registry }, params) => {
130
- const testing = await loadTestingModule();
131
- const widget = requireWidget(registry, params.widgetId);
132
- if (params.clear) {
133
- await testing.userEvent.clear(widget);
134
- }
135
- await testing.userEvent.type(widget, params.text);
136
- return { success: true };
137
- }),
138
- "widget.fireEvent": validated(ServerRequestParamsSchemas["widget.fireEvent"], async ({ registry }, params) => {
139
- const testing = await loadTestingModule();
140
- const widget = requireWidget(registry, params.widgetId);
141
- const signalArgs = (params.args ?? []).map(extractSignalArg);
142
- await testing.fireEvent(widget, params.signal, ...signalArgs);
143
- return { success: true };
144
- }),
145
- "widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
146
- };
199
+ return { data: result.data, mimeType: result.mimeType };
200
+ }
147
201
 
148
202
  const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
149
203
  Object.hasOwn(ServerRequestParamsSchemas, method);
150
204
 
151
- export const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
205
+ const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
152
206
  if (!isServerInitiatedMethod(method)) {
153
207
  throw methodNotFoundError(method);
154
208
  }
209
+
155
210
  return HANDLERS[method](ctx, params);
156
211
  };
212
+
213
+ export { dispatch, type HandlerContext };
package/src/mcp/index.ts CHANGED
@@ -1,22 +1,46 @@
1
1
  import { McpClient } from "./client.js";
2
2
 
3
- let globalClient: McpClient | null = null;
3
+ type McpClientController = {
4
+ start: (applicationId: string) => Promise<McpClient>;
5
+ stop: () => void;
6
+ };
7
+
8
+ const { start: startMcpClient, stop: stopMcpClient } = createMcpClientController();
4
9
 
5
- export const startMcpClient = async (applicationId: string): Promise<McpClient> => {
6
- if (globalClient) {
7
- return globalClient;
10
+ async function connectQuietly(client: McpClient): Promise<boolean> {
11
+ try {
12
+ await client.connect();
13
+
14
+ return true;
15
+ } catch {
16
+ return false;
8
17
  }
18
+ }
9
19
 
10
- globalClient = new McpClient({ applicationId });
20
+ function createMcpClientController(): McpClientController {
21
+ let current: McpClient | null = null;
11
22
 
12
- await globalClient.connect().catch(() => {});
23
+ return {
24
+ start: async (applicationId: string): Promise<McpClient> => {
25
+ if (current) {
26
+ return current;
27
+ }
13
28
 
14
- return globalClient;
15
- };
29
+ const client = new McpClient({ applicationId });
30
+ current = client;
31
+ await connectQuietly(client);
16
32
 
17
- export const stopMcpClient = (): void => {
18
- if (globalClient) {
19
- globalClient.disconnect();
20
- globalClient = null;
21
- }
22
- };
33
+ return client;
34
+ },
35
+ stop: (): void => {
36
+ if (!current) {
37
+ return;
38
+ }
39
+
40
+ current.disconnect();
41
+ current = null;
42
+ },
43
+ };
44
+ }
45
+
46
+ export { startMcpClient, stopMcpClient };
@@ -3,25 +3,30 @@ import type { SerializedWidget } from "@gtkx/mcp/internal";
3
3
 
4
4
  type WidgetIdResolver = (widget: Gtk.Widget) => string;
5
5
 
6
- export type WidgetFormatting = {
6
+ type WidgetFormatting = {
7
7
  formatRole(role: Gtk.AccessibleRole): string;
8
8
  getWidgetNodeText(widget: Gtk.Widget): string | null;
9
9
  };
10
10
 
11
- export const serializeWidget = (
11
+ const serializeWidget = (
12
12
  widget: Gtk.Widget,
13
- idFor: WidgetIdResolver,
13
+ resolveId: WidgetIdResolver,
14
14
  testing: WidgetFormatting,
15
+ maxDepth = Infinity,
15
16
  ): SerializedWidget => {
16
17
  const children: SerializedWidget[] = [];
17
- let child = widget.getFirstChild();
18
- while (child) {
19
- children.push(serializeWidget(child, idFor, testing));
20
- child = child.getNextSibling();
18
+
19
+ if (maxDepth > 0) {
20
+ let child = widget.getFirstChild();
21
+
22
+ while (child) {
23
+ children.push(serializeWidget(child, resolveId, testing, maxDepth - 1));
24
+ child = child.getNextSibling();
25
+ }
21
26
  }
22
27
 
23
28
  return {
24
- id: idFor(widget),
29
+ id: resolveId(widget),
25
30
  type: widget.constructor.name,
26
31
  role: testing.formatRole(widget.getAccessibleRole()),
27
32
  name: widget.getName() || null,
@@ -32,3 +37,5 @@ export const serializeWidget = (
32
37
  children,
33
38
  };
34
39
  };
40
+
41
+ export { serializeWidget, type WidgetFormatting };
@@ -1,32 +1,73 @@
1
1
  type TestingModule = typeof import("@gtkx/testing");
2
+ type TestingModuleLoader = () => Promise<TestingModule>;
2
3
 
3
- export type TestingModuleLoader = () => Promise<TestingModule>;
4
+ type TestingModuleCache = {
5
+ setLoader: (next: TestingModuleLoader | null) => void;
6
+ load: () => Promise<TestingModule>;
7
+ };
8
+
9
+ type TestingModuleState = {
10
+ loader: TestingModuleLoader;
11
+ testingModule: TestingModule | null;
12
+ testingLoadError: Error | null;
13
+ };
4
14
 
5
- const defaultLoader: TestingModuleLoader = () => import("@gtkx/testing");
15
+ const { setLoader: setTestingModuleLoader, load: loadTestingModule } = createTestingModuleCache();
6
16
 
7
- let loader: TestingModuleLoader = defaultLoader;
8
- let testingModule: TestingModule | null = null;
9
- let testingLoadError: Error | null = null;
17
+ function defaultLoader(): Promise<TestingModule> {
18
+ return import("@gtkx/testing");
19
+ }
10
20
 
11
- export const setTestingModuleLoader = (next: TestingModuleLoader | null): void => {
12
- loader = next ?? defaultLoader;
13
- testingModule = null;
14
- testingLoadError = null;
15
- };
21
+ function missingTestingPackageError(cause: unknown): Error {
22
+ return new Error(
23
+ "@gtkx/testing is not installed, install it to enable MCP widget interactions: " +
24
+ `pnpm add -D @gtkx/testing (import failed: ${String(cause)})`,
25
+ { cause },
26
+ );
27
+ }
16
28
 
17
- export const loadTestingModule = async (): Promise<TestingModule> => {
18
- if (testingModule) return testingModule;
19
- if (testingLoadError) throw testingLoadError;
29
+ function replaceLoader(state: TestingModuleState, next: TestingModuleLoader | null): void {
30
+ state.loader = next ?? defaultLoader;
31
+ state.testingModule = null;
32
+ state.testingLoadError = null;
33
+ }
20
34
 
35
+ async function importTestingModule(state: TestingModuleState): Promise<TestingModule> {
21
36
  try {
22
- testingModule = await loader();
23
- return testingModule;
24
- } catch (cause) {
25
- testingLoadError = new Error(
26
- "@gtkx/testing is not installed, install it to enable MCP widget interactions: " +
27
- `pnpm add -D @gtkx/testing (import failed: ${String(cause)})`,
28
- { cause },
29
- );
30
- throw testingLoadError;
37
+ state.testingModule = await state.loader();
38
+
39
+ return state.testingModule;
40
+ } catch (error) {
41
+ state.testingLoadError = missingTestingPackageError(error);
42
+ throw state.testingLoadError;
31
43
  }
32
- };
44
+ }
45
+
46
+ function loadCachedTestingModule(state: TestingModuleState): Promise<TestingModule> {
47
+ if (state.testingModule) {
48
+ return Promise.resolve(state.testingModule);
49
+ }
50
+
51
+ if (state.testingLoadError) {
52
+ return Promise.reject(state.testingLoadError);
53
+ }
54
+
55
+ return importTestingModule(state);
56
+ }
57
+
58
+ function createTestingModuleCache(): TestingModuleCache {
59
+ const state: TestingModuleState = {
60
+ loader: defaultLoader,
61
+ testingModule: null,
62
+ testingLoadError: null,
63
+ };
64
+
65
+ return {
66
+ setLoader: (next: TestingModuleLoader | null): void => {
67
+ replaceLoader(state, next);
68
+ },
69
+ load: (): Promise<TestingModule> => loadCachedTestingModule(state),
70
+ };
71
+ }
72
+
73
+ export { setTestingModuleLoader, loadTestingModule, type TestingModule, type TestingModuleLoader };
@@ -1,14 +1,15 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
2
 
3
- export class WidgetRegistry {
4
- private idByWidget = new WeakMap<Gtk.Widget, string>();
3
+ class WidgetRegistry {
4
+ private idByWidget: WeakMap<Gtk.Widget, string> = new WeakMap();
5
5
  private nextId = 0;
6
- private widgetById = new Map<string, Gtk.Widget>();
6
+ private widgetById: Map<string, Gtk.Widget> = new Map();
7
7
  private toplevelWindows: Gtk.Window[] = [];
8
8
 
9
9
  refresh(): void {
10
10
  this.widgetById.clear();
11
11
  this.toplevelWindows = Gtk.Window.listToplevels() as Gtk.Window[];
12
+
12
13
  for (const window of this.toplevelWindows) {
13
14
  this.register(window);
14
15
  }
@@ -19,21 +20,24 @@ export class WidgetRegistry {
19
20
  }
20
21
 
21
22
  register(widget: Gtk.Widget): void {
22
- const id = this.idFor(widget);
23
+ const id = this.getOrCreateId(widget);
23
24
  this.widgetById.set(id, widget);
24
25
  let child = widget.getFirstChild();
26
+
25
27
  while (child) {
26
28
  this.register(child);
27
29
  child = child.getNextSibling();
28
30
  }
29
31
  }
30
32
 
31
- idFor(widget: Gtk.Widget): string {
33
+ getOrCreateId(widget: Gtk.Widget): string {
32
34
  let id = this.idByWidget.get(widget);
35
+
33
36
  if (!id) {
34
37
  id = String(this.nextId++);
35
38
  this.idByWidget.set(widget, id);
36
39
  }
40
+
37
41
  return id;
38
42
  }
39
43
 
@@ -41,3 +45,5 @@ export class WidgetRegistry {
41
45
  return this.widgetById.get(id);
42
46
  }
43
47
  }
48
+
49
+ export { WidgetRegistry };
@@ -0,0 +1,10 @@
1
+ import RefreshRuntime from "react-refresh/runtime";
2
+
3
+ const identity = (type: unknown): unknown => type;
4
+
5
+ RefreshRuntime.injectIntoGlobalHook(globalThis);
6
+
7
+ Object.assign(globalThis, {
8
+ $RefreshReg$: (): void => undefined,
9
+ $RefreshSig$: () => identity,
10
+ });
@@ -1,11 +1,8 @@
1
1
  import RefreshRuntime from "react-refresh/runtime";
2
+ import "./refresh-globals.js";
2
3
 
3
4
  type ComponentType = (...args: unknown[]) => unknown;
4
5
 
5
- RefreshRuntime.injectIntoGlobalHook(globalThis);
6
- globalThis.$RefreshReg$ = () => {};
7
- globalThis.$RefreshSig$ = () => (type: unknown) => type;
8
-
9
6
  /**
10
7
  * Builds the per-module `$RefreshReg$` and `$RefreshSig$` hooks that React Refresh
11
8
  * transformed code expects, scoping every registered component to the given module.
@@ -13,7 +10,7 @@ globalThis.$RefreshSig$ = () => (type: unknown) => type;
13
10
  * @param moduleId Identifier used to namespace registered component types so they are unique across modules.
14
11
  * @returns An object with `$RefreshReg$` and `$RefreshSig$` to install as module-local globals.
15
12
  */
16
- export function createModuleRegistration(moduleId: string): {
13
+ function createModuleRegistration(moduleId: string): {
17
14
  $RefreshReg$: (type: ComponentType, id: string) => void;
18
15
  $RefreshSig$: typeof RefreshRuntime.createSignatureFunctionForTransform;
19
16
  } {
@@ -32,21 +29,26 @@ export function createModuleRegistration(moduleId: string): {
32
29
  * @param moduleExports The module's export object to inspect.
33
30
  * @returns `true` when the module qualifies as a refresh boundary, `false` otherwise.
34
31
  */
35
- export function isRefreshBoundary(moduleExports: Record<string, unknown>): boolean {
36
- if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
37
- return true;
38
- }
32
+ const isComponentExport = (key: string, value: unknown): boolean =>
33
+ key === "__esModule" || RefreshRuntime.isLikelyComponentType(value);
39
34
 
35
+ const areAllExportsComponents = (moduleExports: Record<string, unknown>): boolean => {
40
36
  for (const key in moduleExports) {
41
- if (key === "__esModule") {
42
- continue;
37
+ if (!isComponentExport(key, moduleExports[key])) {
38
+ return false;
43
39
  }
40
+ }
44
41
 
45
- const value = moduleExports[key];
42
+ return true;
43
+ };
46
44
 
47
- if (!RefreshRuntime.isLikelyComponentType(value)) {
48
- return false;
49
- }
45
+ function isRefreshBoundary(moduleExports: Record<string, unknown>): boolean {
46
+ if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
47
+ return true;
48
+ }
49
+
50
+ if (!areAllExportsComponents(moduleExports)) {
51
+ return false;
50
52
  }
51
53
 
52
54
  return Object.keys(moduleExports).some((k) => k !== "__esModule");
@@ -55,6 +57,8 @@ export function isRefreshBoundary(moduleExports: Record<string, unknown>): boole
55
57
  /**
56
58
  * Applies all pending React Refresh updates, re-rendering components whose modules have changed.
57
59
  */
58
- export function performRefresh(): void {
60
+ function performRefresh(): void {
59
61
  RefreshRuntime.performReactRefresh();
60
62
  }
63
+
64
+ export { createModuleRegistration, isRefreshBoundary, performRefresh };
@@ -1,19 +1,22 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { formatChildProcessError } from "@gtkx/utils";
2
+ import { execFileSync } from "node:child_process";
3
3
  import { resolveCliTool } from "../internal/resolve-cli-tool.js";
4
4
 
5
5
  const SCHEMA_COMPILER = "glib-compile-schemas";
6
6
  const SCHEMA_COMPILE_TIMEOUT_MS = 30_000;
7
7
 
8
- export const compileSchemas = (dir: string): void => {
8
+ const compileSchemas = (dir: string): void => {
9
9
  try {
10
10
  execFileSync(resolveCliTool(SCHEMA_COMPILER), ["--strict", dir], {
11
11
  stdio: ["ignore", "pipe", "pipe"],
12
12
  timeout: SCHEMA_COMPILE_TIMEOUT_MS,
13
- encoding: "utf-8",
13
+ encoding: "utf8",
14
14
  });
15
15
  } catch (error) {
16
16
  const details = formatChildProcessError(error);
17
- throw new Error(`glib-compile-schemas failed for ${dir}${details ? `:\n${details}` : ""}`, { cause: error });
17
+ const suffix = details ? `:\n${details}` : "";
18
+ throw new Error(`glib-compile-schemas failed for ${dir}${suffix}`, { cause: error });
18
19
  }
19
20
  };
21
+
22
+ export { compileSchemas };