@gtkx/cli 0.21.0 → 1.0.0-rc.1

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 (346) hide show
  1. package/README.md +173 -0
  2. package/bin/gtkx-dev-runner.js +9 -0
  3. package/dist/builder.d.ts +2 -50
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +22 -56
  6. package/dist/builder.js.map +1 -1
  7. package/dist/cli.d.ts +1 -2
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +11 -124
  10. package/dist/cli.js.map +1 -1
  11. package/dist/codegen/freshness.d.ts +11 -0
  12. package/dist/codegen/freshness.d.ts.map +1 -0
  13. package/dist/codegen/freshness.js +42 -0
  14. package/dist/codegen/freshness.js.map +1 -0
  15. package/dist/codegen/report.d.ts +3 -0
  16. package/dist/codegen/report.d.ts.map +1 -0
  17. package/dist/codegen/report.js +16 -0
  18. package/dist/codegen/report.js.map +1 -0
  19. package/dist/codegen/run-codegen.d.ts +35 -0
  20. package/dist/codegen/run-codegen.d.ts.map +1 -0
  21. package/dist/codegen/run-codegen.js +133 -0
  22. package/dist/codegen/run-codegen.js.map +1 -0
  23. package/dist/codegen/store-resolver.d.ts +21 -0
  24. package/dist/codegen/store-resolver.d.ts.map +1 -0
  25. package/dist/codegen/store-resolver.js +52 -0
  26. package/dist/codegen/store-resolver.js.map +1 -0
  27. package/dist/commands/build.d.ts +9 -0
  28. package/dist/commands/build.d.ts.map +1 -0
  29. package/dist/commands/build.js +33 -0
  30. package/dist/commands/build.js.map +1 -0
  31. package/dist/commands/codegen.d.ts +9 -0
  32. package/dist/commands/codegen.d.ts.map +1 -0
  33. package/dist/commands/codegen.js +40 -0
  34. package/dist/commands/codegen.js.map +1 -0
  35. package/dist/commands/dev.d.ts +5 -0
  36. package/dist/commands/dev.d.ts.map +1 -0
  37. package/dist/commands/dev.js +21 -0
  38. package/dist/commands/dev.js.map +1 -0
  39. package/dist/commands/docs.d.ts +19 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +59 -0
  42. package/dist/commands/docs.js.map +1 -0
  43. package/dist/dev/icon-dir.d.ts +2 -0
  44. package/dist/dev/icon-dir.d.ts.map +1 -0
  45. package/dist/dev/icon-dir.js +13 -0
  46. package/dist/dev/icon-dir.js.map +1 -0
  47. package/dist/dev/refresh-tracker.d.ts +6 -0
  48. package/dist/dev/refresh-tracker.d.ts.map +1 -0
  49. package/dist/dev/refresh-tracker.js +18 -0
  50. package/dist/dev/refresh-tracker.js.map +1 -0
  51. package/dist/dev/runner-deps.d.ts +3 -0
  52. package/dist/dev/runner-deps.d.ts.map +1 -0
  53. package/dist/dev/runner-deps.js +39 -0
  54. package/dist/dev/runner-deps.js.map +1 -0
  55. package/dist/dev/runner-main.d.ts +2 -0
  56. package/dist/dev/runner-main.d.ts.map +1 -0
  57. package/dist/dev/runner-main.js +28 -0
  58. package/dist/dev/runner-main.js.map +1 -0
  59. package/dist/dev/runner.d.ts +23 -0
  60. package/dist/dev/runner.d.ts.map +1 -0
  61. package/dist/dev/runner.js +88 -0
  62. package/dist/dev/runner.js.map +1 -0
  63. package/dist/dev/schema-dir.d.ts +2 -0
  64. package/dist/dev/schema-dir.d.ts.map +1 -0
  65. package/dist/dev/schema-dir.js +10 -0
  66. package/dist/dev/schema-dir.js.map +1 -0
  67. package/dist/dev/supervisor.d.ts +17 -0
  68. package/dist/dev/supervisor.d.ts.map +1 -0
  69. package/dist/dev/supervisor.js +142 -0
  70. package/dist/dev/supervisor.js.map +1 -0
  71. package/dist/dev/vite-dev-server.d.ts +21 -0
  72. package/dist/dev/vite-dev-server.d.ts.map +1 -0
  73. package/dist/dev/vite-dev-server.js +12 -0
  74. package/dist/dev/vite-dev-server.js.map +1 -0
  75. package/dist/internal/banner.d.ts +3 -0
  76. package/dist/internal/banner.d.ts.map +1 -0
  77. package/dist/internal/banner.js +8 -0
  78. package/dist/internal/banner.js.map +1 -0
  79. package/dist/internal/data-dir.d.ts +3 -0
  80. package/dist/internal/data-dir.d.ts.map +1 -0
  81. package/dist/internal/data-dir.js +37 -0
  82. package/dist/internal/data-dir.js.map +1 -0
  83. package/dist/internal/entry-arg.d.ts +19 -0
  84. package/dist/internal/entry-arg.d.ts.map +1 -0
  85. package/dist/internal/entry-arg.js +33 -0
  86. package/dist/internal/entry-arg.js.map +1 -0
  87. package/dist/internal/errors.d.ts +4 -0
  88. package/dist/internal/errors.d.ts.map +1 -0
  89. package/dist/internal/errors.js +27 -0
  90. package/dist/internal/errors.js.map +1 -0
  91. package/dist/internal/list-files.d.ts +6 -0
  92. package/dist/internal/list-files.d.ts.map +1 -0
  93. package/dist/internal/list-files.js +13 -0
  94. package/dist/internal/list-files.js.map +1 -0
  95. package/dist/internal/resolve-cli-tool.d.ts +2 -0
  96. package/dist/internal/resolve-cli-tool.d.ts.map +1 -0
  97. package/dist/internal/resolve-cli-tool.js +20 -0
  98. package/dist/internal/resolve-cli-tool.js.map +1 -0
  99. package/dist/internal/staging-dir.d.ts +3 -0
  100. package/dist/internal/staging-dir.d.ts.map +1 -0
  101. package/dist/internal/staging-dir.js +16 -0
  102. package/dist/internal/staging-dir.js.map +1 -0
  103. package/dist/internal/xdg-data-dirs.d.ts +2 -0
  104. package/dist/internal/xdg-data-dirs.d.ts.map +1 -0
  105. package/dist/internal/xdg-data-dirs.js +8 -0
  106. package/dist/internal/xdg-data-dirs.js.map +1 -0
  107. package/dist/mcp/client.d.ts +23 -0
  108. package/dist/mcp/client.d.ts.map +1 -0
  109. package/dist/mcp/client.js +157 -0
  110. package/dist/mcp/client.js.map +1 -0
  111. package/dist/mcp/handlers.d.ts +8 -0
  112. package/dist/mcp/handlers.d.ts.map +1 -0
  113. package/dist/mcp/handlers.js +119 -0
  114. package/dist/mcp/handlers.js.map +1 -0
  115. package/dist/mcp/index.d.ts +4 -0
  116. package/dist/mcp/index.d.ts.map +1 -0
  117. package/dist/mcp/index.js +17 -0
  118. package/dist/mcp/index.js.map +1 -0
  119. package/dist/mcp/serialize-widget.d.ts +10 -0
  120. package/dist/mcp/serialize-widget.d.ts.map +1 -0
  121. package/dist/mcp/serialize-widget.js +20 -0
  122. package/dist/mcp/serialize-widget.js.map +1 -0
  123. package/dist/mcp/testing-loader.d.ts +6 -0
  124. package/dist/mcp/testing-loader.d.ts.map +1 -0
  125. package/dist/mcp/testing-loader.js +25 -0
  126. package/dist/mcp/testing-loader.js.map +1 -0
  127. package/dist/mcp/widget-registry.d.ts +13 -0
  128. package/dist/mcp/widget-registry.d.ts.map +1 -0
  129. package/dist/mcp/widget-registry.js +38 -0
  130. package/dist/mcp/widget-registry.js.map +1 -0
  131. package/dist/refresh-runtime.d.ts +10 -20
  132. package/dist/refresh-runtime.d.ts.map +1 -1
  133. package/dist/refresh-runtime.js +12 -36
  134. package/dist/refresh-runtime.js.map +1 -1
  135. package/dist/settings/compile.d.ts +2 -0
  136. package/dist/settings/compile.d.ts.map +1 -0
  137. package/dist/settings/compile.js +19 -0
  138. package/dist/settings/compile.js.map +1 -0
  139. package/dist/settings/parser.d.ts +23 -0
  140. package/dist/settings/parser.d.ts.map +1 -0
  141. package/dist/settings/parser.js +117 -0
  142. package/dist/settings/parser.js.map +1 -0
  143. package/dist/settings/render.d.ts +4 -0
  144. package/dist/settings/render.d.ts.map +1 -0
  145. package/dist/settings/render.js +148 -0
  146. package/dist/settings/render.js.map +1 -0
  147. package/dist/settings/schema.d.ts +13 -0
  148. package/dist/settings/schema.d.ts.map +1 -0
  149. package/dist/settings/schema.js +103 -0
  150. package/dist/settings/schema.js.map +1 -0
  151. package/dist/vite-plugins/asset-extensions.d.ts +4 -0
  152. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -0
  153. package/dist/vite-plugins/asset-extensions.js +27 -0
  154. package/dist/vite-plugins/asset-extensions.js.map +1 -0
  155. package/dist/vite-plugins/built-url.d.ts +3 -0
  156. package/dist/vite-plugins/built-url.d.ts.map +1 -0
  157. package/dist/{vite-plugin-gtkx-built-url.js → vite-plugins/built-url.js} +4 -17
  158. package/dist/vite-plugins/built-url.js.map +1 -0
  159. package/dist/vite-plugins/css.d.ts +3 -0
  160. package/dist/vite-plugins/css.d.ts.map +1 -0
  161. package/dist/vite-plugins/css.js +30 -0
  162. package/dist/vite-plugins/css.js.map +1 -0
  163. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts +7 -0
  164. package/dist/vite-plugins/fast-refresh/refresh-filter.d.ts.map +1 -0
  165. package/dist/vite-plugins/fast-refresh/refresh-filter.js +15 -0
  166. package/dist/vite-plugins/fast-refresh/refresh-filter.js.map +1 -0
  167. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts +5 -0
  168. package/dist/vite-plugins/fast-refresh/swc-refresh.d.ts.map +1 -0
  169. package/dist/vite-plugins/fast-refresh/swc-refresh.js +66 -0
  170. package/dist/vite-plugins/fast-refresh/swc-refresh.js.map +1 -0
  171. package/dist/vite-plugins/icons.d.ts +3 -0
  172. package/dist/vite-plugins/icons.d.ts.map +1 -0
  173. package/dist/vite-plugins/icons.js +47 -0
  174. package/dist/vite-plugins/icons.js.map +1 -0
  175. package/dist/vite-plugins/index.d.ts +3 -0
  176. package/dist/vite-plugins/index.d.ts.map +1 -0
  177. package/dist/vite-plugins/index.js +21 -0
  178. package/dist/vite-plugins/index.js.map +1 -0
  179. package/dist/vite-plugins/native.d.ts +3 -0
  180. package/dist/vite-plugins/native.d.ts.map +1 -0
  181. package/dist/vite-plugins/native.js +60 -0
  182. package/dist/vite-plugins/native.js.map +1 -0
  183. package/dist/vite-plugins/react-compiler.d.ts +4 -0
  184. package/dist/vite-plugins/react-compiler.d.ts.map +1 -0
  185. package/dist/vite-plugins/react-compiler.js +56 -0
  186. package/dist/vite-plugins/react-compiler.js.map +1 -0
  187. package/dist/vite-plugins/react-dom-prebundle.d.ts +3 -0
  188. package/dist/vite-plugins/react-dom-prebundle.d.ts.map +1 -0
  189. package/dist/vite-plugins/react-dom-prebundle.js +11 -0
  190. package/dist/vite-plugins/react-dom-prebundle.js.map +1 -0
  191. package/dist/vite-plugins/resource-init-module.d.ts +5 -0
  192. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -0
  193. package/dist/vite-plugins/resource-init-module.js +50 -0
  194. package/dist/vite-plugins/resource-init-module.js.map +1 -0
  195. package/dist/vite-plugins/resource-shared.d.ts +6 -0
  196. package/dist/vite-plugins/resource-shared.d.ts.map +1 -0
  197. package/dist/vite-plugins/resource-shared.js +21 -0
  198. package/dist/vite-plugins/resource-shared.js.map +1 -0
  199. package/dist/vite-plugins/resources.d.ts +4 -0
  200. package/dist/vite-plugins/resources.d.ts.map +1 -0
  201. package/dist/vite-plugins/resources.js +220 -0
  202. package/dist/vite-plugins/resources.js.map +1 -0
  203. package/dist/vite-plugins/settings-worker-env.d.ts +3 -0
  204. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -0
  205. package/dist/vite-plugins/settings-worker-env.js +21 -0
  206. package/dist/vite-plugins/settings-worker-env.js.map +1 -0
  207. package/dist/vite-plugins/settings.d.ts +3 -0
  208. package/dist/vite-plugins/settings.d.ts.map +1 -0
  209. package/dist/vite-plugins/settings.js +190 -0
  210. package/dist/vite-plugins/settings.js.map +1 -0
  211. package/dist/vite-plugins/undeclared-library.d.ts +3 -0
  212. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -0
  213. package/dist/vite-plugins/undeclared-library.js +47 -0
  214. package/dist/vite-plugins/undeclared-library.js.map +1 -0
  215. package/dist/vite-plugins/virtual-module.d.ts +22 -0
  216. package/dist/vite-plugins/virtual-module.d.ts.map +1 -0
  217. package/dist/vite-plugins/virtual-module.js +18 -0
  218. package/dist/vite-plugins/virtual-module.js.map +1 -0
  219. package/dist/vitest-plugin.d.ts +10 -0
  220. package/dist/vitest-plugin.d.ts.map +1 -0
  221. package/dist/vitest-plugin.js +12 -0
  222. package/dist/vitest-plugin.js.map +1 -0
  223. package/env.d.ts +131 -8
  224. package/package.json +32 -38
  225. package/src/babel-modules.d.ts +13 -0
  226. package/src/builder.ts +28 -84
  227. package/src/cli.ts +22 -0
  228. package/src/codegen/freshness.ts +52 -0
  229. package/src/codegen/report.ts +20 -0
  230. package/src/codegen/run-codegen.ts +185 -0
  231. package/src/codegen/store-resolver.ts +75 -0
  232. package/src/commands/build.ts +37 -0
  233. package/src/commands/codegen.ts +44 -0
  234. package/src/commands/dev.ts +25 -0
  235. package/src/commands/docs.ts +66 -0
  236. package/src/dev/icon-dir.ts +11 -0
  237. package/src/dev/refresh-tracker.ts +21 -0
  238. package/src/dev/runner-deps.ts +42 -0
  239. package/src/dev/runner-main.ts +33 -0
  240. package/src/dev/runner.ts +126 -0
  241. package/src/dev/schema-dir.ts +11 -0
  242. package/src/dev/supervisor.ts +178 -0
  243. package/src/dev/vite-dev-server.ts +34 -0
  244. package/src/internal/banner.ts +9 -0
  245. package/src/internal/data-dir.ts +39 -0
  246. package/src/internal/entry-arg.ts +38 -0
  247. package/src/internal/errors.ts +28 -0
  248. package/src/internal/list-files.ts +17 -0
  249. package/src/internal/resolve-cli-tool.ts +21 -0
  250. package/src/internal/staging-dir.ts +16 -0
  251. package/src/internal/xdg-data-dirs.ts +7 -0
  252. package/src/mcp/client.ts +177 -0
  253. package/src/mcp/handlers.ts +156 -0
  254. package/src/mcp/index.ts +22 -0
  255. package/src/mcp/serialize-widget.ts +34 -0
  256. package/src/mcp/testing-loader.ts +32 -0
  257. package/src/mcp/widget-registry.ts +43 -0
  258. package/src/refresh-runtime.ts +12 -41
  259. package/src/settings/compile.ts +19 -0
  260. package/src/settings/parser.ts +154 -0
  261. package/src/settings/render.ts +167 -0
  262. package/src/settings/schema.ts +115 -0
  263. package/src/vite-plugins/asset-extensions.ts +28 -0
  264. package/src/{vite-plugin-gtkx-built-url.ts → vite-plugins/built-url.ts} +3 -16
  265. package/src/vite-plugins/css.ts +38 -0
  266. package/src/vite-plugins/fast-refresh/refresh-filter.ts +18 -0
  267. package/src/vite-plugins/fast-refresh/swc-refresh.ts +79 -0
  268. package/src/vite-plugins/icons.ts +58 -0
  269. package/src/vite-plugins/index.ts +22 -0
  270. package/src/vite-plugins/native.ts +74 -0
  271. package/src/vite-plugins/react-compiler.ts +70 -0
  272. package/src/vite-plugins/react-dom-prebundle.ts +14 -0
  273. package/src/vite-plugins/resource-init-module.ts +54 -0
  274. package/src/vite-plugins/resource-shared.ts +27 -0
  275. package/src/vite-plugins/resources.ts +272 -0
  276. package/src/vite-plugins/settings-worker-env.ts +25 -0
  277. package/src/vite-plugins/settings.ts +221 -0
  278. package/src/vite-plugins/undeclared-library.ts +69 -0
  279. package/src/vite-plugins/virtual-module.ts +37 -0
  280. package/src/vitest-plugin.ts +14 -0
  281. package/dist/create.d.ts +0 -55
  282. package/dist/create.d.ts.map +0 -1
  283. package/dist/create.js +0 -236
  284. package/dist/create.js.map +0 -1
  285. package/dist/dev-server.d.ts +0 -37
  286. package/dist/dev-server.d.ts.map +0 -1
  287. package/dist/dev-server.js +0 -130
  288. package/dist/dev-server.js.map +0 -1
  289. package/dist/index.d.ts +0 -4
  290. package/dist/index.d.ts.map +0 -1
  291. package/dist/index.js +0 -4
  292. package/dist/index.js.map +0 -1
  293. package/dist/mcp-client.d.ts +0 -31
  294. package/dist/mcp-client.d.ts.map +0 -1
  295. package/dist/mcp-client.js +0 -430
  296. package/dist/mcp-client.js.map +0 -1
  297. package/dist/templates.d.ts +0 -9
  298. package/dist/templates.d.ts.map +0 -1
  299. package/dist/templates.js +0 -16
  300. package/dist/templates.js.map +0 -1
  301. package/dist/vite-plugin-gtkx-assets.d.ts +0 -20
  302. package/dist/vite-plugin-gtkx-assets.d.ts.map +0 -1
  303. package/dist/vite-plugin-gtkx-assets.js +0 -57
  304. package/dist/vite-plugin-gtkx-assets.js.map +0 -1
  305. package/dist/vite-plugin-gtkx-built-url.d.ts +0 -18
  306. package/dist/vite-plugin-gtkx-built-url.d.ts.map +0 -1
  307. package/dist/vite-plugin-gtkx-built-url.js.map +0 -1
  308. package/dist/vite-plugin-gtkx-gsettings.d.ts +0 -28
  309. package/dist/vite-plugin-gtkx-gsettings.d.ts.map +0 -1
  310. package/dist/vite-plugin-gtkx-gsettings.js +0 -147
  311. package/dist/vite-plugin-gtkx-gsettings.js.map +0 -1
  312. package/dist/vite-plugin-gtkx-native.d.ts +0 -14
  313. package/dist/vite-plugin-gtkx-native.d.ts.map +0 -1
  314. package/dist/vite-plugin-gtkx-native.js +0 -53
  315. package/dist/vite-plugin-gtkx-native.js.map +0 -1
  316. package/dist/vite-plugin-gtkx-refresh.d.ts +0 -8
  317. package/dist/vite-plugin-gtkx-refresh.d.ts.map +0 -1
  318. package/dist/vite-plugin-gtkx-refresh.js +0 -37
  319. package/dist/vite-plugin-gtkx-refresh.js.map +0 -1
  320. package/dist/vite-plugin-swc-ssr-refresh.d.ts +0 -8
  321. package/dist/vite-plugin-swc-ssr-refresh.d.ts.map +0 -1
  322. package/dist/vite-plugin-swc-ssr-refresh.js +0 -46
  323. package/dist/vite-plugin-swc-ssr-refresh.js.map +0 -1
  324. package/src/cli.tsx +0 -154
  325. package/src/create.ts +0 -326
  326. package/src/dev-server.tsx +0 -162
  327. package/src/index.ts +0 -3
  328. package/src/mcp-client.ts +0 -518
  329. package/src/templates.ts +0 -26
  330. package/src/vite-plugin-gtkx-assets.ts +0 -69
  331. package/src/vite-plugin-gtkx-gsettings.ts +0 -168
  332. package/src/vite-plugin-gtkx-native.ts +0 -64
  333. package/src/vite-plugin-gtkx-refresh.ts +0 -54
  334. package/src/vite-plugin-swc-ssr-refresh.ts +0 -61
  335. package/templates/claude/EXAMPLES.md.ejs +0 -744
  336. package/templates/claude/SKILL.md.ejs +0 -93
  337. package/templates/claude/WIDGETS.md.ejs +0 -820
  338. package/templates/config/vitest.config.ts.ejs +0 -11
  339. package/templates/gitignore.ejs +0 -4
  340. package/templates/package.json.ejs +0 -16
  341. package/templates/src/app.tsx.ejs +0 -32
  342. package/templates/src/dev.tsx.ejs +0 -5
  343. package/templates/src/gtkx-env.d.ts.ejs +0 -1
  344. package/templates/src/index.tsx.ejs +0 -5
  345. package/templates/tests/app.test.tsx.ejs +0 -16
  346. package/templates/tsconfig.json.ejs +0 -14
@@ -0,0 +1,177 @@
1
+ import * as net from "node:net";
2
+ import * as Gio from "@gtkx/gi/gio";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { DEFAULT_SOCKET_PATH, ErrorCode, ProtocolConnection, ProtocolError, type Request } from "@gtkx/mcp/internal";
5
+ import { error, errorMessage, info, normalizeError, warn } from "@gtkx/utils";
6
+ import { dispatch } from "./handlers.js";
7
+ import { WidgetRegistry } from "./widget-registry.js";
8
+
9
+ export type McpClientOptions = {
10
+ socketPath?: string;
11
+ applicationId: string;
12
+ };
13
+
14
+ const RECONNECT_DELAY_MS = 2000;
15
+ const REGISTER_TIMEOUT_MS = 30000;
16
+
17
+ export class McpClient {
18
+ private socket: net.Socket | null = null;
19
+ private connection: ProtocolConnection | null = null;
20
+ private socketPath: string;
21
+ private applicationId: string;
22
+ private reconnectTimer: NodeJS.Timeout | null = null;
23
+ private hasConnected = false;
24
+ private isStopping = false;
25
+ private pendingConnectReject: ((error: Error) => void) | null = null;
26
+ private registry = new WidgetRegistry();
27
+
28
+ constructor(options: McpClientOptions) {
29
+ this.socketPath = options.socketPath ?? DEFAULT_SOCKET_PATH;
30
+ this.applicationId = options.applicationId;
31
+ }
32
+
33
+ async connect(): Promise<void> {
34
+ return new Promise<void>((resolve, reject) => {
35
+ this.pendingConnectReject = reject;
36
+ this.attemptConnect(
37
+ () => {
38
+ this.pendingConnectReject = null;
39
+ resolve();
40
+ },
41
+ (error) => {
42
+ this.pendingConnectReject = null;
43
+ reject(error);
44
+ },
45
+ );
46
+ });
47
+ }
48
+
49
+ disconnect(): void {
50
+ this.isStopping = true;
51
+ if (this.pendingConnectReject) {
52
+ this.pendingConnectReject(new Error("Client disconnected before connection registered"));
53
+ this.pendingConnectReject = null;
54
+ }
55
+ if (this.reconnectTimer) {
56
+ clearTimeout(this.reconnectTimer);
57
+ this.reconnectTimer = null;
58
+ }
59
+ this.connection?.rejectPending(new Error("Client disconnected"));
60
+ if (this.socket) {
61
+ this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
62
+ this.socket.destroy();
63
+ this.socket = null;
64
+ }
65
+ this.connection = null;
66
+ this.hasConnected = false;
67
+ }
68
+
69
+ private attemptConnect(onSuccess?: () => void, onError?: (error: Error) => void): void {
70
+ let settled = false;
71
+
72
+ const settle = <T extends unknown[]>(callback: ((...args: T) => void) | undefined, ...args: T) => {
73
+ if (settled) return;
74
+ settled = true;
75
+ callback?.(...args);
76
+ };
77
+
78
+ const socket = net.createConnection(this.socketPath, () => {
79
+ info(`Connected to MCP server at ${this.socketPath}`);
80
+ this.hasConnected = true;
81
+ this.register()
82
+ .then(() => {
83
+ info("Registered with MCP server");
84
+ settle(onSuccess);
85
+ })
86
+ .catch((cause) => {
87
+ error("Failed to register with MCP server:", cause.message);
88
+ settle(onError, normalizeError(cause));
89
+ });
90
+ });
91
+
92
+ const connection = ProtocolConnection.fromSocket(socket, {
93
+ onClose: () => {
94
+ if (this.hasConnected) {
95
+ info("Disconnected from MCP server");
96
+ this.hasConnected = false;
97
+ }
98
+ this.socket = null;
99
+ this.connection = null;
100
+ this.scheduleReconnect();
101
+ },
102
+ onError: (socketError) => {
103
+ const code = (socketError as NodeJS.ErrnoException).code;
104
+ const isDisconnectError =
105
+ code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "ECONNRESET";
106
+ if (isDisconnectError) {
107
+ this.scheduleReconnect();
108
+ } else {
109
+ error("Socket error:", socketError.message);
110
+ }
111
+ settle(onError, socketError);
112
+ },
113
+ });
114
+ connection.on("request", (request) => {
115
+ this.handleRequest(request).catch((cause) => {
116
+ error("Error handling request:", cause);
117
+ });
118
+ });
119
+ connection.on("invalid", ({ error: parseError }) => {
120
+ warn(`Received invalid JSON from MCP server: ${parseError.message}`);
121
+ });
122
+
123
+ this.socket = socket;
124
+ this.connection = connection;
125
+ }
126
+
127
+ private scheduleReconnect(): void {
128
+ if (this.reconnectTimer || this.isStopping) return;
129
+ this.reconnectTimer = setTimeout(() => {
130
+ this.reconnectTimer = null;
131
+ this.attemptConnect();
132
+ }, RECONNECT_DELAY_MS);
133
+ }
134
+
135
+ private register(): Promise<unknown> {
136
+ if (!this.connection) {
137
+ return Promise.reject(new Error("Connection not initialized"));
138
+ }
139
+ return this.connection.send(
140
+ "app.register",
141
+ {
142
+ applicationId: this.applicationId,
143
+ pid: process.pid,
144
+ projectRoot: process.cwd(),
145
+ },
146
+ REGISTER_TIMEOUT_MS,
147
+ );
148
+ }
149
+
150
+ private async handleRequest(request: Request): Promise<void> {
151
+ const { id, method, params } = request;
152
+ const connection = this.connection;
153
+ if (!connection) return;
154
+
155
+ try {
156
+ const defaultApp = Gio.Application.getDefault();
157
+ if (!(defaultApp instanceof Gtk.Application)) {
158
+ throw new TypeError("Application not initialized");
159
+ }
160
+ this.registry.refresh();
161
+ const result = await dispatch(method, params, { app: defaultApp, registry: this.registry });
162
+ connection.write({ id, result });
163
+ } catch (error) {
164
+ if (error instanceof ProtocolError) {
165
+ connection.write({ id, error: error.toErrorObject() });
166
+ } else {
167
+ connection.write({
168
+ id,
169
+ error: {
170
+ code: ErrorCode.INTERNAL_ERROR,
171
+ message: errorMessage(error),
172
+ },
173
+ });
174
+ }
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,156 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import {
5
+ invalidRequestError,
6
+ methodNotFoundError,
7
+ type ParamsSchema,
8
+ type ServerInitiatedMethod,
9
+ type ServerRequestParams,
10
+ ServerRequestParamsSchemas,
11
+ widgetNotFoundError,
12
+ } from "@gtkx/mcp/internal";
13
+ import { serializeWidget } from "./serialize-widget.js";
14
+ import { loadTestingModule } from "./testing-loader.js";
15
+ import type { WidgetRegistry } from "./widget-registry.js";
16
+
17
+ export type HandlerContext = {
18
+ app: Gtk.Application;
19
+ registry: WidgetRegistry;
20
+ };
21
+
22
+ type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
23
+
24
+ const validated = <Params>(
25
+ schema: ParamsSchema<Params>,
26
+ handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
27
+ ): ValidatedHandler => {
28
+ return (ctx, params) => {
29
+ const parsed = schema.safeParse(params ?? {});
30
+ if (!parsed.success) {
31
+ throw invalidRequestError(parsed.error.message);
32
+ }
33
+ return handler(ctx, parsed.data);
34
+ };
35
+ };
36
+
37
+ const requireWidget = (registry: WidgetRegistry, widgetId: string | undefined): Gtk.Widget => {
38
+ if (widgetId === undefined) {
39
+ throw widgetNotFoundError("undefined");
40
+ }
41
+ const widget = registry.get(widgetId);
42
+ if (!widget) {
43
+ throw widgetNotFoundError(widgetId);
44
+ }
45
+ return widget;
46
+ };
47
+
48
+ const extractSignalArg = (arg: unknown): unknown => {
49
+ const isTypedArg = typeof arg === "object" && arg !== null && "type" in arg && "value" in arg;
50
+ return isTypedArg ? (arg as { value: unknown }).value : arg;
51
+ };
52
+
53
+ const handleQuery = async (
54
+ { app, registry }: HandlerContext,
55
+ params: ServerRequestParams<"widget.query">,
56
+ ): Promise<unknown> => {
57
+ const testing = await loadTestingModule();
58
+ let widgets: Gtk.Widget[] = [];
59
+
60
+ switch (params.by) {
61
+ 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);
67
+ break;
68
+ }
69
+ case "text":
70
+ widgets = await testing.findAllByText(app, String(params.value), params.options);
71
+ break;
72
+ case "name":
73
+ widgets = await testing.findAllByName(app, String(params.value), params.options);
74
+ break;
75
+ case "labelText":
76
+ widgets = await testing.findAllByLabelText(app, String(params.value), params.options);
77
+ break;
78
+ }
79
+
80
+ return { widgets: widgets.map((w) => serializeWidget(w, (widget) => registry.idFor(widget), testing)) };
81
+ };
82
+
83
+ const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
84
+ const [toplevel] = registry.toplevels();
85
+ if (!toplevel) {
86
+ throw new Error("No windows available for screenshot");
87
+ }
88
+ return toplevel;
89
+ };
90
+
91
+ const handleScreenshot = async (
92
+ { registry }: HandlerContext,
93
+ params: ServerRequestParams<"widget.screenshot">,
94
+ ): Promise<unknown> => {
95
+ const testing = await loadTestingModule();
96
+ const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
97
+ const result = await testing.screenshot(target);
98
+ if (params.path) {
99
+ mkdirSync(dirname(params.path), { recursive: true });
100
+ writeFileSync(params.path, Buffer.from(result.data, "base64"));
101
+ return { data: result.data, mimeType: result.mimeType, savedPath: params.path };
102
+ }
103
+ return { data: result.data, mimeType: result.mimeType };
104
+ };
105
+
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
+ };
147
+
148
+ const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
149
+ Object.hasOwn(ServerRequestParamsSchemas, method);
150
+
151
+ export const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
152
+ if (!isServerInitiatedMethod(method)) {
153
+ throw methodNotFoundError(method);
154
+ }
155
+ return HANDLERS[method](ctx, params);
156
+ };
@@ -0,0 +1,22 @@
1
+ import { McpClient } from "./client.js";
2
+
3
+ let globalClient: McpClient | null = null;
4
+
5
+ export const startMcpClient = async (applicationId: string): Promise<McpClient> => {
6
+ if (globalClient) {
7
+ return globalClient;
8
+ }
9
+
10
+ globalClient = new McpClient({ applicationId });
11
+
12
+ await globalClient.connect().catch(() => {});
13
+
14
+ return globalClient;
15
+ };
16
+
17
+ export const stopMcpClient = (): void => {
18
+ if (globalClient) {
19
+ globalClient.disconnect();
20
+ globalClient = null;
21
+ }
22
+ };
@@ -0,0 +1,34 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { SerializedWidget } from "@gtkx/mcp/internal";
3
+
4
+ type WidgetIdResolver = (widget: Gtk.Widget) => string;
5
+
6
+ export type WidgetFormatting = {
7
+ formatRole(role: Gtk.AccessibleRole): string;
8
+ getWidgetNodeText(widget: Gtk.Widget): string | null;
9
+ };
10
+
11
+ export const serializeWidget = (
12
+ widget: Gtk.Widget,
13
+ idFor: WidgetIdResolver,
14
+ testing: WidgetFormatting,
15
+ ): SerializedWidget => {
16
+ const children: SerializedWidget[] = [];
17
+ let child = widget.getFirstChild();
18
+ while (child) {
19
+ children.push(serializeWidget(child, idFor, testing));
20
+ child = child.getNextSibling();
21
+ }
22
+
23
+ return {
24
+ id: idFor(widget),
25
+ type: widget.constructor.name,
26
+ role: testing.formatRole(widget.getAccessibleRole()),
27
+ name: widget.getName() || null,
28
+ text: testing.getWidgetNodeText(widget),
29
+ sensitive: widget.getSensitive(),
30
+ visible: widget.getVisible(),
31
+ cssClasses: widget.getCssClasses(),
32
+ children,
33
+ };
34
+ };
@@ -0,0 +1,32 @@
1
+ type TestingModule = typeof import("@gtkx/testing");
2
+
3
+ export type TestingModuleLoader = () => Promise<TestingModule>;
4
+
5
+ const defaultLoader: TestingModuleLoader = () => import("@gtkx/testing");
6
+
7
+ let loader: TestingModuleLoader = defaultLoader;
8
+ let testingModule: TestingModule | null = null;
9
+ let testingLoadError: Error | null = null;
10
+
11
+ export const setTestingModuleLoader = (next: TestingModuleLoader | null): void => {
12
+ loader = next ?? defaultLoader;
13
+ testingModule = null;
14
+ testingLoadError = null;
15
+ };
16
+
17
+ export const loadTestingModule = async (): Promise<TestingModule> => {
18
+ if (testingModule) return testingModule;
19
+ if (testingLoadError) throw testingLoadError;
20
+
21
+ 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;
31
+ }
32
+ };
@@ -0,0 +1,43 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+
3
+ export class WidgetRegistry {
4
+ private idByWidget = new WeakMap<Gtk.Widget, string>();
5
+ private nextId = 0;
6
+ private widgetById = new Map<string, Gtk.Widget>();
7
+ private toplevelWindows: Gtk.Window[] = [];
8
+
9
+ refresh(): void {
10
+ this.widgetById.clear();
11
+ this.toplevelWindows = Gtk.Window.listToplevels() as Gtk.Window[];
12
+ for (const window of this.toplevelWindows) {
13
+ this.register(window);
14
+ }
15
+ }
16
+
17
+ toplevels(): Gtk.Window[] {
18
+ return this.toplevelWindows;
19
+ }
20
+
21
+ register(widget: Gtk.Widget): void {
22
+ const id = this.idFor(widget);
23
+ this.widgetById.set(id, widget);
24
+ let child = widget.getFirstChild();
25
+ while (child) {
26
+ this.register(child);
27
+ child = child.getNextSibling();
28
+ }
29
+ }
30
+
31
+ idFor(widget: Gtk.Widget): string {
32
+ let id = this.idByWidget.get(widget);
33
+ if (!id) {
34
+ id = String(this.nextId++);
35
+ this.idByWidget.set(widget, id);
36
+ }
37
+ return id;
38
+ }
39
+
40
+ get(id: string): Gtk.Widget | undefined {
41
+ return this.widgetById.get(id);
42
+ }
43
+ }
@@ -7,14 +7,11 @@ globalThis.$RefreshReg$ = () => {};
7
7
  globalThis.$RefreshSig$ = () => (type: unknown) => type;
8
8
 
9
9
  /**
10
- * Creates registration functions for a module's React components.
10
+ * Builds the per-module `$RefreshReg$` and `$RefreshSig$` hooks that React Refresh
11
+ * transformed code expects, scoping every registered component to the given module.
11
12
  *
12
- * Used internally by the Vite plugin to register components
13
- * for React Fast Refresh.
14
- *
15
- * @param moduleId - Unique identifier for the module
16
- * @returns Registration functions for the module
17
- * @internal
13
+ * @param moduleId Identifier used to namespace registered component types so they are unique across modules.
14
+ * @returns An object with `$RefreshReg$` and `$RefreshSig$` to install as module-local globals.
18
15
  */
19
16
  export function createModuleRegistration(moduleId: string): {
20
17
  $RefreshReg$: (type: ComponentType, id: string) => void;
@@ -28,36 +25,14 @@ export function createModuleRegistration(moduleId: string): {
28
25
  };
29
26
  }
30
27
 
31
- function isLikelyComponentType(value: unknown): boolean {
32
- if (typeof value !== "function") {
33
- return false;
34
- }
35
-
36
- const func = value as { $$typeof?: symbol };
37
-
38
- if (func.$$typeof === Symbol.for("react.memo") || func.$$typeof === Symbol.for("react.forward_ref")) {
39
- return true;
40
- }
41
-
42
- const name = (value as { name?: string }).name;
43
- if (typeof name === "string" && /^[A-Z]/.test(name)) {
44
- return true;
45
- }
46
-
47
- return false;
48
- }
49
-
50
28
  /**
51
- * Checks if a module's exports form a React Refresh boundary.
29
+ * Determines whether a module can act as a React Refresh boundary, meaning every one of
30
+ * its exports (ignoring `__esModule`) looks like a React component type.
52
31
  *
53
- * A module is a refresh boundary if all its exports are React components,
54
- * allowing for fast refresh without full page reload.
55
- *
56
- * @param moduleExports - The module's exports object
57
- * @returns `true` if the module can be fast-refreshed
58
- * @internal
32
+ * @param moduleExports The module's export object to inspect.
33
+ * @returns `true` when the module qualifies as a refresh boundary, `false` otherwise.
59
34
  */
60
- export function isReactRefreshBoundary(moduleExports: Record<string, unknown>): boolean {
35
+ export function isRefreshBoundary(moduleExports: Record<string, unknown>): boolean {
61
36
  if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
62
37
  return true;
63
38
  }
@@ -69,20 +44,16 @@ export function isReactRefreshBoundary(moduleExports: Record<string, unknown>):
69
44
 
70
45
  const value = moduleExports[key];
71
46
 
72
- if (!isLikelyComponentType(value)) {
47
+ if (!RefreshRuntime.isLikelyComponentType(value)) {
73
48
  return false;
74
49
  }
75
50
  }
76
51
 
77
- return Object.keys(moduleExports).filter((k) => k !== "__esModule").length > 0;
52
+ return Object.keys(moduleExports).some((k) => k !== "__esModule");
78
53
  }
79
54
 
80
55
  /**
81
- * Triggers React Fast Refresh to re-render components.
82
- *
83
- * Called after module updates when all exports are React components.
84
- *
85
- * @internal
56
+ * Applies all pending React Refresh updates, re-rendering components whose modules have changed.
86
57
  */
87
58
  export function performRefresh(): void {
88
59
  RefreshRuntime.performReactRefresh();
@@ -0,0 +1,19 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { formatChildProcessError } from "@gtkx/utils";
3
+ import { resolveCliTool } from "../internal/resolve-cli-tool.js";
4
+
5
+ const SCHEMA_COMPILER = "glib-compile-schemas";
6
+ const SCHEMA_COMPILE_TIMEOUT_MS = 30_000;
7
+
8
+ export const compileSchemas = (dir: string): void => {
9
+ try {
10
+ execFileSync(resolveCliTool(SCHEMA_COMPILER), ["--strict", dir], {
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ timeout: SCHEMA_COMPILE_TIMEOUT_MS,
13
+ encoding: "utf-8",
14
+ });
15
+ } catch (error) {
16
+ const details = formatChildProcessError(error);
17
+ throw new Error(`glib-compile-schemas failed for ${dir}${details ? `:\n${details}` : ""}`, { cause: error });
18
+ }
19
+ };