@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.
- package/README.md +5 -5
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen/freshness.d.ts.map +1 -1
- package/dist/codegen/freshness.js +45 -15
- package/dist/codegen/freshness.js.map +1 -1
- package/dist/codegen/reference.d.ts +17 -0
- package/dist/codegen/reference.d.ts.map +1 -0
- package/dist/codegen/reference.js +31 -0
- package/dist/codegen/reference.js.map +1 -0
- package/dist/codegen/report.d.ts.map +1 -1
- package/dist/codegen/report.js +18 -15
- package/dist/codegen/report.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +5 -5
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +24 -59
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +0 -1
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js +8 -17
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +6 -22
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +68 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/deploy/freedesktop/copyright.d.ts.map +1 -1
- package/dist/deploy/freedesktop/copyright.js +2 -13
- package/dist/deploy/freedesktop/copyright.js.map +1 -1
- package/dist/deploy/freedesktop/xml.d.ts.map +1 -1
- package/dist/deploy/freedesktop/xml.js +21 -25
- package/dist/deploy/freedesktop/xml.js.map +1 -1
- package/dist/deploy/node-runtime/license.d.ts.map +1 -1
- package/dist/deploy/node-runtime/license.js +2 -9
- package/dist/deploy/node-runtime/license.js.map +1 -1
- package/dist/deploy/notices/gtkx.js +1 -1
- package/dist/deploy/notices/gtkx.js.map +1 -1
- package/dist/deploy/notices/libraries.d.ts.map +1 -1
- package/dist/deploy/notices/libraries.js +2 -1
- package/dist/deploy/notices/libraries.js.map +1 -1
- package/dist/deploy/notices/packages.d.ts.map +1 -1
- package/dist/deploy/notices/packages.js +3 -10
- package/dist/deploy/notices/packages.js.map +1 -1
- package/dist/deploy/payload/copy-tree.d.ts.map +1 -1
- package/dist/deploy/payload/copy-tree.js +3 -3
- package/dist/deploy/payload/copy-tree.js.map +1 -1
- package/dist/deploy/payload/stage.d.ts.map +1 -1
- package/dist/deploy/payload/stage.js +1 -3
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
- package/dist/deploy/settings/build-manifest.js +4 -15
- package/dist/deploy/settings/build-manifest.js.map +1 -1
- package/dist/deploy/settings/libraries.d.ts.map +1 -1
- package/dist/deploy/settings/libraries.js +2 -21
- package/dist/deploy/settings/libraries.js.map +1 -1
- package/dist/deploy/settings/paths.d.ts +1 -2
- package/dist/deploy/settings/paths.d.ts.map +1 -1
- package/dist/deploy/settings/paths.js +5 -15
- package/dist/deploy/settings/paths.js.map +1 -1
- package/dist/deploy/settings/screenshots.d.ts.map +1 -1
- package/dist/deploy/settings/screenshots.js +6 -31
- package/dist/deploy/settings/screenshots.js.map +1 -1
- package/dist/deploy/targets/flatpak-pnpm.js +4 -4
- package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
- package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-source.js +4 -4
- package/dist/deploy/targets/flatpak-source.js.map +1 -1
- package/dist/deploy/targets/flatpak-sources.js +1 -1
- package/dist/deploy/targets/flatpak-sources.js.map +1 -1
- package/dist/deploy/tools.d.ts.map +1 -1
- package/dist/deploy/tools.js +1 -3
- package/dist/deploy/tools.js.map +1 -1
- package/dist/dev/runner-main.js +1 -1
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/schema-dir.d.ts +1 -1
- package/dist/dev/schema-dir.d.ts.map +1 -1
- package/dist/dev/schema-dir.js +2 -4
- package/dist/dev/schema-dir.js.map +1 -1
- package/dist/i18n/catalogs.d.ts.map +1 -1
- package/dist/i18n/catalogs.js +2 -1
- package/dist/i18n/catalogs.js.map +1 -1
- package/dist/i18n/metadata-templates.d.ts.map +1 -1
- package/dist/i18n/metadata-templates.js +2 -1
- package/dist/i18n/metadata-templates.js.map +1 -1
- package/dist/i18n/source-messages.d.ts +3 -21
- package/dist/i18n/source-messages.d.ts.map +1 -1
- package/dist/i18n/source-messages.js +66 -85
- package/dist/i18n/source-messages.js.map +1 -1
- package/dist/i18n/types.d.ts +6 -7
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/i18n/types.js +83 -96
- package/dist/i18n/types.js.map +1 -1
- package/dist/internal/agent-rules.d.ts +7 -0
- package/dist/internal/agent-rules.d.ts.map +1 -0
- package/dist/internal/agent-rules.js +106 -0
- package/dist/internal/agent-rules.js.map +1 -0
- package/dist/internal/docs-elements.d.ts +8 -0
- package/dist/internal/docs-elements.d.ts.map +1 -0
- package/dist/internal/docs-elements.js +12 -0
- package/dist/internal/docs-elements.js.map +1 -0
- package/dist/internal/icon-path.d.ts.map +1 -1
- package/dist/internal/icon-path.js +5 -16
- package/dist/internal/icon-path.js.map +1 -1
- package/dist/internal/source-imports.d.ts.map +1 -1
- package/dist/internal/source-imports.js +10 -42
- package/dist/internal/source-imports.js.map +1 -1
- package/dist/mcp/client-config.d.ts +22 -0
- package/dist/mcp/client-config.d.ts.map +1 -0
- package/dist/mcp/client-config.js +94 -0
- package/dist/mcp/client-config.js.map +1 -0
- package/dist/settings/render.d.ts +0 -1
- package/dist/settings/render.d.ts.map +1 -1
- package/dist/settings/render.js +1 -11
- package/dist/settings/render.js.map +1 -1
- package/dist/settings/schema.d.ts +2 -2
- package/dist/settings/schema.d.ts.map +1 -1
- package/dist/settings/schema.js +21 -62
- package/dist/settings/schema.js.map +1 -1
- package/dist/vite-plugins/asset-imports.d.ts +1 -2
- package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
- package/dist/vite-plugins/asset-imports.js +19 -32
- package/dist/vite-plugins/asset-imports.js.map +1 -1
- package/dist/vite-plugins/asset-specifier.d.ts +1 -3
- package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
- package/dist/vite-plugins/asset-specifier.js +1 -4
- package/dist/vite-plugins/asset-specifier.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts.map +1 -1
- package/dist/vite-plugins/i18n.js +2 -1
- package/dist/vite-plugins/i18n.js.map +1 -1
- package/dist/vite-plugins/index.js +2 -2
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +31 -96
- package/dist/vite-plugins/resources.js.map +1 -1
- package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
- package/dist/vite-plugins/settings-worker-env.js +1 -3
- package/dist/vite-plugins/settings-worker-env.js.map +1 -1
- package/dist/vite-plugins/settings.d.ts +1 -2
- package/dist/vite-plugins/settings.d.ts.map +1 -1
- package/dist/vite-plugins/settings.js +8 -13
- package/dist/vite-plugins/settings.js.map +1 -1
- package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
- package/dist/vite-plugins/undeclared-library.js +10 -13
- package/dist/vite-plugins/undeclared-library.js.map +1 -1
- package/dist/vite-plugins/worker.d.ts.map +1 -1
- package/dist/vite-plugins/worker.js +0 -1
- package/dist/vite-plugins/worker.js.map +1 -1
- package/package.json +21 -14
- package/src/cli.ts +5 -0
- package/src/codegen/freshness.ts +66 -16
- package/src/codegen/reference.ts +52 -0
- package/src/codegen/report.ts +16 -17
- package/src/codegen/run-codegen.ts +31 -91
- package/src/codegen/store-resolver.ts +8 -24
- package/src/commands/docs.ts +6 -36
- package/src/commands/mcp.ts +81 -0
- package/src/deploy/freedesktop/copyright.ts +2 -14
- package/src/deploy/freedesktop/xml.ts +24 -32
- package/src/deploy/node-runtime/license.ts +2 -8
- package/src/deploy/notices/gtkx.ts +1 -1
- package/src/deploy/notices/libraries.ts +2 -1
- package/src/deploy/notices/packages.ts +3 -11
- package/src/deploy/payload/copy-tree.ts +3 -4
- package/src/deploy/payload/stage.ts +2 -6
- package/src/deploy/settings/build-manifest.ts +4 -16
- package/src/deploy/settings/libraries.ts +1 -25
- package/src/deploy/settings/paths.ts +6 -16
- package/src/deploy/settings/screenshots.ts +8 -46
- package/src/deploy/targets/flatpak-pnpm.ts +4 -4
- package/src/deploy/targets/flatpak-source.ts +4 -4
- package/src/deploy/targets/flatpak-sources.ts +1 -1
- package/src/deploy/tools.ts +1 -3
- package/src/dev/runner-main.ts +1 -1
- package/src/dev/schema-dir.ts +2 -4
- package/src/i18n/catalogs.ts +2 -1
- package/src/i18n/metadata-templates.ts +2 -1
- package/src/i18n/source-messages.ts +77 -115
- package/src/i18n/types.ts +97 -131
- package/src/internal/agent-rules.ts +135 -0
- package/src/internal/docs-elements.ts +21 -0
- package/src/internal/icon-path.ts +6 -19
- package/src/internal/source-imports.ts +14 -50
- package/src/mcp/client-config.ts +130 -0
- package/src/settings/render.ts +1 -13
- package/src/settings/schema.ts +19 -81
- package/src/vite-plugins/asset-imports.ts +20 -44
- package/src/vite-plugins/asset-specifier.ts +0 -5
- package/src/vite-plugins/i18n.ts +2 -1
- package/src/vite-plugins/index.ts +2 -2
- package/src/vite-plugins/resources.ts +36 -139
- package/src/vite-plugins/settings-worker-env.ts +1 -3
- package/src/vite-plugins/settings.ts +11 -19
- package/src/vite-plugins/undeclared-library.ts +14 -20
- package/src/vite-plugins/worker.ts +0 -1
- package/dist/i18n/binding-extractor.d.ts +0 -30
- package/dist/i18n/binding-extractor.d.ts.map +0 -1
- package/dist/i18n/binding-extractor.js +0 -1324
- package/dist/i18n/binding-extractor.js.map +0 -1
- package/dist/i18n/extractor-plugin.d.ts +0 -8
- package/dist/i18n/extractor-plugin.d.ts.map +0 -1
- package/dist/i18n/extractor-plugin.js +0 -247
- package/dist/i18n/extractor-plugin.js.map +0 -1
- package/dist/internal/data-dir.d.ts +0 -4
- package/dist/internal/data-dir.d.ts.map +0 -1
- package/dist/internal/data-dir.js +0 -52
- package/dist/internal/data-dir.js.map +0 -1
- package/src/i18n/binding-extractor.ts +0 -1969
- package/src/i18n/extractor-plugin.ts +0 -413
- 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 {
|
|
2
|
-
import {
|
|
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) =>
|
|
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 (!
|
|
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 =
|
|
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 {
|
|
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.
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 };
|
package/src/settings/render.ts
CHANGED
|
@@ -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: [],
|
|
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`;
|
package/src/settings/schema.ts
CHANGED
|
@@ -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,
|
|
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
|
|
57
|
+
let projectRoot: string;
|
|
58
|
+
let path: string;
|
|
64
59
|
|
|
65
60
|
try {
|
|
66
|
-
|
|
61
|
+
projectRoot = realpathSync(root);
|
|
62
|
+
path = realpathSync(filePath);
|
|
67
63
|
} catch {
|
|
68
64
|
return null;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
|
238
|
-
assertUniqueSchemaBasenames(
|
|
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
|
|
264
|
+
const assets = findAssetModuleSpecifiers(imports);
|
|
327
265
|
const references = existsSync(i18nTypesPath(rootDir)) ? [`./${I18N_TYPES_FILENAME}`] : [];
|
|
328
|
-
const content = renderEnvModule(
|
|
266
|
+
const content = renderEnvModule(imported, assets, { references });
|
|
329
267
|
const path = schemaEnvPath(rootDir);
|
|
330
268
|
const isWritten = didWriteChanges(path, content);
|
|
331
269
|
|