@gtkx/cli 1.5.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 (91) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/codegen/freshness.d.ts.map +1 -1
  4. package/dist/codegen/freshness.js +2 -1
  5. package/dist/codegen/freshness.js.map +1 -1
  6. package/dist/codegen/reference.d.ts +17 -0
  7. package/dist/codegen/reference.d.ts.map +1 -0
  8. package/dist/codegen/reference.js +37 -0
  9. package/dist/codegen/reference.js.map +1 -0
  10. package/dist/codegen/report.d.ts.map +1 -1
  11. package/dist/codegen/report.js +17 -3
  12. package/dist/codegen/report.js.map +1 -1
  13. package/dist/codegen/run-codegen.d.ts +2 -0
  14. package/dist/codegen/run-codegen.d.ts.map +1 -1
  15. package/dist/codegen/run-codegen.js +63 -21
  16. package/dist/codegen/run-codegen.js.map +1 -1
  17. package/dist/commands/docs.d.ts.map +1 -1
  18. package/dist/commands/docs.js +10 -16
  19. package/dist/commands/docs.js.map +1 -1
  20. package/dist/commands/mcp.d.ts +13 -0
  21. package/dist/commands/mcp.d.ts.map +1 -0
  22. package/dist/commands/mcp.js +68 -0
  23. package/dist/commands/mcp.js.map +1 -0
  24. package/dist/deploy/notices/gtkx.js +1 -1
  25. package/dist/deploy/notices/gtkx.js.map +1 -1
  26. package/dist/deploy/notices/libraries.d.ts.map +1 -1
  27. package/dist/deploy/notices/libraries.js +2 -1
  28. package/dist/deploy/notices/libraries.js.map +1 -1
  29. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  30. package/dist/deploy/settings/libraries.js +7 -9
  31. package/dist/deploy/settings/libraries.js.map +1 -1
  32. package/dist/deploy/tools.d.ts.map +1 -1
  33. package/dist/deploy/tools.js +1 -3
  34. package/dist/deploy/tools.js.map +1 -1
  35. package/dist/dev/runner-main.d.ts.map +1 -1
  36. package/dist/dev/runner-main.js +2 -1
  37. package/dist/dev/runner-main.js.map +1 -1
  38. package/dist/internal/agent-rules.d.ts +7 -0
  39. package/dist/internal/agent-rules.d.ts.map +1 -0
  40. package/dist/internal/agent-rules.js +106 -0
  41. package/dist/internal/agent-rules.js.map +1 -0
  42. package/dist/internal/docs-elements.d.ts +8 -0
  43. package/dist/internal/docs-elements.d.ts.map +1 -0
  44. package/dist/internal/docs-elements.js +12 -0
  45. package/dist/internal/docs-elements.js.map +1 -0
  46. package/dist/internal/source-imports.d.ts.map +1 -1
  47. package/dist/internal/source-imports.js +2 -3
  48. package/dist/internal/source-imports.js.map +1 -1
  49. package/dist/mcp/client-config.d.ts +22 -0
  50. package/dist/mcp/client-config.d.ts.map +1 -0
  51. package/dist/mcp/client-config.js +94 -0
  52. package/dist/mcp/client-config.js.map +1 -0
  53. package/dist/vite-plugins/animated.d.ts +5 -0
  54. package/dist/vite-plugins/animated.d.ts.map +1 -0
  55. package/dist/vite-plugins/animated.js +325 -0
  56. package/dist/vite-plugins/animated.js.map +1 -0
  57. package/dist/vite-plugins/asset-imports.js +2 -2
  58. package/dist/vite-plugins/asset-imports.js.map +1 -1
  59. package/dist/vite-plugins/index.d.ts.map +1 -1
  60. package/dist/vite-plugins/index.js +2 -0
  61. package/dist/vite-plugins/index.js.map +1 -1
  62. package/dist/vite-plugins/resources.js +2 -2
  63. package/dist/vite-plugins/resources.js.map +1 -1
  64. package/dist/vite-plugins/settings-worker-env.js +2 -2
  65. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  66. package/dist/vite-plugins/settings.d.ts.map +1 -1
  67. package/dist/vite-plugins/settings.js +2 -1
  68. package/dist/vite-plugins/settings.js.map +1 -1
  69. package/package.json +10 -9
  70. package/src/cli.ts +5 -0
  71. package/src/codegen/freshness.ts +2 -1
  72. package/src/codegen/reference.ts +59 -0
  73. package/src/codegen/report.ts +18 -3
  74. package/src/codegen/run-codegen.ts +81 -24
  75. package/src/commands/docs.ts +10 -28
  76. package/src/commands/mcp.ts +81 -0
  77. package/src/deploy/notices/gtkx.ts +1 -1
  78. package/src/deploy/notices/libraries.ts +2 -1
  79. package/src/deploy/settings/libraries.ts +7 -10
  80. package/src/deploy/tools.ts +1 -3
  81. package/src/dev/runner-main.ts +2 -1
  82. package/src/internal/agent-rules.ts +135 -0
  83. package/src/internal/docs-elements.ts +21 -0
  84. package/src/internal/source-imports.ts +3 -3
  85. package/src/mcp/client-config.ts +130 -0
  86. package/src/vite-plugins/animated.ts +486 -0
  87. package/src/vite-plugins/asset-imports.ts +2 -2
  88. package/src/vite-plugins/index.ts +2 -0
  89. package/src/vite-plugins/resources.ts +2 -2
  90. package/src/vite-plugins/settings-worker-env.ts +2 -2
  91. package/src/vite-plugins/settings.ts +2 -1
@@ -36,7 +36,8 @@ const SUMMARY = [
36
36
  "their own terms; this package does not distribute them at all.",
37
37
  "The GTKX native addon does link GLib, GObject and GIO dynamically against the copies already installed",
38
38
  "on the system, which makes the addon a work that uses those libraries, and section 6 of the LGPL asks",
39
- "such a work to carry three things. This is the notice it asks for. The license itself is published at",
39
+ "such a work to carry a notice, the license, and the copyright notices. This is the notice it asks for,",
40
+ "and the license itself is published at",
40
41
  `${LGPL_URL},`,
41
42
  "and the copyright notice of each library is published with its sources, at the address listed beside",
42
43
  "it below. Linking against an installed shared library is the mechanism section 6(b) allows, so the",
@@ -6,15 +6,13 @@ import {
6
6
  resolveGirPath,
7
7
  resolveStore,
8
8
  } from "@gtkx/codegen";
9
+ import { resolveFuture } from "@gtkx/config/internal";
9
10
 
10
11
  type ResolvedLibraries = {
11
12
  libraries: string[];
12
13
  minimumLibraryVersions: Record<string, string>;
13
14
  };
14
15
 
15
- const DEFAULT_LIBRARY = "Gtk-4.0";
16
- const DEFAULT_LIBRARY_PREFIX = "Gtk-";
17
-
18
16
  const generatedLibraries = (root: string): GeneratedLibraries | null => {
19
17
  try {
20
18
  return readGeneratedLibraries(resolveStore(root).gi.storeDir);
@@ -23,24 +21,23 @@ const generatedLibraries = (root: string): GeneratedLibraries | null => {
23
21
  }
24
22
  };
25
23
 
26
- const discoveredLibraries = (config: Config): string[] | null => {
24
+ const discoveredLibraries = (config: Config, isAdwaitaDefault: boolean): string[] | null => {
27
25
  try {
28
- return expandLibraries(config.libraries, resolveGirPath(config.girPath));
26
+ return expandLibraries(config.libraries, resolveGirPath(config.girPath), isAdwaitaDefault);
29
27
  } catch {
30
28
  return null;
31
29
  }
32
30
  };
33
31
 
34
32
  const configLibraries = (config: Config): string[] => {
33
+ const { isAdwaitaDefault } = resolveFuture(config.future);
35
34
  const libraries = config.libraries;
36
35
 
37
- if (!Array.isArray(libraries)) {
38
- return discoveredLibraries(config) ?? [DEFAULT_LIBRARY];
36
+ if (Array.isArray(libraries)) {
37
+ return expandLibraries(libraries, [], isAdwaitaDefault);
39
38
  }
40
39
 
41
- return libraries.some((library) => library.startsWith(DEFAULT_LIBRARY_PREFIX))
42
- ? [...libraries]
43
- : [DEFAULT_LIBRARY, ...libraries];
40
+ return discoveredLibraries(config, isAdwaitaDefault) ?? expandLibraries(undefined, [], isAdwaitaDefault);
44
41
  };
45
42
 
46
43
  const resolveLibraries = (root: string, config: Config): ResolvedLibraries => {
@@ -145,11 +145,9 @@ const toolLines = (tool: DeployTool, hints: string[]): string[] => [
145
145
  const missingToolsMessage = (report: ToolReport): string => {
146
146
  const family = detectPackageFamily();
147
147
  const required = report.missingRequired.flatMap((tool) => toolLines(tool, installHints(tool.command, family)));
148
- const count = report.missingRequired.length;
149
- const plural = count === 1 ? "tool is" : "tools are";
150
148
 
151
149
  return [
152
- `Cannot deploy: ${String(count)} required ${plural} missing.`,
150
+ "Cannot deploy without these tools:",
153
151
  "",
154
152
  ...required,
155
153
  "",
@@ -1,4 +1,5 @@
1
1
  import { loadConfig } from "@gtkx/config";
2
+ import { resolveFuture } from "@gtkx/config/internal";
2
3
  import { error } from "@gtkx/utils";
3
4
  import { resolve } from "node:path";
4
5
  import { DEV_ENTRY_ENV } from "./entry-env.js";
@@ -24,7 +25,7 @@ const main = async (): Promise<void> => {
24
25
 
25
26
  const { config, root } = await loadConfig(cwd, { mode: "development" });
26
27
  prepareDevLocaleDir(root, config.applicationId);
27
- prepareDevSchemaDir(root, config.future?.v2ResourceImports === true);
28
+ prepareDevSchemaDir(root, resolveFuture(config.future).isResourceImported);
28
29
  prepareDevIconDir(root, config.applicationId, config.applicationIcon);
29
30
  const entryPath = resolve(cwd, entryArg);
30
31
  const { defaultDevRunnerDeps } = await import("./runner-deps.js");
@@ -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 };
@@ -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
 
@@ -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 };