@gtkx/cli 1.4.0 → 1.6.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.
Files changed (206) hide show
  1. package/README.md +1 -1
  2. package/dist/builder.d.ts +1 -0
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +2 -2
  5. package/dist/builder.js.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codegen/freshness.d.ts.map +1 -1
  9. package/dist/codegen/freshness.js +2 -1
  10. package/dist/codegen/freshness.js.map +1 -1
  11. package/dist/codegen/reference.d.ts +17 -0
  12. package/dist/codegen/reference.d.ts.map +1 -0
  13. package/dist/codegen/reference.js +37 -0
  14. package/dist/codegen/reference.js.map +1 -0
  15. package/dist/codegen/report.d.ts.map +1 -1
  16. package/dist/codegen/report.js +17 -3
  17. package/dist/codegen/report.js.map +1 -1
  18. package/dist/codegen/run-codegen.d.ts +4 -0
  19. package/dist/codegen/run-codegen.d.ts.map +1 -1
  20. package/dist/codegen/run-codegen.js +93 -27
  21. package/dist/codegen/run-codegen.js.map +1 -1
  22. package/dist/commands/codegen.js +1 -1
  23. package/dist/commands/codegen.js.map +1 -1
  24. package/dist/commands/docs.d.ts.map +1 -1
  25. package/dist/commands/docs.js +10 -16
  26. package/dist/commands/docs.js.map +1 -1
  27. package/dist/commands/mcp.d.ts +13 -0
  28. package/dist/commands/mcp.d.ts.map +1 -0
  29. package/dist/commands/mcp.js +68 -0
  30. package/dist/commands/mcp.js.map +1 -0
  31. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  32. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  33. package/dist/deploy/freedesktop/localize.js +107 -0
  34. package/dist/deploy/freedesktop/localize.js.map +1 -0
  35. package/dist/deploy/install-hints.d.ts.map +1 -1
  36. package/dist/deploy/install-hints.js +5 -0
  37. package/dist/deploy/install-hints.js.map +1 -1
  38. package/dist/deploy/notices/gtkx.js +1 -1
  39. package/dist/deploy/notices/gtkx.js.map +1 -1
  40. package/dist/deploy/notices/libraries.d.ts.map +1 -1
  41. package/dist/deploy/notices/libraries.js +2 -1
  42. package/dist/deploy/notices/libraries.js.map +1 -1
  43. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  44. package/dist/deploy/payload/launcher.js +2 -0
  45. package/dist/deploy/payload/launcher.js.map +1 -1
  46. package/dist/deploy/payload/stage.d.ts +1 -1
  47. package/dist/deploy/payload/stage.d.ts.map +1 -1
  48. package/dist/deploy/payload/stage.js +20 -11
  49. package/dist/deploy/payload/stage.js.map +1 -1
  50. package/dist/deploy/run-deploy.d.ts.map +1 -1
  51. package/dist/deploy/run-deploy.js +87 -15
  52. package/dist/deploy/run-deploy.js.map +1 -1
  53. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  54. package/dist/deploy/settings/libraries.js +7 -9
  55. package/dist/deploy/settings/libraries.js.map +1 -1
  56. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  57. package/dist/deploy/targets/flatpak-source.js +21 -10
  58. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  59. package/dist/deploy/tools.d.ts +6 -1
  60. package/dist/deploy/tools.d.ts.map +1 -1
  61. package/dist/deploy/tools.js +27 -4
  62. package/dist/deploy/tools.js.map +1 -1
  63. package/dist/dev/locale-dir.d.ts +3 -0
  64. package/dist/dev/locale-dir.d.ts.map +1 -0
  65. package/dist/dev/locale-dir.js +17 -0
  66. package/dist/dev/locale-dir.js.map +1 -0
  67. package/dist/dev/runner-main.d.ts.map +1 -1
  68. package/dist/dev/runner-main.js +4 -1
  69. package/dist/dev/runner-main.js.map +1 -1
  70. package/dist/dev/runner.d.ts.map +1 -1
  71. package/dist/dev/runner.js +6 -0
  72. package/dist/dev/runner.js.map +1 -1
  73. package/dist/dev/vite-dev-server.d.ts +1 -0
  74. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  75. package/dist/dev/vite-dev-server.js.map +1 -1
  76. package/dist/i18n/binding-extractor.d.ts +30 -0
  77. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  78. package/dist/i18n/binding-extractor.js +1324 -0
  79. package/dist/i18n/binding-extractor.js.map +1 -0
  80. package/dist/i18n/catalog-template.d.ts +3 -0
  81. package/dist/i18n/catalog-template.d.ts.map +1 -0
  82. package/dist/i18n/catalog-template.js +18 -0
  83. package/dist/i18n/catalog-template.js.map +1 -0
  84. package/dist/i18n/catalogs.d.ts +21 -0
  85. package/dist/i18n/catalogs.d.ts.map +1 -0
  86. package/dist/i18n/catalogs.js +139 -0
  87. package/dist/i18n/catalogs.js.map +1 -0
  88. package/dist/i18n/extractor-plugin.d.ts +8 -0
  89. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  90. package/dist/i18n/extractor-plugin.js +247 -0
  91. package/dist/i18n/extractor-plugin.js.map +1 -0
  92. package/dist/i18n/metadata-templates.d.ts +11 -0
  93. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  94. package/dist/i18n/metadata-templates.js +11 -0
  95. package/dist/i18n/metadata-templates.js.map +1 -0
  96. package/dist/i18n/source-messages.d.ts +23 -0
  97. package/dist/i18n/source-messages.d.ts.map +1 -0
  98. package/dist/i18n/source-messages.js +403 -0
  99. package/dist/i18n/source-messages.js.map +1 -0
  100. package/dist/i18n/types.d.ts +10 -0
  101. package/dist/i18n/types.d.ts.map +1 -0
  102. package/dist/i18n/types.js +108 -0
  103. package/dist/i18n/types.js.map +1 -0
  104. package/dist/internal/agent-rules.d.ts +7 -0
  105. package/dist/internal/agent-rules.d.ts.map +1 -0
  106. package/dist/internal/agent-rules.js +106 -0
  107. package/dist/internal/agent-rules.js.map +1 -0
  108. package/dist/internal/docs-elements.d.ts +8 -0
  109. package/dist/internal/docs-elements.d.ts.map +1 -0
  110. package/dist/internal/docs-elements.js +12 -0
  111. package/dist/internal/docs-elements.js.map +1 -0
  112. package/dist/internal/source-imports.d.ts +5 -1
  113. package/dist/internal/source-imports.d.ts.map +1 -1
  114. package/dist/internal/source-imports.js +25 -47
  115. package/dist/internal/source-imports.js.map +1 -1
  116. package/dist/mcp/client-config.d.ts +22 -0
  117. package/dist/mcp/client-config.d.ts.map +1 -0
  118. package/dist/mcp/client-config.js +94 -0
  119. package/dist/mcp/client-config.js.map +1 -0
  120. package/dist/mcp/client.d.ts +0 -1
  121. package/dist/mcp/client.d.ts.map +1 -1
  122. package/dist/mcp/client.js +11 -39
  123. package/dist/mcp/client.js.map +1 -1
  124. package/dist/mcp/handlers.d.ts +2 -1
  125. package/dist/mcp/handlers.d.ts.map +1 -1
  126. package/dist/mcp/handlers.js.map +1 -1
  127. package/dist/settings/render.d.ts +4 -1
  128. package/dist/settings/render.d.ts.map +1 -1
  129. package/dist/settings/render.js +11 -2
  130. package/dist/settings/render.js.map +1 -1
  131. package/dist/settings/schema.d.ts.map +1 -1
  132. package/dist/settings/schema.js +3 -1
  133. package/dist/settings/schema.js.map +1 -1
  134. package/dist/vite-plugins/animated.d.ts +5 -0
  135. package/dist/vite-plugins/animated.d.ts.map +1 -0
  136. package/dist/vite-plugins/animated.js +325 -0
  137. package/dist/vite-plugins/animated.js.map +1 -0
  138. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  139. package/dist/vite-plugins/asset-imports.js +4 -14
  140. package/dist/vite-plugins/asset-imports.js.map +1 -1
  141. package/dist/vite-plugins/i18n.d.ts +5 -0
  142. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  143. package/dist/vite-plugins/i18n.js +88 -0
  144. package/dist/vite-plugins/i18n.js.map +1 -0
  145. package/dist/vite-plugins/index.d.ts +1 -1
  146. package/dist/vite-plugins/index.d.ts.map +1 -1
  147. package/dist/vite-plugins/index.js +5 -1
  148. package/dist/vite-plugins/index.js.map +1 -1
  149. package/dist/vite-plugins/resources.d.ts.map +1 -1
  150. package/dist/vite-plugins/resources.js +12 -57
  151. package/dist/vite-plugins/resources.js.map +1 -1
  152. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  153. package/dist/vite-plugins/self-contained.js +128 -135
  154. package/dist/vite-plugins/self-contained.js.map +1 -1
  155. package/dist/vite-plugins/settings-worker-env.js +2 -2
  156. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  157. package/dist/vite-plugins/settings.d.ts.map +1 -1
  158. package/dist/vite-plugins/settings.js +2 -1
  159. package/dist/vite-plugins/settings.js.map +1 -1
  160. package/package.json +11 -9
  161. package/src/builder.ts +3 -2
  162. package/src/cli.ts +5 -0
  163. package/src/codegen/freshness.ts +2 -1
  164. package/src/codegen/reference.ts +59 -0
  165. package/src/codegen/report.ts +18 -3
  166. package/src/codegen/run-codegen.ts +134 -31
  167. package/src/commands/codegen.ts +1 -1
  168. package/src/commands/docs.ts +10 -28
  169. package/src/commands/mcp.ts +81 -0
  170. package/src/deploy/freedesktop/localize.ts +144 -0
  171. package/src/deploy/install-hints.ts +5 -0
  172. package/src/deploy/notices/gtkx.ts +1 -1
  173. package/src/deploy/notices/libraries.ts +2 -1
  174. package/src/deploy/payload/launcher.ts +2 -0
  175. package/src/deploy/payload/stage.ts +35 -10
  176. package/src/deploy/run-deploy.ts +128 -13
  177. package/src/deploy/settings/libraries.ts +7 -10
  178. package/src/deploy/targets/flatpak-source.ts +33 -10
  179. package/src/deploy/tools.ts +36 -3
  180. package/src/dev/locale-dir.ts +22 -0
  181. package/src/dev/runner-main.ts +4 -1
  182. package/src/dev/runner.ts +8 -0
  183. package/src/dev/vite-dev-server.ts +1 -0
  184. package/src/i18n/binding-extractor.ts +1969 -0
  185. package/src/i18n/catalog-template.ts +24 -0
  186. package/src/i18n/catalogs.ts +216 -0
  187. package/src/i18n/extractor-plugin.ts +413 -0
  188. package/src/i18n/metadata-templates.ts +29 -0
  189. package/src/i18n/source-messages.ts +608 -0
  190. package/src/i18n/types.ts +154 -0
  191. package/src/internal/agent-rules.ts +135 -0
  192. package/src/internal/docs-elements.ts +21 -0
  193. package/src/internal/source-imports.ts +31 -60
  194. package/src/mcp/client-config.ts +130 -0
  195. package/src/mcp/client.ts +17 -44
  196. package/src/mcp/handlers.ts +7 -6
  197. package/src/settings/render.ts +16 -2
  198. package/src/settings/schema.ts +3 -1
  199. package/src/vite-plugins/animated.ts +486 -0
  200. package/src/vite-plugins/asset-imports.ts +5 -17
  201. package/src/vite-plugins/i18n.ts +146 -0
  202. package/src/vite-plugins/index.ts +5 -0
  203. package/src/vite-plugins/resources.ts +12 -74
  204. package/src/vite-plugins/self-contained.ts +150 -169
  205. package/src/vite-plugins/settings-worker-env.ts +2 -2
  206. package/src/vite-plugins/settings.ts +2 -1
@@ -0,0 +1,154 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { SourceMessage } from "./source-messages.js";
4
+
5
+ type I18nTypesResult = {
6
+ isWritten: boolean;
7
+ path: string;
8
+ };
9
+
10
+ const I18N_TYPES_FILENAME = "i18n.d.ts";
11
+ const INTERPOLATION_PATTERN = /\{\{[^{}]+\}\}/u;
12
+
13
+ const stringType = (values: string[]): string =>
14
+ [...new Set(values)].map((value) => JSON.stringify(value)).join(" | ");
15
+
16
+ const contextField = (context: string | null): string =>
17
+ context === null ? "" : ` context: ${JSON.stringify(context)};`;
18
+
19
+ const hasInterpolation = (values: string[]): boolean => values.some((value) => INTERPOLATION_PATTERN.test(value));
20
+
21
+ const pointVariant = (message: SourceMessage): string => {
22
+ const isRequired = message.context !== null || hasInterpolation([message.msgid]);
23
+ const isInterpolated = hasInterpolation([message.msgid]);
24
+
25
+ return (
26
+ `{${contextField(message.context)} interpolated: ${String(isInterpolated)}; kind: "point"; ` +
27
+ `required: ${String(isRequired)}; value: ${JSON.stringify(message.msgid)} }`
28
+ );
29
+ };
30
+
31
+ const defaultVariant = (message: SourceMessage): string => {
32
+ const values = [message.singular];
33
+ const isInterpolated = hasInterpolation(values);
34
+ const isRequired = message.context !== null || isInterpolated;
35
+
36
+ return (
37
+ `{${contextField(message.context)} defaultValue: ${JSON.stringify(message.singular)}; ` +
38
+ `interpolated: ${String(isInterpolated)}; kind: "default"; required: ${String(isRequired)}; ` +
39
+ `value: ${stringType(values)} }`
40
+ );
41
+ };
42
+
43
+ const requirePlural = (message: SourceMessage): string => {
44
+ if (message.plural === null) {
45
+ throw new Error("Plural translation metadata requires a plural source string");
46
+ }
47
+
48
+ return message.plural;
49
+ };
50
+
51
+ const pluralVariant = (message: SourceMessage): string => {
52
+ const plural = requirePlural(message);
53
+ const values = [message.singular, plural];
54
+ const isInterpolated = hasInterpolation(values);
55
+
56
+ return (
57
+ `{${contextField(message.context)} defaultValue: ${JSON.stringify(plural)}; ` +
58
+ `interpolated: ${String(isInterpolated)}; kind: "plural"; required: true; ` +
59
+ `value: ${stringType(values)} }`
60
+ );
61
+ };
62
+
63
+ const pluralDefaultsVariant = (message: SourceMessage): string => {
64
+ const plural = requirePlural(message);
65
+ const values = [message.singular, plural];
66
+ const isInterpolated = hasInterpolation(values);
67
+
68
+ return (
69
+ `{${contextField(message.context)} defaultValueOne: ${JSON.stringify(message.singular)}; ` +
70
+ `defaultValueOther: ${JSON.stringify(plural)}; interpolated: ${String(isInterpolated)}; ` +
71
+ `kind: "pluralDefaults"; required: true; value: ${stringType(values)} }`
72
+ );
73
+ };
74
+
75
+ const variantsFor = (message: SourceMessage): string[] => {
76
+ if (message.kind === "plural") {
77
+ return [pluralVariant(message)];
78
+ }
79
+
80
+ if (message.kind === "pluralDefaults") {
81
+ return [pluralDefaultsVariant(message)];
82
+ }
83
+
84
+ return message.kind === "default" ? [defaultVariant(message)] : [pointVariant(message)];
85
+ };
86
+
87
+ const renderRegistry = (messages: SourceMessage[]): string[] => {
88
+ const entries: Map<string, Set<string>> = new Map();
89
+
90
+ for (const message of messages) {
91
+ const variants = entries.get(message.msgid) ?? new Set<string>();
92
+
93
+ for (const variant of variantsFor(message)) {
94
+ variants.add(variant);
95
+ }
96
+
97
+ entries.set(message.msgid, variants);
98
+ }
99
+
100
+ return [...entries]
101
+ .toSorted(([left], [right]) => left.localeCompare(right))
102
+ .map(([msgid, variants]) => ` ${JSON.stringify(msgid)}: ${[...variants].join(" | ")};`);
103
+ };
104
+
105
+ const generatedHeader = (): string[] => [
106
+ "/**",
107
+ " * Translation declarations generated by GTKX. Regenerated by",
108
+ " * `gtkx codegen`, `gtkx dev`, and `gtkx build`; do not edit.",
109
+ " */",
110
+ ];
111
+
112
+ const renderI18nTypes = (messages: SourceMessage[], isEnabled = true): string => {
113
+ if (!isEnabled) {
114
+ return [...generatedHeader(), "export {};", ""].join("\n");
115
+ }
116
+
117
+ return [
118
+ ...generatedHeader(),
119
+ 'import "@gtkx/i18n";',
120
+ "",
121
+ 'declare module "@gtkx/i18n" {',
122
+ " interface TranslationRegistry {",
123
+ ...renderRegistry(messages),
124
+ " }",
125
+ "}",
126
+ "",
127
+ ].join("\n");
128
+ };
129
+
130
+ const i18nTypesPath = (root: string): string => join(root, "node_modules", ".gtkx", I18N_TYPES_FILENAME);
131
+
132
+ const readFileOrNull = (path: string): string | null => {
133
+ try {
134
+ return readFileSync(path, "utf8");
135
+ } catch {
136
+ return null;
137
+ }
138
+ };
139
+
140
+ const emitI18nTypes = (root: string, messages: SourceMessage[], isEnabled = true): I18nTypesResult => {
141
+ const path = i18nTypesPath(root);
142
+ const content = renderI18nTypes(messages, isEnabled);
143
+
144
+ if (readFileOrNull(path) === content) {
145
+ return { isWritten: false, path };
146
+ }
147
+
148
+ mkdirSync(dirname(path), { recursive: true });
149
+ writeFileSync(path, content);
150
+
151
+ return { isWritten: true, path };
152
+ };
153
+
154
+ export { I18N_TYPES_FILENAME, emitI18nTypes, i18nTypesPath };
@@ -0,0 +1,135 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { REFERENCE_PATH } from "../codegen/reference.js";
4
+
5
+ type AgentRulesResult = {
6
+ isWritten: boolean;
7
+ files: string[];
8
+ };
9
+
10
+ const AGENTS_FILENAME = "AGENTS.md";
11
+ const CLAUDE_FILENAME = "CLAUDE.md";
12
+ const CLAUDE_CONTENTS = `@${AGENTS_FILENAME}\n`;
13
+ const BEGIN_MARKER = "<!-- BEGIN:gtkx-agent-rules -->";
14
+ const END_MARKER = "<!-- END:gtkx-agent-rules -->";
15
+ const BLOCK_PATTERN = /<!-- BEGIN:gtkx-agent-rules -->[\s\S]*?<!-- END:gtkx-agent-rules -->/;
16
+ const TOP_LEVEL_HEADING = /^# /m;
17
+
18
+ const rulesBody = (heading: string): string[] => [
19
+ `${heading} GTKX`,
20
+ "",
21
+ "This is not the GTK you have seen before. Most GTK code in your training data is C, PyGObject, Vala or " +
22
+ "GJS, and almost none of it is valid here. Check the rules below against what you are about to write.",
23
+ "",
24
+ "- Children are JSX, never `.append()`, `pack_start()`, `set_child()` or `add()`.",
25
+ '- Signals are props: `onClicked`, not `widget.connect("clicked", ...)`.',
26
+ "- Props are camelCase: `marginTop`, not `margin-top` or `margin_top`.",
27
+ "- There is no `Gtk.Template`, no `.ui` XML, and no `GtkBuilder`. The JSX tree is the definition.",
28
+ "- Elements come from `@gtkx/jsx/<namespace>` and classes, enums and functions from " +
29
+ "`@gtkx/gi/<namespace>`. Both are generated for this project by `gtkx codegen`, not installed from npm, " +
30
+ "so they match the GIR libraries this project declares.",
31
+ "",
32
+ `Read \`${REFERENCE_PATH}/index.md\` before writing widget code. It is generated from this project's own ` +
33
+ "GIR libraries and is the authority on which props, signals and methods exist. Do not infer a prop from " +
34
+ "another toolkit, from a C function name, or from a similar element.",
35
+ "",
36
+ "| Command | What it does |",
37
+ "| --- | --- |",
38
+ "| `gtkx dev` | Run the app with fast refresh |",
39
+ "| `gtkx codegen` | Regenerate bindings and this reference |",
40
+ "| `tsc --noEmit` | Typecheck |",
41
+ "| `vitest run` | Run the tests |",
42
+ "",
43
+ "Never call UI work done without looking at the running app. With `gtkx dev` up, the gtkx MCP server " +
44
+ "exposes the live widget tree, queries, clicks and screenshots; use them to confirm the change landed.",
45
+ "",
46
+ "This block is written by `gtkx codegen`. Anything outside the markers is yours and is left alone, and " +
47
+ "committing the block with your work keeps the tree clean.",
48
+ ];
49
+
50
+ const renderBlock = (heading: string): string =>
51
+ [BEGIN_MARKER, "", ...rulesBody(heading), "", END_MARKER].join("\n");
52
+
53
+ const headingFor = (contents: string): string => (TOP_LEVEL_HEADING.test(contents) ? "##" : "#");
54
+ const withTrailingNewline = (contents: string): string => (contents.endsWith("\n") ? contents : `${contents}\n`);
55
+
56
+ const appendBlock = (contents: string, block: string): string => {
57
+ if (contents.trim() === "") {
58
+ return `${block}\n`;
59
+ }
60
+
61
+ return `${withTrailingNewline(contents)}\n${block}\n`;
62
+ };
63
+
64
+ const replaceBlock = (contents: string, block: string): string => {
65
+ const match = BLOCK_PATTERN.exec(contents);
66
+
67
+ if (match === null) {
68
+ return appendBlock(contents, block);
69
+ }
70
+
71
+ return contents.slice(0, match.index) + block + contents.slice(match.index + match[0].length);
72
+ };
73
+
74
+ const nextContents = (contents: string): string => {
75
+ const heading = headingFor(contents.replace(BLOCK_PATTERN, ""));
76
+
77
+ return replaceBlock(contents, renderBlock(heading));
78
+ };
79
+
80
+ const errorCode = (error: unknown): string | undefined =>
81
+ error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
82
+
83
+ const readOrUndefined = (path: string): string | undefined => {
84
+ try {
85
+ return readFileSync(path, "utf8");
86
+ } catch (error) {
87
+ if (errorCode(error) === "ENOENT") {
88
+ return undefined;
89
+ }
90
+
91
+ throw error;
92
+ }
93
+ };
94
+
95
+ const hasWritten = (path: string, contents: string): boolean => {
96
+ if (readOrUndefined(path) === contents) {
97
+ return false;
98
+ }
99
+
100
+ writeFileSync(path, contents);
101
+
102
+ return true;
103
+ };
104
+
105
+ const hasWrittenClaudeImport = (root: string): boolean => {
106
+ try {
107
+ writeFileSync(join(root, CLAUDE_FILENAME), CLAUDE_CONTENTS, { flag: "wx" });
108
+
109
+ return true;
110
+ } catch (error) {
111
+ if (errorCode(error) === "EEXIST") {
112
+ return false;
113
+ }
114
+
115
+ throw error;
116
+ }
117
+ };
118
+
119
+ const upsertAgentRules = (root: string): AgentRulesResult => {
120
+ const agentsPath = join(root, AGENTS_FILENAME);
121
+ const existing = readOrUndefined(agentsPath) ?? "";
122
+ const files: string[] = [];
123
+
124
+ if (hasWritten(agentsPath, nextContents(existing))) {
125
+ files.push(AGENTS_FILENAME);
126
+ }
127
+
128
+ if (hasWrittenClaudeImport(root)) {
129
+ files.push(CLAUDE_FILENAME);
130
+ }
131
+
132
+ return { isWritten: files.length > 0, files };
133
+ };
134
+
135
+ export { upsertAgentRules };
@@ -0,0 +1,21 @@
1
+ import { type ElementProps, type OmittedProps, readBuiltinElements, resolveStore } from "@gtkx/codegen";
2
+ import { existsSync } from "node:fs";
3
+
4
+ type DocsElements = {
5
+ props: ElementProps;
6
+ omittedProps: OmittedProps;
7
+ };
8
+
9
+ const resolveDocsElements = async (cwd: string): Promise<DocsElements> => {
10
+ const { gi, reactSubexports } = resolveStore(cwd);
11
+
12
+ if (!existsSync(gi.storeDir)) {
13
+ return { props: {}, omittedProps: {} };
14
+ }
15
+
16
+ const { props, omittedProps } = await readBuiltinElements(reactSubexports, gi.storeDir);
17
+
18
+ return { props, omittedProps };
19
+ };
20
+
21
+ export { resolveDocsElements };
@@ -1,8 +1,8 @@
1
- import type { ParseResult, ParserOptions } from "vite";
2
- import { isRecord, sortStringsBy } from "@gtkx/utils";
1
+ import type { ESTree, ParseResult, ParserOptions } from "vite";
2
+ import { sortStringsBy } from "@gtkx/utils";
3
3
  import { type Dirent, readdirSync, readFileSync } from "node:fs";
4
4
  import { extname, join } from "node:path";
5
- import { parseSync } from "vite";
5
+ import { parseSync, Visitor } from "vite";
6
6
 
7
7
  type SourceImport = {
8
8
  importer: string;
@@ -24,8 +24,6 @@ const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
24
24
  ]);
25
25
 
26
26
  const EXCLUDED_DIRECTORIES: Set<string> = new Set([
27
- ".git",
28
- ".gtkx",
29
27
  "build",
30
28
  "coverage",
31
29
  "dist",
@@ -41,11 +39,13 @@ const sourceLanguage = (path: string): SourceLanguage | undefined => {
41
39
  return SOURCE_LANGUAGES.get(extname(path).toLowerCase());
42
40
  };
43
41
 
42
+ const isWalkableDirectory = (name: string): boolean => !name.startsWith(".") && !EXCLUDED_DIRECTORIES.has(name);
43
+
44
44
  const collectSourceEntry = (dir: string, entry: Dirent, found: string[]): void => {
45
45
  const path = join(dir, entry.name);
46
46
 
47
47
  if (entry.isDirectory()) {
48
- if (!EXCLUDED_DIRECTORIES.has(entry.name)) {
48
+ if (isWalkableDirectory(entry.name)) {
49
49
  sourceFilesIn(path, found);
50
50
  }
51
51
 
@@ -71,6 +71,13 @@ const sourceFilesIn = (dir: string, found: string[]): void => {
71
71
  }
72
72
  };
73
73
 
74
+ const discoverSourceFiles = (dir: string): string[] => {
75
+ const files: string[] = [];
76
+ sourceFilesIn(dir, files);
77
+
78
+ return sortStringsBy(files, (path) => path);
79
+ };
80
+
74
81
  const staticImportSources = (module: ParsedModule): string[] => [
75
82
  ...module.staticImports.map((statement) => statement.moduleRequest.value),
76
83
  ...module.staticExports.flatMap((statement) =>
@@ -92,65 +99,30 @@ const staticRuntimeImportSources = (module: ParsedModule): string[] => [
92
99
  ),
93
100
  ];
94
101
 
95
- const templateImportSource = (node: Record<string, unknown>): string | null => {
96
- if (!Array.isArray(node.expressions) || node.expressions.length > 0 || !Array.isArray(node.quasis)) {
97
- return null;
98
- }
99
-
100
- const quasis: unknown[] = node.quasis;
101
- const first: unknown = quasis[0];
102
-
103
- if (!isRecord(first) || !isRecord(first.value)) {
104
- return null;
105
- }
106
-
107
- return typeof first.value.cooked === "string" ? first.value.cooked : null;
108
- };
109
-
110
- const literalImportSource = (node: unknown): string | null => {
111
- if (!isRecord(node)) {
112
- return null;
113
- }
114
-
115
- if (node.type === "Literal") {
116
- return typeof node.value === "string" ? node.value : null;
117
- }
102
+ const templateImportSource = (node: ESTree.TemplateLiteral): string | null => {
103
+ const [quasi] = node.quasis;
118
104
 
119
- return node.type === "TemplateLiteral" ? templateImportSource(node) : null;
120
- };
121
-
122
- const collectDynamicImportArray = (nodes: unknown[], found: string[]): void => {
123
- for (const child of nodes) {
124
- collectDynamicImportSources(child, found);
125
- }
105
+ return quasi === undefined || node.expressions.length > 0 ? null : quasi.value.cooked;
126
106
  };
127
107
 
128
- const collectDynamicImportRecord = (node: Record<string, unknown>, found: string[]): void => {
129
- if (node.type === "ImportExpression") {
130
- const source = literalImportSource(node.source);
131
-
132
- if (source !== null) {
133
- found.push(source);
134
- }
135
-
136
- return;
108
+ const literalImportSource = (node: ESTree.Expression): string | null => {
109
+ if (node.type === "TemplateLiteral") {
110
+ return templateImportSource(node);
137
111
  }
138
112
 
139
- collectDynamicImportArray(Object.values(node), found);
113
+ return node.type === "Literal" && typeof node.value === "string" ? node.value : null;
140
114
  };
141
115
 
142
- const collectDynamicImportSources = (node: unknown, found: string[]): void => {
143
- if (Array.isArray(node)) {
144
- collectDynamicImportArray(node, found);
116
+ const collectDynamicImportSources = (program: ESTree.Program, found: string[]): void => {
117
+ new Visitor({
118
+ ImportExpression: (node) => {
119
+ const source = literalImportSource(node.source);
145
120
 
146
- return;
147
- }
148
-
149
- if (!isRecord(node)) {
150
- return;
151
- }
152
-
153
- collectDynamicImportRecord(node, found);
121
+ if (source !== null) {
122
+ found.push(source);
123
+ }
124
+ },
125
+ }).visit(program);
154
126
  };
155
127
 
156
128
  const parseImportsInWith = (
@@ -190,11 +162,10 @@ const importsIn = (path: string): SourceImport[] => parseImportsIn(path) ?? [];
190
162
  const importKey = (entry: SourceImport): string => `${entry.importer}\0${entry.source}`;
191
163
 
192
164
  const discoverSourceImports = (dir: string): SourceImport[] => {
193
- const files: string[] = [];
194
- sourceFilesIn(dir, files);
165
+ const files = discoverSourceFiles(dir);
195
166
  const imports = files.flatMap((path) => importsIn(path));
196
167
 
197
168
  return sortStringsBy(imports, importKey);
198
169
  };
199
170
 
200
- export { discoverSourceImports, parseRuntimeImportsIn, type SourceImport };
171
+ export { discoverSourceFiles, discoverSourceImports, parseRuntimeImportsIn, type SourceImport, sourceLanguage };
@@ -0,0 +1,130 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ type ClientName = "claude" | "cursor" | "vscode" | "opencode" | "codex";
5
+ type ServerEntry = Record<string, unknown>;
6
+
7
+ type Client = {
8
+ name: ClientName;
9
+ file: string;
10
+ key: string;
11
+ entry: ServerEntry;
12
+ };
13
+
14
+ type ClientResult =
15
+ | { kind: "written"; path: string; isCreated: boolean } |
16
+ { kind: "manual"; path: string; snippet: string };
17
+
18
+ const SERVER_NAME = "gtkx";
19
+ const COMMAND = "npx";
20
+ const ARGS = ["gtkx", "mcp"];
21
+ const CODEX_PATH = "~/.codex/config.toml";
22
+
23
+ const CODEX_SNIPPET = [
24
+ `[mcp_servers.${SERVER_NAME}]`,
25
+ `command = ${JSON.stringify(COMMAND)}`,
26
+ `args = ${JSON.stringify(ARGS)}`,
27
+ ].join("\n");
28
+
29
+ const CLIENTS: Client[] = [
30
+ {
31
+ name: "claude",
32
+ file: ".mcp.json",
33
+ key: "mcpServers",
34
+ entry: { command: COMMAND, args: ARGS },
35
+ },
36
+ {
37
+ name: "cursor",
38
+ file: join(".cursor", "mcp.json"),
39
+ key: "mcpServers",
40
+ entry: { command: COMMAND, args: ARGS },
41
+ },
42
+ {
43
+ name: "vscode",
44
+ file: join(".vscode", "mcp.json"),
45
+ key: "servers",
46
+ entry: { type: "stdio", command: COMMAND, args: ARGS },
47
+ },
48
+ {
49
+ name: "opencode",
50
+ file: "opencode.json",
51
+ key: "mcp",
52
+ entry: { type: "local", command: [COMMAND, ...ARGS], enabled: true },
53
+ },
54
+ {
55
+ name: "codex",
56
+ file: CODEX_PATH,
57
+ key: "mcp_servers",
58
+ entry: {},
59
+ },
60
+ ];
61
+
62
+ const CLIENT_NAMES: Set<string> = new Set(CLIENTS.map((client) => client.name));
63
+
64
+ const isClientName = (value: string): value is ClientName => CLIENT_NAMES.has(value);
65
+
66
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
67
+ typeof value === "object" && value !== null && !Array.isArray(value);
68
+
69
+ const readOrUndefined = (path: string): string | undefined => {
70
+ try {
71
+ return readFileSync(path, "utf8");
72
+ } catch (error) {
73
+ if (error instanceof Error && (error as NodeJS.ErrnoException).code === "ENOENT") {
74
+ return undefined;
75
+ }
76
+
77
+ throw error;
78
+ }
79
+ };
80
+
81
+ const readJson = (path: string): Record<string, unknown> | undefined => {
82
+ const source = readOrUndefined(path);
83
+
84
+ if (source === undefined) {
85
+ return undefined;
86
+ }
87
+
88
+ const parsed: unknown = JSON.parse(source);
89
+
90
+ if (!isRecord(parsed)) {
91
+ throw new Error(`Refusing to edit ${path}: its contents are not a JSON object.`);
92
+ }
93
+
94
+ return parsed;
95
+ };
96
+
97
+ const getSection = (document: Record<string, unknown>, key: string): Record<string, unknown> => {
98
+ const section = document[key];
99
+
100
+ return isRecord(section) ? section : {};
101
+ };
102
+
103
+ const clientFor = (name: ClientName): Client => {
104
+ const client = CLIENTS.find((candidate) => candidate.name === name);
105
+
106
+ if (client === undefined) {
107
+ throw new Error(`Unknown MCP client "${name}".`);
108
+ }
109
+
110
+ return client;
111
+ };
112
+
113
+ const writeClientConfig = (root: string, name: ClientName): ClientResult => {
114
+ const client = clientFor(name);
115
+
116
+ if (name === "codex") {
117
+ return { kind: "manual", path: CODEX_PATH, snippet: CODEX_SNIPPET };
118
+ }
119
+
120
+ const path = join(root, client.file);
121
+ const existing = readJson(path);
122
+ const document = existing ?? {};
123
+ const section = { ...getSection(document, client.key), [SERVER_NAME]: client.entry };
124
+ mkdirSync(dirname(path), { recursive: true });
125
+ writeFileSync(path, `${JSON.stringify({ ...document, [client.key]: section }, null, 4)}\n`);
126
+
127
+ return { kind: "written", path, isCreated: existing === undefined };
128
+ };
129
+
130
+ export { CLIENTS, isClientName, writeClientConfig, type ClientName, type ClientResult };
package/src/mcp/client.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import * as Gio from "@gtkx/gi/gio";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import { DEFAULT_SOCKET_PATH, ErrorCode, ProtocolConnection, ProtocolError, type Request } from "@gtkx/mcp/internal";
4
- import { error, errorMessage, info, normalizeError, warn } from "@gtkx/utils";
3
+ import {
4
+ DEFAULT_SOCKET_PATH,
5
+ isConnectionClosedError,
6
+ type JSONRPCRequest,
7
+ ProtocolConnection,
8
+ type Result,
9
+ } from "@gtkx/mcp/internal";
10
+ import { error, errorMessage, info, normalizeError } from "@gtkx/utils";
5
11
  import * as net from "node:net";
6
12
  import { dispatch } from "./handlers.js";
7
13
  import { WidgetRegistry } from "./widget-registry.js";
@@ -25,11 +31,6 @@ const RECONNECT_DELAY_MS = 2000;
25
31
  const REGISTER_TIMEOUT_MS = 30_000;
26
32
  const DISCONNECT_ERROR_CODES: Set<string> = new Set(["ENOENT", "ECONNREFUSED", "EPIPE", "ECONNRESET"]);
27
33
 
28
- const toResponseError = (error: unknown): { code: number; message: string; data?: unknown } =>
29
- error instanceof ProtocolError
30
- ? error.toErrorObject()
31
- : { code: ErrorCode.INTERNAL_ERROR, message: errorMessage(error) };
32
-
33
34
  const connectSettler = (callbacks: ConnectCallbacks): ConnectSettler => {
34
35
  let isSettled = false;
35
36
 
@@ -82,7 +83,7 @@ class McpClient {
82
83
  private handleSocketError(socketError: Error): void {
83
84
  const code = (socketError as NodeJS.ErrnoException).code;
84
85
 
85
- if (code !== undefined && DISCONNECT_ERROR_CODES.has(code)) {
86
+ if (isConnectionClosedError(socketError) || (code !== undefined && DISCONNECT_ERROR_CODES.has(code))) {
86
87
  this.scheduleReconnect();
87
88
  } else {
88
89
  error("Socket error:", socketError.message);
@@ -119,14 +120,7 @@ class McpClient {
119
120
  },
120
121
  });
121
122
 
122
- connection.on("request", (request) => {
123
- void this.handleRequest(request);
124
- });
125
-
126
- connection.on("invalid", ({ error: parseError }) => {
127
- warn(`Received invalid JSON from MCP server: ${parseError.message}`);
128
- });
129
-
123
+ connection.fallbackRequestHandler = (request) => this.handleRequest(request);
130
124
  this.socket = socket;
131
125
  this.connection = connection;
132
126
  }
@@ -158,35 +152,16 @@ class McpClient {
158
152
  );
159
153
  }
160
154
 
161
- private async respondToRequest(request: Request): Promise<void> {
162
- const { id, method, params } = request;
163
- const connection = this.connection;
155
+ private handleRequest(request: JSONRPCRequest): Promise<Result> {
156
+ const defaultApp = Gio.Application.getDefault();
164
157
 
165
- if (!connection) {
166
- return;
158
+ if (!(defaultApp instanceof Gtk.Application)) {
159
+ throw new TypeError("Application not initialized");
167
160
  }
168
161
 
169
- try {
170
- const defaultApp = Gio.Application.getDefault();
171
-
172
- if (!(defaultApp instanceof Gtk.Application)) {
173
- throw new TypeError("Application not initialized");
174
- }
162
+ this.registry.refresh();
175
163
 
176
- this.registry.refresh();
177
- const result = await dispatch(method, params, { app: defaultApp, registry: this.registry });
178
- connection.write({ id, result });
179
- } catch (dispatchError) {
180
- connection.write({ id, error: toResponseError(dispatchError) });
181
- }
182
- }
183
-
184
- private async handleRequest(request: Request): Promise<void> {
185
- try {
186
- await this.respondToRequest(request);
187
- } catch (requestError) {
188
- error("Error handling request:", requestError);
189
- }
164
+ return dispatch(request.method, request.params, { app: defaultApp, registry: this.registry });
190
165
  }
191
166
 
192
167
  async connect(): Promise<void> {
@@ -219,10 +194,8 @@ class McpClient {
219
194
  this.reconnectTimer = null;
220
195
  }
221
196
 
222
- this.connection?.rejectPending(new Error("Client disconnected"));
223
-
224
197
  if (this.socket) {
225
- this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
198
+ void Promise.allSettled([this.connection?.send("app.unregister")]);
226
199
  this.socket.destroy();
227
200
  this.socket = null;
228
201
  }