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