@gtkx/cli 1.5.0 → 2.0.0-beta.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 (211) hide show
  1. package/README.md +5 -5
  2. package/dist/cli.js +4 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/codegen/freshness.d.ts.map +1 -1
  5. package/dist/codegen/freshness.js +45 -15
  6. package/dist/codegen/freshness.js.map +1 -1
  7. package/dist/codegen/reference.d.ts +17 -0
  8. package/dist/codegen/reference.d.ts.map +1 -0
  9. package/dist/codegen/reference.js +31 -0
  10. package/dist/codegen/reference.js.map +1 -0
  11. package/dist/codegen/report.d.ts.map +1 -1
  12. package/dist/codegen/report.js +18 -15
  13. package/dist/codegen/report.js.map +1 -1
  14. package/dist/codegen/run-codegen.d.ts +5 -5
  15. package/dist/codegen/run-codegen.d.ts.map +1 -1
  16. package/dist/codegen/run-codegen.js +24 -59
  17. package/dist/codegen/run-codegen.js.map +1 -1
  18. package/dist/codegen/store-resolver.d.ts +0 -1
  19. package/dist/codegen/store-resolver.d.ts.map +1 -1
  20. package/dist/codegen/store-resolver.js +8 -17
  21. package/dist/codegen/store-resolver.js.map +1 -1
  22. package/dist/commands/docs.d.ts.map +1 -1
  23. package/dist/commands/docs.js +6 -22
  24. package/dist/commands/docs.js.map +1 -1
  25. package/dist/commands/mcp.d.ts +13 -0
  26. package/dist/commands/mcp.d.ts.map +1 -0
  27. package/dist/commands/mcp.js +68 -0
  28. package/dist/commands/mcp.js.map +1 -0
  29. package/dist/deploy/freedesktop/copyright.d.ts.map +1 -1
  30. package/dist/deploy/freedesktop/copyright.js +2 -13
  31. package/dist/deploy/freedesktop/copyright.js.map +1 -1
  32. package/dist/deploy/freedesktop/xml.d.ts.map +1 -1
  33. package/dist/deploy/freedesktop/xml.js +21 -25
  34. package/dist/deploy/freedesktop/xml.js.map +1 -1
  35. package/dist/deploy/node-runtime/license.d.ts.map +1 -1
  36. package/dist/deploy/node-runtime/license.js +2 -9
  37. package/dist/deploy/node-runtime/license.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/notices/packages.d.ts.map +1 -1
  44. package/dist/deploy/notices/packages.js +3 -10
  45. package/dist/deploy/notices/packages.js.map +1 -1
  46. package/dist/deploy/payload/copy-tree.d.ts.map +1 -1
  47. package/dist/deploy/payload/copy-tree.js +3 -3
  48. package/dist/deploy/payload/copy-tree.js.map +1 -1
  49. package/dist/deploy/payload/stage.d.ts.map +1 -1
  50. package/dist/deploy/payload/stage.js +1 -3
  51. package/dist/deploy/payload/stage.js.map +1 -1
  52. package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
  53. package/dist/deploy/settings/build-manifest.js +4 -15
  54. package/dist/deploy/settings/build-manifest.js.map +1 -1
  55. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  56. package/dist/deploy/settings/libraries.js +2 -21
  57. package/dist/deploy/settings/libraries.js.map +1 -1
  58. package/dist/deploy/settings/paths.d.ts +1 -2
  59. package/dist/deploy/settings/paths.d.ts.map +1 -1
  60. package/dist/deploy/settings/paths.js +5 -15
  61. package/dist/deploy/settings/paths.js.map +1 -1
  62. package/dist/deploy/settings/screenshots.d.ts.map +1 -1
  63. package/dist/deploy/settings/screenshots.js +6 -31
  64. package/dist/deploy/settings/screenshots.js.map +1 -1
  65. package/dist/deploy/targets/flatpak-pnpm.js +4 -4
  66. package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
  67. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  68. package/dist/deploy/targets/flatpak-source.js +4 -4
  69. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  70. package/dist/deploy/targets/flatpak-sources.js +1 -1
  71. package/dist/deploy/targets/flatpak-sources.js.map +1 -1
  72. package/dist/deploy/tools.d.ts.map +1 -1
  73. package/dist/deploy/tools.js +1 -3
  74. package/dist/deploy/tools.js.map +1 -1
  75. package/dist/dev/runner-main.js +1 -1
  76. package/dist/dev/runner-main.js.map +1 -1
  77. package/dist/dev/schema-dir.d.ts +1 -1
  78. package/dist/dev/schema-dir.d.ts.map +1 -1
  79. package/dist/dev/schema-dir.js +2 -4
  80. package/dist/dev/schema-dir.js.map +1 -1
  81. package/dist/i18n/catalogs.d.ts.map +1 -1
  82. package/dist/i18n/catalogs.js +2 -1
  83. package/dist/i18n/catalogs.js.map +1 -1
  84. package/dist/i18n/metadata-templates.d.ts.map +1 -1
  85. package/dist/i18n/metadata-templates.js +2 -1
  86. package/dist/i18n/metadata-templates.js.map +1 -1
  87. package/dist/i18n/source-messages.d.ts +3 -21
  88. package/dist/i18n/source-messages.d.ts.map +1 -1
  89. package/dist/i18n/source-messages.js +66 -85
  90. package/dist/i18n/source-messages.js.map +1 -1
  91. package/dist/i18n/types.d.ts +6 -7
  92. package/dist/i18n/types.d.ts.map +1 -1
  93. package/dist/i18n/types.js +83 -96
  94. package/dist/i18n/types.js.map +1 -1
  95. package/dist/internal/agent-rules.d.ts +7 -0
  96. package/dist/internal/agent-rules.d.ts.map +1 -0
  97. package/dist/internal/agent-rules.js +106 -0
  98. package/dist/internal/agent-rules.js.map +1 -0
  99. package/dist/internal/docs-elements.d.ts +8 -0
  100. package/dist/internal/docs-elements.d.ts.map +1 -0
  101. package/dist/internal/docs-elements.js +12 -0
  102. package/dist/internal/docs-elements.js.map +1 -0
  103. package/dist/internal/icon-path.d.ts.map +1 -1
  104. package/dist/internal/icon-path.js +5 -16
  105. package/dist/internal/icon-path.js.map +1 -1
  106. package/dist/internal/source-imports.d.ts.map +1 -1
  107. package/dist/internal/source-imports.js +10 -42
  108. package/dist/internal/source-imports.js.map +1 -1
  109. package/dist/mcp/client-config.d.ts +22 -0
  110. package/dist/mcp/client-config.d.ts.map +1 -0
  111. package/dist/mcp/client-config.js +94 -0
  112. package/dist/mcp/client-config.js.map +1 -0
  113. package/dist/settings/render.d.ts +0 -1
  114. package/dist/settings/render.d.ts.map +1 -1
  115. package/dist/settings/render.js +1 -11
  116. package/dist/settings/render.js.map +1 -1
  117. package/dist/settings/schema.d.ts +2 -2
  118. package/dist/settings/schema.d.ts.map +1 -1
  119. package/dist/settings/schema.js +21 -62
  120. package/dist/settings/schema.js.map +1 -1
  121. package/dist/vite-plugins/asset-imports.d.ts +1 -2
  122. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  123. package/dist/vite-plugins/asset-imports.js +19 -32
  124. package/dist/vite-plugins/asset-imports.js.map +1 -1
  125. package/dist/vite-plugins/asset-specifier.d.ts +1 -3
  126. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  127. package/dist/vite-plugins/asset-specifier.js +1 -4
  128. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  129. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  130. package/dist/vite-plugins/i18n.js +2 -1
  131. package/dist/vite-plugins/i18n.js.map +1 -1
  132. package/dist/vite-plugins/index.js +2 -2
  133. package/dist/vite-plugins/index.js.map +1 -1
  134. package/dist/vite-plugins/resources.d.ts.map +1 -1
  135. package/dist/vite-plugins/resources.js +31 -96
  136. package/dist/vite-plugins/resources.js.map +1 -1
  137. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  138. package/dist/vite-plugins/settings-worker-env.js +1 -3
  139. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  140. package/dist/vite-plugins/settings.d.ts +1 -2
  141. package/dist/vite-plugins/settings.d.ts.map +1 -1
  142. package/dist/vite-plugins/settings.js +8 -13
  143. package/dist/vite-plugins/settings.js.map +1 -1
  144. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
  145. package/dist/vite-plugins/undeclared-library.js +10 -13
  146. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  147. package/dist/vite-plugins/worker.d.ts.map +1 -1
  148. package/dist/vite-plugins/worker.js +0 -1
  149. package/dist/vite-plugins/worker.js.map +1 -1
  150. package/package.json +21 -14
  151. package/src/cli.ts +5 -0
  152. package/src/codegen/freshness.ts +66 -16
  153. package/src/codegen/reference.ts +52 -0
  154. package/src/codegen/report.ts +16 -17
  155. package/src/codegen/run-codegen.ts +31 -91
  156. package/src/codegen/store-resolver.ts +8 -24
  157. package/src/commands/docs.ts +6 -36
  158. package/src/commands/mcp.ts +81 -0
  159. package/src/deploy/freedesktop/copyright.ts +2 -14
  160. package/src/deploy/freedesktop/xml.ts +24 -32
  161. package/src/deploy/node-runtime/license.ts +2 -8
  162. package/src/deploy/notices/gtkx.ts +1 -1
  163. package/src/deploy/notices/libraries.ts +2 -1
  164. package/src/deploy/notices/packages.ts +3 -11
  165. package/src/deploy/payload/copy-tree.ts +3 -4
  166. package/src/deploy/payload/stage.ts +2 -6
  167. package/src/deploy/settings/build-manifest.ts +4 -16
  168. package/src/deploy/settings/libraries.ts +1 -25
  169. package/src/deploy/settings/paths.ts +6 -16
  170. package/src/deploy/settings/screenshots.ts +8 -46
  171. package/src/deploy/targets/flatpak-pnpm.ts +4 -4
  172. package/src/deploy/targets/flatpak-source.ts +4 -4
  173. package/src/deploy/targets/flatpak-sources.ts +1 -1
  174. package/src/deploy/tools.ts +1 -3
  175. package/src/dev/runner-main.ts +1 -1
  176. package/src/dev/schema-dir.ts +2 -4
  177. package/src/i18n/catalogs.ts +2 -1
  178. package/src/i18n/metadata-templates.ts +2 -1
  179. package/src/i18n/source-messages.ts +77 -115
  180. package/src/i18n/types.ts +97 -131
  181. package/src/internal/agent-rules.ts +135 -0
  182. package/src/internal/docs-elements.ts +21 -0
  183. package/src/internal/icon-path.ts +6 -19
  184. package/src/internal/source-imports.ts +14 -50
  185. package/src/mcp/client-config.ts +130 -0
  186. package/src/settings/render.ts +1 -13
  187. package/src/settings/schema.ts +19 -81
  188. package/src/vite-plugins/asset-imports.ts +20 -44
  189. package/src/vite-plugins/asset-specifier.ts +0 -5
  190. package/src/vite-plugins/i18n.ts +2 -1
  191. package/src/vite-plugins/index.ts +2 -2
  192. package/src/vite-plugins/resources.ts +36 -139
  193. package/src/vite-plugins/settings-worker-env.ts +1 -3
  194. package/src/vite-plugins/settings.ts +11 -19
  195. package/src/vite-plugins/undeclared-library.ts +14 -20
  196. package/src/vite-plugins/worker.ts +0 -1
  197. package/dist/i18n/binding-extractor.d.ts +0 -30
  198. package/dist/i18n/binding-extractor.d.ts.map +0 -1
  199. package/dist/i18n/binding-extractor.js +0 -1324
  200. package/dist/i18n/binding-extractor.js.map +0 -1
  201. package/dist/i18n/extractor-plugin.d.ts +0 -8
  202. package/dist/i18n/extractor-plugin.d.ts.map +0 -1
  203. package/dist/i18n/extractor-plugin.js +0 -247
  204. package/dist/i18n/extractor-plugin.js.map +0 -1
  205. package/dist/internal/data-dir.d.ts +0 -4
  206. package/dist/internal/data-dir.d.ts.map +0 -1
  207. package/dist/internal/data-dir.js +0 -52
  208. package/dist/internal/data-dir.js.map +0 -1
  209. package/src/i18n/binding-extractor.ts +0 -1969
  210. package/src/i18n/extractor-plugin.ts +0 -413
  211. package/src/internal/data-dir.ts +0 -70
@@ -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 } = resolveStore(cwd);
11
+
12
+ if (!existsSync(gi.storeDir)) {
13
+ return { props: {}, omittedProps: {} };
14
+ }
15
+
16
+ const { props, omittedProps } = await readBuiltinElements();
17
+
18
+ return { props, omittedProps };
19
+ };
20
+
21
+ export { resolveDocsElements };
@@ -1,5 +1,6 @@
1
- import { type Stats, statSync } from "node:fs";
2
- import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
1
+ import { isPathInside } from "@gtkx/utils";
2
+ import { statSync } from "node:fs";
3
+ import { basename, extname, isAbsolute, join, resolve } from "node:path";
3
4
 
4
5
  type ResolvedApplicationIcon = { kind: "file"; path: string } |
5
6
  { kind: "none" } |
@@ -10,20 +11,6 @@ const SCALABLE_DIR = "hicolor/scalable/apps";
10
11
  const SIZE_PATTERN = /^(?<width>[1-9]\d{0,3})x(?<height>[1-9]\d{0,3})$/;
11
12
  const SIZE_SEPARATORS = /[-_.]/;
12
13
 
13
- const isInside = (parent: string, candidate: string): boolean => {
14
- const rel = relative(parent, candidate);
15
-
16
- return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
17
- };
18
-
19
- const getStats = (path: string): Stats | undefined => {
20
- try {
21
- return statSync(path, { throwIfNoEntry: false });
22
- } catch {
23
- return undefined;
24
- }
25
- };
26
-
27
14
  const iconExtension = (file: string): string => {
28
15
  const extension = extname(file).toLowerCase();
29
16
 
@@ -37,7 +24,7 @@ const iconExtension = (file: string): string => {
37
24
  const defaultIconFiles = (root: string, applicationId: string): string[] =>
38
25
  [...ICON_EXTENSIONS]
39
26
  .map((extension) => join(root, `${applicationId}${extension}`))
40
- .filter((path) => getStats(path)?.isFile() === true);
27
+ .filter((path) => statSync(path, { throwIfNoEntry: false })?.isFile() === true);
41
28
 
42
29
  const resolveDefaultApplicationIcon = (root: string, applicationId: string): ResolvedApplicationIcon => {
43
30
  const files = defaultIconFiles(root, applicationId);
@@ -76,11 +63,11 @@ const resolveApplicationIcon = (
76
63
 
77
64
  const path = resolve(root, configured);
78
65
 
79
- if (!isInside(root, path)) {
66
+ if (!isPathInside(root, path)) {
80
67
  throw new Error(`Cannot use "${configured}" as the application icon: it is outside ${root}`);
81
68
  }
82
69
 
83
- const stats = getStats(path);
70
+ const stats = statSync(path, { throwIfNoEntry: false });
84
71
 
85
72
  if (stats?.isDirectory() === true) {
86
73
  return { kind: "theme", path };
@@ -1,6 +1,6 @@
1
1
  import type { ESTree, ParseResult, ParserOptions } from "vite";
2
2
  import { sortStringsBy } from "@gtkx/utils";
3
- import { type Dirent, readdirSync, readFileSync } from "node:fs";
3
+ import { globSync, readFileSync } from "node:fs";
4
4
  import { extname, join } from "node:path";
5
5
  import { parseSync, Visitor } from "vite";
6
6
 
@@ -13,8 +13,6 @@ type SourceLanguage = NonNullable<ParserOptions["lang"]>;
13
13
  type ParsedModule = ParseResult["module"];
14
14
 
15
15
  const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
16
- [".cjs", "jsx"],
17
- [".cts", "ts"],
18
16
  [".js", "jsx"],
19
17
  [".jsx", "jsx"],
20
18
  [".mjs", "jsx"],
@@ -23,60 +21,26 @@ const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
23
21
  [".tsx", "tsx"],
24
22
  ]);
25
23
 
26
- const EXCLUDED_DIRECTORIES: Set<string> = new Set([
27
- ".git",
28
- ".gtkx",
29
- "build",
30
- "coverage",
31
- "dist",
32
- "node_modules",
33
- "out-tsc",
34
- ]);
35
-
36
24
  const sourceLanguage = (path: string): SourceLanguage | undefined => {
37
- if (path.endsWith(".d.ts") || path.endsWith(".d.cts") || path.endsWith(".d.mts")) {
25
+ if (path.endsWith(".d.ts") || path.endsWith(".d.mts")) {
38
26
  return undefined;
39
27
  }
40
28
 
41
29
  return SOURCE_LANGUAGES.get(extname(path).toLowerCase());
42
30
  };
43
31
 
44
- const collectSourceEntry = (dir: string, entry: Dirent, found: string[]): void => {
45
- const path = join(dir, entry.name);
46
-
47
- if (entry.isDirectory()) {
48
- if (!EXCLUDED_DIRECTORIES.has(entry.name)) {
49
- sourceFilesIn(path, found);
50
- }
51
-
52
- return;
53
- }
54
-
55
- if (entry.isFile() && sourceLanguage(path) !== undefined) {
56
- found.push(path);
57
- }
58
- };
59
-
60
- const sourceFilesIn = (dir: string, found: string[]): void => {
61
- let entries: Dirent[];
62
-
63
- try {
64
- entries = readdirSync(dir, { withFileTypes: true });
65
- } catch {
66
- return;
67
- }
68
-
69
- for (const entry of entries) {
70
- collectSourceEntry(dir, entry, found);
71
- }
72
- };
73
-
74
- const discoverSourceFiles = (dir: string): string[] => {
75
- const files: string[] = [];
76
- sourceFilesIn(dir, files);
77
-
78
- return sortStringsBy(files, (path) => path);
79
- };
32
+ const discoverSourceFiles = (dir: string): string[] =>
33
+ sortStringsBy(
34
+ globSync("**/{*,.*}", {
35
+ cwd: dir,
36
+ exclude: ["**/{build,coverage,dist,node_modules,out-tsc}/**"],
37
+ withFileTypes: true,
38
+ })
39
+ .filter((entry) => entry.isFile())
40
+ .map((entry) => join(entry.parentPath, entry.name))
41
+ .filter((path) => sourceLanguage(path) !== undefined),
42
+ (path) => path,
43
+ );
80
44
 
81
45
  const staticImportSources = (module: ParsedModule): string[] => [
82
46
  ...module.staticImports.map((statement) => statement.moduleRequest.value),
@@ -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 };
@@ -9,7 +9,6 @@ type NamedSchema = {
9
9
  type EnvAssetDeclarations = {
10
10
  blocked: string[];
11
11
  icons: string[];
12
- legacy: string[];
13
12
  resources: string[];
14
13
  };
15
14
 
@@ -209,15 +208,6 @@ const renderIconModule = (specifier: string): string => [
209
208
  "}",
210
209
  ].join("\n");
211
210
 
212
- const renderLegacyAssetModule = (specifier: string): string => [
213
- `declare module ${toJsStringLiteral(specifier)} {`,
214
- " const resourceUri: string;",
215
- " const path: string;",
216
- " export { path };",
217
- " export default resourceUri;",
218
- "}",
219
- ].join("\n");
220
-
221
211
  const renderBlockedAssetModule = (specifier: string): string => [
222
212
  `declare module ${toJsStringLiteral(specifier)} {`,
223
213
  " const assetImportRequiresResourceOrUrlQuery: {",
@@ -232,7 +222,7 @@ const renderEnvModule = (
232
222
  assetDeclarations?: EnvAssetDeclarations,
233
223
  options: EnvModuleOptions = {},
234
224
  ): string => {
235
- const assets = assetDeclarations ?? { blocked: [], icons: [], legacy: [], resources: [] };
225
+ const assets = assetDeclarations ?? { blocked: [], icons: [], resources: [] };
236
226
  const usedNames: Set<string> = new Set();
237
227
 
238
228
  const schemas = files
@@ -241,7 +231,6 @@ const renderEnvModule = (
241
231
 
242
232
  const blocked = assets.blocked.map((specifier) => renderBlockedAssetModule(specifier));
243
233
  const icons = assets.icons.map((specifier) => renderIconModule(specifier));
244
- const legacy = assets.legacy.map((specifier) => renderLegacyAssetModule(specifier));
245
234
  const resources = assets.resources.map((specifier) => renderResourceModule(specifier));
246
235
  const references = (options.references ?? []).map((path) => `/// <reference path=${JSON.stringify(path)} />`);
247
236
 
@@ -250,7 +239,6 @@ const renderEnvModule = (
250
239
  ...references,
251
240
  ...blocked,
252
241
  ...icons,
253
- ...legacy,
254
242
  ...resources,
255
243
  ...schemas,
256
244
  ].join("\n\n")}\n`;
@@ -1,25 +1,21 @@
1
- import { sortStrings, warn } from "@gtkx/utils";
1
+ import { isPathInside, sortStrings, toPosixPath, warn } from "@gtkx/utils";
2
2
  import { createHash } from "node:crypto";
3
3
  import {
4
4
  copyFileSync,
5
- type Dirent,
6
5
  existsSync,
7
6
  mkdirSync,
8
7
  mkdtempSync,
9
- readdirSync,
10
8
  readFileSync,
11
9
  realpathSync,
12
10
  writeFileSync,
13
11
  } from "node:fs";
14
12
  import { tmpdir } from "node:os";
15
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
+ import { basename, dirname, join, relative, resolve } from "node:path";
16
14
  import { I18N_TYPES_FILENAME, i18nTypesPath } from "../i18n/types.js";
17
- import { DATA_IMPORT_PREFIX } from "../internal/data-dir.js";
18
15
  import { discoverSourceImports, type SourceImport } from "../internal/source-imports.js";
19
16
  import { removeTempDir } from "../internal/staging-dir.js";
20
17
  import {
21
18
  isBareRelativeAsset,
22
- isDataAsset,
23
19
  parseIconSpecifier,
24
20
  parseResourceSpecifier,
25
21
  } from "../vite-plugins/asset-specifier.js";
@@ -57,27 +53,20 @@ const stageSchema = (dir: string, filePath: string): void => {
57
53
  copyFileSync(filePath, join(dir, stagedSchemaName(filePath)));
58
54
  };
59
55
 
60
- const toForwardSlashes = (value: string): string => value.replaceAll(/[/\\]/g, "/");
61
-
62
56
  const projectRelativeSchemaPath = (root: string, filePath: string): string | null => {
63
- let rel: string;
57
+ let projectRoot: string;
58
+ let path: string;
64
59
 
65
60
  try {
66
- rel = relative(realpathSync(root), realpathSync(filePath));
61
+ projectRoot = realpathSync(root);
62
+ path = realpathSync(filePath);
67
63
  } catch {
68
64
  return null;
69
65
  }
70
66
 
71
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
72
- return null;
73
- }
74
-
75
- return toForwardSlashes(rel);
67
+ return isPathInside(projectRoot, path) ? toPosixPath(relative(projectRoot, path)) : null;
76
68
  };
77
69
 
78
- const getModuleSpecifier = (dataDirAbs: string, filePath: string): string =>
79
- `${DATA_IMPORT_PREFIX}/${toForwardSlashes(relative(dataDirAbs, filePath))}`;
80
-
81
70
  const getRelativeModuleSpecifier = (filePath: string): string => `*/${basename(filePath)}`;
82
71
 
83
72
  const sourceDirFor = (root: string): string => {
@@ -99,9 +88,6 @@ const findImportedSchemaFiles = (imports: SourceImport[]): string[] => {
99
88
  return sortStrings(new Set(files));
100
89
  };
101
90
 
102
- const legacyAssetSpecifier = (source: string): string | null =>
103
- isDataAsset(source) ? source : null;
104
-
105
91
  const blockedAssetSpecifier = (source: string): string | null => {
106
92
  if (!isBareRelativeAsset(source)) {
107
93
  return null;
@@ -159,21 +145,15 @@ const iconModuleSpecifier = (source: string): string | null => {
159
145
 
160
146
  const findAssetModuleSpecifiers = (
161
147
  imports: SourceImport[],
162
- isV2ResourceImports: boolean,
163
- ): { blocked: string[]; icons: string[]; legacy: string[]; resources: string[] } => {
148
+ ): { blocked: string[]; icons: string[]; resources: string[] } => {
164
149
  const collect = (getSpecifier: (source: string) => string | null): string[] =>
165
150
  sortStrings(new Set(imports.map((entry) => getSpecifier(entry.source)).filter((value) => value !== null)));
166
151
 
167
- if (isV2ResourceImports) {
168
- return {
169
- blocked: collect(blockedAssetSpecifier),
170
- icons: collect(iconModuleSpecifier),
171
- legacy: [],
172
- resources: collect(resourceModuleSpecifier),
173
- };
174
- }
175
-
176
- return { blocked: [], icons: [], legacy: collect(legacyAssetSpecifier), resources: [] };
152
+ return {
153
+ blocked: collect(blockedAssetSpecifier),
154
+ icons: collect(iconModuleSpecifier),
155
+ resources: collect(resourceModuleSpecifier),
156
+ };
177
157
  };
178
158
 
179
159
  const canonicalPath = (path: string): string => {
@@ -203,41 +183,10 @@ const assertUniqueSchemaBasenames = (schemaFiles: string[]): void => {
203
183
  }
204
184
  };
205
185
 
206
- const readVisibleEntries = (dir: string): Dirent[] => {
207
- try {
208
- return readdirSync(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith("."));
209
- } catch {
210
- return [];
211
- }
212
- };
213
-
214
- const isSchemaFile = (entry: Dirent): boolean => entry.isFile() && entry.name.endsWith(SCHEMA_SUFFIX);
215
-
216
- const collectSchemaFiles = (dir: string, found: string[]): void => {
217
- for (const entry of readVisibleEntries(dir)) {
218
- const full = join(dir, entry.name);
219
-
220
- if (entry.isDirectory()) {
221
- collectSchemaFiles(full, found);
222
- } else if (isSchemaFile(entry)) {
223
- found.push(full);
224
- }
225
- }
226
- };
227
-
228
- const findSchemaFiles = (dataDir: string): string[] => {
229
- const found: string[] = [];
230
- collectSchemaFiles(dataDir, found);
231
-
232
- return sortStrings(found);
233
- };
234
-
235
- const stageAndCompileProjectSchemas = (root: string, dataDir: string | null): string | null => {
186
+ const stageAndCompileProjectSchemas = (root: string): string | null => {
236
187
  const imports = discoverSourceImports(sourceDirFor(root));
237
- const imported = findImportedSchemaFiles(imports);
238
- assertUniqueSchemaBasenames(imported);
239
- const legacy = dataDir === null ? [] : findSchemaFiles(join(root, dataDir));
240
- const schemaFiles = sortStrings(new Set([...legacy, ...imported]));
188
+ const schemaFiles = findImportedSchemaFiles(imports);
189
+ assertUniqueSchemaBasenames(schemaFiles);
241
190
 
242
191
  if (schemaFiles.length === 0) {
243
192
  return null;
@@ -307,25 +256,14 @@ const didWriteChanges = (path: string, content: string): boolean => {
307
256
  return true;
308
257
  };
309
258
 
310
- const emitSchemaEnv = (
311
- rootDir: string,
312
- dataDir: string | null,
313
- isV2ResourceImports = false,
314
- ): SchemaEnvResult => {
315
- const dataDirAbs = dataDir === null ? null : join(rootDir, dataDir);
316
- const legacyFiles = dataDirAbs === null ? [] : findSchemaFiles(dataDirAbs);
259
+ const emitSchemaEnv = (rootDir: string): SchemaEnvResult => {
317
260
  const imports = discoverSourceImports(sourceDirFor(rootDir));
318
-
319
- const legacy = dataDirAbs === null
320
- ? []
321
- : parseProjectSchemas(legacyFiles, (filePath) => getModuleSpecifier(dataDirAbs, filePath));
322
-
323
261
  const importedFiles = findImportedSchemaFiles(imports);
324
262
  assertUniqueSchemaBasenames(importedFiles);
325
263
  const imported = parseProjectSchemas(importedFiles, getRelativeModuleSpecifier);
326
- const assets = findAssetModuleSpecifiers(imports, isV2ResourceImports);
264
+ const assets = findAssetModuleSpecifiers(imports);
327
265
  const references = existsSync(i18nTypesPath(rootDir)) ? [`./${I18N_TYPES_FILENAME}`] : [];
328
- const content = renderEnvModule([...legacy, ...imported], assets, { references });
266
+ const content = renderEnvModule(imported, assets, { references });
329
267
  const path = schemaEnvPath(rootDir);
330
268
  const isWritten = didWriteChanges(path, content);
331
269