@gtkx/cli 1.0.0-rc.4 → 1.0.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 +5 -5
- package/dist/builder.d.ts +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -1
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/codegen/freshness.d.ts.map +1 -1
- package/dist/codegen/freshness.js +0 -2
- package/dist/codegen/freshness.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +1 -1
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +1 -0
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +1 -1
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +3 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/dev/entry-env.d.ts +3 -0
- package/dist/dev/entry-env.d.ts.map +1 -0
- package/dist/dev/entry-env.js +3 -0
- package/dist/dev/entry-env.js.map +1 -0
- package/dist/dev/runner-deps.d.ts.map +1 -1
- package/dist/dev/runner-deps.js +1 -0
- package/dist/dev/runner-deps.js.map +1 -1
- package/dist/dev/runner-main.d.ts.map +1 -1
- package/dist/dev/runner-main.js +3 -3
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/runner.d.ts +1 -0
- package/dist/dev/runner.d.ts.map +1 -1
- package/dist/dev/runner.js +22 -12
- package/dist/dev/runner.js.map +1 -1
- package/dist/dev/supervisor.d.ts +10 -3
- package/dist/dev/supervisor.d.ts.map +1 -1
- package/dist/dev/supervisor.js +23 -4
- package/dist/dev/supervisor.js.map +1 -1
- package/dist/internal/application-args.d.ts +6 -0
- package/dist/internal/application-args.d.ts.map +1 -0
- package/dist/internal/application-args.js +10 -0
- package/dist/internal/application-args.js.map +1 -0
- package/dist/internal/errors.d.ts +1 -2
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/run-cli-tool.d.ts +10 -0
- package/dist/internal/run-cli-tool.d.ts.map +1 -0
- package/dist/internal/run-cli-tool.js +15 -0
- package/dist/internal/run-cli-tool.js.map +1 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +3 -0
- package/dist/internal.js.map +1 -0
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/handlers.d.ts +1 -1
- package/dist/mcp/handlers.d.ts.map +1 -1
- package/dist/mcp/handlers.js +78 -41
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/serialize-widget.d.ts +2 -2
- package/dist/mcp/serialize-widget.d.ts.map +1 -1
- package/dist/mcp/serialize-widget.js +47 -17
- package/dist/mcp/serialize-widget.js.map +1 -1
- package/dist/mcp/widget-properties.d.ts +6 -0
- package/dist/mcp/widget-properties.d.ts.map +1 -0
- package/dist/mcp/widget-properties.js +102 -0
- package/dist/mcp/widget-properties.js.map +1 -0
- package/dist/settings/compile.d.ts.map +1 -1
- package/dist/settings/compile.js +8 -12
- package/dist/settings/compile.js.map +1 -1
- package/dist/vite-plugins/asset-emitter.d.ts +9 -0
- package/dist/vite-plugins/asset-emitter.d.ts.map +1 -0
- package/dist/vite-plugins/asset-emitter.js +2 -0
- package/dist/vite-plugins/asset-emitter.js.map +1 -0
- package/dist/vite-plugins/asset-extensions.d.ts +1 -2
- package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
- package/dist/vite-plugins/asset-extensions.js +1 -1
- package/dist/vite-plugins/asset-extensions.js.map +1 -1
- package/dist/vite-plugins/built-url.d.ts.map +1 -1
- package/dist/vite-plugins/built-url.js +14 -10
- package/dist/vite-plugins/built-url.js.map +1 -1
- package/dist/vite-plugins/ensure-store.d.ts +4 -0
- package/dist/vite-plugins/ensure-store.d.ts.map +1 -0
- package/dist/vite-plugins/ensure-store.js +10 -0
- package/dist/vite-plugins/ensure-store.js.map +1 -0
- package/dist/vite-plugins/icons.d.ts.map +1 -1
- package/dist/vite-plugins/icons.js.map +1 -1
- package/dist/vite-plugins/react-compiler.d.ts +1 -1
- package/dist/vite-plugins/react-compiler.d.ts.map +1 -1
- package/dist/vite-plugins/react-compiler.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts +1 -1
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +6 -15
- package/dist/vite-plugins/resources.js.map +1 -1
- package/dist/vite-plugins/settings.js.map +1 -1
- package/dist/vite-plugins/undeclared-library.d.ts +1 -1
- package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
- package/dist/vite-plugins/undeclared-library.js.map +1 -1
- package/dist/vite-plugins/worker.d.ts +4 -0
- package/dist/vite-plugins/worker.d.ts.map +1 -0
- package/dist/vite-plugins/worker.js +136 -0
- package/dist/vite-plugins/worker.js.map +1 -0
- package/dist/vitest-plugin.d.ts +7 -0
- package/dist/vitest-plugin.d.ts.map +1 -1
- package/dist/vitest-plugin.js +14 -1
- package/dist/vitest-plugin.js.map +1 -1
- package/package.json +16 -12
- package/src/builder.ts +3 -2
- package/src/cli.ts +4 -2
- package/src/codegen/freshness.ts +0 -2
- package/src/codegen/run-codegen.ts +1 -1
- package/src/codegen/store-resolver.ts +1 -1
- package/src/commands/dev.ts +3 -1
- package/src/dev/entry-env.ts +3 -0
- package/src/dev/runner-deps.ts +1 -0
- package/src/dev/runner-main.ts +3 -4
- package/src/dev/runner.ts +44 -13
- package/src/dev/supervisor.ts +54 -11
- package/src/internal/application-args.ts +13 -0
- package/src/internal/errors.ts +1 -1
- package/src/internal/run-cli-tool.ts +22 -0
- package/src/internal.ts +2 -0
- package/src/mcp/client.ts +1 -1
- package/src/mcp/handlers.ts +123 -56
- package/src/mcp/index.ts +1 -0
- package/src/mcp/serialize-widget.ts +71 -21
- package/src/mcp/widget-properties.ts +153 -0
- package/src/settings/compile.ts +8 -11
- package/src/vite-plugins/asset-emitter.ts +5 -0
- package/src/vite-plugins/asset-extensions.ts +1 -1
- package/src/vite-plugins/built-url.ts +19 -13
- package/src/vite-plugins/ensure-store.ts +14 -0
- package/src/vite-plugins/icons.ts +2 -5
- package/src/vite-plugins/react-compiler.ts +2 -1
- package/src/vite-plugins/resources.ts +10 -19
- package/src/vite-plugins/settings.ts +2 -2
- package/src/vite-plugins/undeclared-library.ts +1 -1
- package/src/vite-plugins/worker.ts +235 -0
- package/src/vitest-plugin.ts +15 -1
- package/dist/internal/resolve-cli-tool.d.ts +0 -3
- package/dist/internal/resolve-cli-tool.d.ts.map +0 -1
- package/dist/internal/resolve-cli-tool.js +0 -27
- package/dist/internal/resolve-cli-tool.js.map +0 -1
- package/src/internal/resolve-cli-tool.ts +0 -37
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { packageVersion } from "@gtkx/utils";
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { splitApplicationArgs } from "./internal/application-args.js";
|
|
3
4
|
import { withErrorBoundary } from "./internal/errors.js";
|
|
4
5
|
|
|
5
|
-
const version = packageVersion(import.meta.url);
|
|
6
|
+
const version = packageVersion(import.meta.url, "../package.json");
|
|
7
|
+
const { cliArgs } = splitApplicationArgs(process.argv.slice(2));
|
|
6
8
|
|
|
7
9
|
const main = defineCommand({
|
|
8
10
|
meta: {
|
|
@@ -39,4 +41,4 @@ const main = defineCommand({
|
|
|
39
41
|
},
|
|
40
42
|
});
|
|
41
43
|
|
|
42
|
-
await runMain(main);
|
|
44
|
+
await runMain(main, { rawArgs: cliArgs });
|
package/src/codegen/freshness.ts
CHANGED
|
@@ -27,8 +27,6 @@ const namespaceBarrelPath = (giStoreDir: string, library: string): string => {
|
|
|
27
27
|
return join(giStoreDir, namespace, "index.js");
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// The manifest is what makes the store resolvable as a package, and npm's prune of a generated store
|
|
31
|
-
// removes the link while leaving the directory behind, so both have to be checked.
|
|
32
30
|
const hasManifest = (storeDir: string): boolean => existsSync(join(storeDir, "package.json"));
|
|
33
31
|
|
|
34
32
|
const isGiStoreStale = (store: CodegenStore, libraries: string[]): boolean => {
|
|
@@ -200,6 +200,7 @@ const getRunOptions = ({ root, mode, inputs, resolved }: RunOptionsInput): RunCo
|
|
|
200
200
|
return { cwd: root, mode, inputs, resolved };
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
/* eslint-disable-next-line unicorn/consistent-boolean-name -- the boolean reports whether codegen ran */
|
|
203
204
|
const ensureGenerated = async (
|
|
204
205
|
cwd: string,
|
|
205
206
|
options: { shouldAnnounce?: boolean; mode?: string } = {},
|
|
@@ -245,6 +246,5 @@ export {
|
|
|
245
246
|
syncSchemaEnv,
|
|
246
247
|
ensureGenerated,
|
|
247
248
|
resolveConfigWatch,
|
|
248
|
-
type RunCodegenOptions,
|
|
249
249
|
type RunCodegenResult,
|
|
250
250
|
};
|
|
@@ -83,4 +83,4 @@ const resolveCodegenContext = async (cwd: string, mode?: string): Promise<Codege
|
|
|
83
83
|
return { root: cwd, config, configFile };
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
export { resolveCodegenStore, resolveCodegenContext, type CodegenStore
|
|
86
|
+
export { resolveCodegenStore, resolveCodegenContext, type CodegenStore };
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { ensureGenerated, resolveConfigWatch } from "../codegen/run-codegen.js";
|
|
3
3
|
import { type DevWatch, runDevSupervisor } from "../dev/supervisor.js";
|
|
4
|
+
import { splitApplicationArgs } from "../internal/application-args.js";
|
|
4
5
|
import { entryArg, resolveEntry } from "../internal/entry-arg.js";
|
|
5
6
|
|
|
6
7
|
const DEV_MODE = "development";
|
|
@@ -17,7 +18,8 @@ const dev = defineCommand({
|
|
|
17
18
|
const { cwd, entry: entryPath } = resolveEntry(args);
|
|
18
19
|
await ensureGenerated(cwd, { shouldAnnounce: true, mode: DEV_MODE });
|
|
19
20
|
const watch: DevWatch | undefined = await resolveConfigWatch(cwd, DEV_MODE);
|
|
20
|
-
|
|
21
|
+
const { applicationArgs } = splitApplicationArgs(process.argv.slice(2));
|
|
22
|
+
await runDevSupervisor({ entryPath, cwd, args: applicationArgs, watch });
|
|
21
23
|
},
|
|
22
24
|
});
|
|
23
25
|
|
package/src/dev/runner-deps.ts
CHANGED
|
@@ -45,6 +45,7 @@ const defaultDevRunnerDeps = (): DevRunnerDeps => ({
|
|
|
45
45
|
watchApplicationShutdown: (onShutdown) => {
|
|
46
46
|
Gio.Application.getDefault()?.on("shutdown", onShutdown);
|
|
47
47
|
},
|
|
48
|
+
isApplicationRegistered: () => Gio.Application.getDefault()?.getIsRegistered() === true,
|
|
48
49
|
installShutdownHandlers: (onSignal) => {
|
|
49
50
|
installGracefulShutdown({ onSignal });
|
|
50
51
|
},
|
package/src/dev/runner-main.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { error } from "@gtkx/utils";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { resolveDataDir } from "../internal/data-dir.js";
|
|
4
|
+
import { DEV_ENTRY_ENV } from "./entry-env.js";
|
|
4
5
|
import { prepareDevIconDir } from "./icon-dir.js";
|
|
5
6
|
import { createDevRunner } from "./runner.js";
|
|
6
7
|
import { prepareDevSchemaDir } from "./schema-dir.js";
|
|
7
8
|
|
|
8
|
-
const ENTRY_ARG_INDEX = 2;
|
|
9
|
-
|
|
10
9
|
const main = async (): Promise<void> => {
|
|
11
10
|
if (process.channel) {
|
|
12
11
|
process.once("disconnect", () => {
|
|
@@ -15,10 +14,10 @@ const main = async (): Promise<void> => {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
const cwd = process.cwd();
|
|
18
|
-
const entryArg = process.
|
|
17
|
+
const entryArg = process.env[DEV_ENTRY_ENV];
|
|
19
18
|
|
|
20
19
|
if (!entryArg) {
|
|
21
|
-
error(
|
|
20
|
+
error(`Missing ${DEV_ENTRY_ENV}`);
|
|
22
21
|
process.exit(1);
|
|
23
22
|
}
|
|
24
23
|
|
package/src/dev/runner.ts
CHANGED
|
@@ -14,6 +14,7 @@ type DevRunnerDeps = {
|
|
|
14
14
|
): Promise<unknown>;
|
|
15
15
|
stopMcpClient(): void;
|
|
16
16
|
watchApplicationShutdown(onShutdown: () => void): void;
|
|
17
|
+
isApplicationRegistered(): boolean;
|
|
17
18
|
installShutdownHandlers(onSignal: () => void | Promise<void>): void;
|
|
18
19
|
quitDefaultApplication(): void;
|
|
19
20
|
performRefresh: () => void;
|
|
@@ -126,7 +127,11 @@ const onShutdownSignal =
|
|
|
126
127
|
});
|
|
127
128
|
};
|
|
128
129
|
|
|
129
|
-
const closeAndExit = async (
|
|
130
|
+
const closeAndExit = async (
|
|
131
|
+
deps: DevRunnerDeps,
|
|
132
|
+
controller: ShutdownController,
|
|
133
|
+
code = 0,
|
|
134
|
+
): Promise<never> => {
|
|
130
135
|
try {
|
|
131
136
|
await controller.shutdown((): void => undefined);
|
|
132
137
|
} catch (error_) {
|
|
@@ -135,7 +140,7 @@ const closeAndExit = async (deps: DevRunnerDeps, controller: ShutdownController)
|
|
|
135
140
|
return deps.exit(1);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
return deps.exit(
|
|
143
|
+
return deps.exit(code);
|
|
139
144
|
};
|
|
140
145
|
|
|
141
146
|
const onApplicationShutdown =
|
|
@@ -155,6 +160,42 @@ const onApplicationShutdown =
|
|
|
155
160
|
void closeAndExit(deps, controller);
|
|
156
161
|
};
|
|
157
162
|
|
|
163
|
+
const refusedExitCode = (): number => (process.exitCode === undefined ? 1 : Number(process.exitCode));
|
|
164
|
+
|
|
165
|
+
const connectApplication = async (
|
|
166
|
+
deps: DevRunnerDeps,
|
|
167
|
+
server: DevServer,
|
|
168
|
+
liveApplicationId: string,
|
|
169
|
+
tracked: { refreshTracker: RefreshTracker; controller: ShutdownController },
|
|
170
|
+
): Promise<void> => {
|
|
171
|
+
deps.watchApplicationShutdown(onApplicationShutdown(deps, tracked.refreshTracker, tracked.controller));
|
|
172
|
+
const applicationId = (await deps.getConfiguredApplicationId(process.cwd())) ?? liveApplicationId;
|
|
173
|
+
deps.log(`Connected application ID: ${applicationId}`);
|
|
174
|
+
await deps.startMcpClient(applicationId, (id) => server.ssrLoadModule(id));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const attachApplication = async (
|
|
178
|
+
deps: DevRunnerDeps,
|
|
179
|
+
server: DevServer,
|
|
180
|
+
tracked: { refreshTracker: RefreshTracker; controller: ShutdownController },
|
|
181
|
+
): Promise<void> => {
|
|
182
|
+
const liveApplicationId = await deps.waitForApplicationId(APPLICATION_MOUNT_TIMEOUT_MS);
|
|
183
|
+
|
|
184
|
+
if (liveApplicationId === null) {
|
|
185
|
+
deps.log("Entry did not mount an application - MCP client not started.");
|
|
186
|
+
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!deps.isApplicationRegistered()) {
|
|
191
|
+
deps.log("Application refused its command line - stopping dev runner...");
|
|
192
|
+
|
|
193
|
+
return closeAndExit(deps, tracked.controller, refusedExitCode());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await connectApplication(deps, server, liveApplicationId, tracked);
|
|
197
|
+
};
|
|
198
|
+
|
|
158
199
|
const createDevRunner = (deps: DevRunnerDeps): DevRunner => ({
|
|
159
200
|
async run(entryPath: string): Promise<void> {
|
|
160
201
|
const root = process.cwd();
|
|
@@ -166,17 +207,7 @@ const createDevRunner = (deps: DevRunnerDeps): DevRunner => ({
|
|
|
166
207
|
server.watcher.on("change", onFileChange(server, refreshTrackingDeps, controller));
|
|
167
208
|
deps.log(`Loading entry: ${entryPath}`);
|
|
168
209
|
await server.ssrLoadModule(entryPath);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (liveApplicationId) {
|
|
172
|
-
deps.watchApplicationShutdown(onApplicationShutdown(deps, refreshTracker, controller));
|
|
173
|
-
const applicationId = (await deps.getConfiguredApplicationId(root)) ?? liveApplicationId;
|
|
174
|
-
deps.log(`Connected application ID: ${applicationId}`);
|
|
175
|
-
await deps.startMcpClient(applicationId, (id) => server.ssrLoadModule(id));
|
|
176
|
-
} else {
|
|
177
|
-
deps.log("Entry did not mount an application - MCP client not started.");
|
|
178
|
-
}
|
|
179
|
-
|
|
210
|
+
await attachApplication(deps, server, { refreshTracker, controller });
|
|
180
211
|
deps.log("HMR enabled - watching for changes...");
|
|
181
212
|
},
|
|
182
213
|
});
|
package/src/dev/supervisor.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { fork as nodeFork } from "node:child_process";
|
|
|
3
3
|
import { type FSWatcher, watch as watchFs } from "node:fs";
|
|
4
4
|
import { basename, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { DEV_ENTRY_ENV } from "./entry-env.js";
|
|
6
7
|
|
|
7
8
|
type SupervisedChild = {
|
|
8
9
|
killed: boolean;
|
|
@@ -13,7 +14,7 @@ type SupervisedChild = {
|
|
|
13
14
|
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): void;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
type ForkRunner = (modulePath: string, args: string[], cwd: string) => SupervisedChild;
|
|
17
|
+
type ForkRunner = (modulePath: string, args: string[], env: NodeJS.ProcessEnv, cwd: string) => SupervisedChild;
|
|
17
18
|
|
|
18
19
|
type DevWatch = {
|
|
19
20
|
paths: string[];
|
|
@@ -24,6 +25,7 @@ type SupervisorState = {
|
|
|
24
25
|
runnerPath: string;
|
|
25
26
|
entryPath: string;
|
|
26
27
|
cwd: string;
|
|
28
|
+
args: string[];
|
|
27
29
|
watch: DevWatch | undefined;
|
|
28
30
|
watchers: FSWatcher[];
|
|
29
31
|
fork: ForkRunner;
|
|
@@ -33,15 +35,45 @@ type SupervisorState = {
|
|
|
33
35
|
capturedChildExit: number | undefined;
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
type DevSupervisorOptions = {
|
|
39
|
+
entryPath: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
args?: string[] | undefined;
|
|
42
|
+
watch?: DevWatch | undefined;
|
|
43
|
+
fork?: ForkRunner | undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
36
46
|
type DebounceTimer = { handle: NodeJS.Timeout | null };
|
|
37
47
|
|
|
38
48
|
const DEV_RUNNER_URL = new URL("../../bin/gtkx-dev-runner.js", import.meta.url);
|
|
39
49
|
const FORCE_KILL_TIMEOUT_MS = 5000;
|
|
40
50
|
const CONFIG_DEBOUNCE_MS = 150;
|
|
41
51
|
const RESTART_EXIT_CODE = 75;
|
|
52
|
+
const CONDITION_FLAG = /^(?:--conditions|-C)(?:=.*)?$/;
|
|
53
|
+
const SPLIT_CONDITION_FLAG = /^(?:--conditions|-C)$/;
|
|
54
|
+
|
|
55
|
+
const withoutConditions = (argv: string[]): string[] =>
|
|
56
|
+
argv.filter(
|
|
57
|
+
(argument, index) => !CONDITION_FLAG.test(argument) && !SPLIT_CONDITION_FLAG.test(argv[index - 1] ?? ""),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const getNodeOptions = (env: NodeJS.ProcessEnv): string | undefined => {
|
|
61
|
+
const options = env.NODE_OPTIONS;
|
|
62
|
+
|
|
63
|
+
return options === undefined ? undefined : withoutConditions(options.split(/\s+/)).join(" ");
|
|
64
|
+
};
|
|
42
65
|
|
|
43
|
-
const defaultForkRunner: ForkRunner = (modulePath, args, cwd) =>
|
|
44
|
-
|
|
66
|
+
const defaultForkRunner: ForkRunner = (modulePath, args, env, cwd) => {
|
|
67
|
+
const nodeOptions = getNodeOptions(env);
|
|
68
|
+
|
|
69
|
+
return nodeFork(modulePath, args, {
|
|
70
|
+
cwd,
|
|
71
|
+
env: { ...env, ...(nodeOptions !== undefined && { NODE_OPTIONS: nodeOptions }) },
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
detached: true,
|
|
74
|
+
execArgv: withoutConditions(process.execArgv),
|
|
75
|
+
});
|
|
76
|
+
};
|
|
45
77
|
|
|
46
78
|
const forwardSignal = (child: SupervisedChild, signal: NodeJS.Signals): void => {
|
|
47
79
|
if (!child.killed) {
|
|
@@ -93,7 +125,13 @@ const handleChildExit = (state: SupervisorState, code: number | null, signal: No
|
|
|
93
125
|
};
|
|
94
126
|
|
|
95
127
|
const launch = (state: SupervisorState): void => {
|
|
96
|
-
const child = state.fork(
|
|
128
|
+
const child = state.fork(
|
|
129
|
+
state.runnerPath,
|
|
130
|
+
state.args,
|
|
131
|
+
{ ...process.env, [DEV_ENTRY_ENV]: state.entryPath },
|
|
132
|
+
state.cwd,
|
|
133
|
+
);
|
|
134
|
+
|
|
97
135
|
state.child = child;
|
|
98
136
|
|
|
99
137
|
child.on("exit", (code, signal) => {
|
|
@@ -247,16 +285,14 @@ const installShutdown = (state: SupervisorState): void => {
|
|
|
247
285
|
});
|
|
248
286
|
};
|
|
249
287
|
|
|
250
|
-
const runDevSupervisor = async (
|
|
251
|
-
entryPath
|
|
252
|
-
|
|
253
|
-
watch?: DevWatch,
|
|
254
|
-
fork: ForkRunner = defaultForkRunner,
|
|
255
|
-
): Promise<never> => {
|
|
288
|
+
const runDevSupervisor = async (options: DevSupervisorOptions): Promise<never> => {
|
|
289
|
+
const { entryPath, cwd, args = [], watch, fork = defaultForkRunner } = options;
|
|
290
|
+
|
|
256
291
|
const state: SupervisorState = {
|
|
257
292
|
runnerPath: fileURLToPath(DEV_RUNNER_URL),
|
|
258
293
|
entryPath,
|
|
259
294
|
cwd,
|
|
295
|
+
args,
|
|
260
296
|
watch,
|
|
261
297
|
watchers: [],
|
|
262
298
|
fork,
|
|
@@ -273,4 +309,11 @@ const runDevSupervisor = async (
|
|
|
273
309
|
return new Promise<never>((): void => undefined);
|
|
274
310
|
};
|
|
275
311
|
|
|
276
|
-
export {
|
|
312
|
+
export {
|
|
313
|
+
RESTART_EXIT_CODE,
|
|
314
|
+
defaultForkRunner,
|
|
315
|
+
runDevSupervisor,
|
|
316
|
+
type DevWatch,
|
|
317
|
+
type ForkRunner,
|
|
318
|
+
type SupervisedChild,
|
|
319
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const APPLICATION_ARGS_SEPARATOR = "--";
|
|
2
|
+
|
|
3
|
+
const splitApplicationArgs = (argv: string[]): { cliArgs: string[]; applicationArgs: string[] } => {
|
|
4
|
+
const separator = argv.indexOf(APPLICATION_ARGS_SEPARATOR);
|
|
5
|
+
|
|
6
|
+
if (separator === -1) {
|
|
7
|
+
return { cliArgs: argv, applicationArgs: [] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return { cliArgs: argv.slice(0, separator), applicationArgs: argv.slice(separator + 1) };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { splitApplicationArgs };
|
package/src/internal/errors.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { formatChildProcessError, resolveExecutable } from "@gtkx/utils";
|
|
2
|
+
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
type CliToolInvocation = {
|
|
5
|
+
tool: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
target?: string;
|
|
8
|
+
options?: ExecFileSyncOptions;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const runCliTool = ({ tool, args, target, options }: CliToolInvocation): void => {
|
|
12
|
+
try {
|
|
13
|
+
execFileSync(resolveExecutable(tool), args, options);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const details = formatChildProcessError(error);
|
|
16
|
+
const suffix = details ? `:\n${details}` : "";
|
|
17
|
+
const subject = target === undefined ? "" : ` for ${target}`;
|
|
18
|
+
throw new Error(`${tool} failed${subject}${suffix}`, { cause: error });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { runCliTool };
|
package/src/internal.ts
ADDED
package/src/mcp/client.ts
CHANGED
package/src/mcp/handlers.ts
CHANGED
|
@@ -8,11 +8,10 @@ import {
|
|
|
8
8
|
ServerRequestParamsSchemas,
|
|
9
9
|
widgetNotFoundError,
|
|
10
10
|
} from "@gtkx/mcp/internal";
|
|
11
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
11
|
import type { WidgetRegistry } from "./widget-registry.js";
|
|
14
12
|
import { serializeWidget } from "./serialize-widget.js";
|
|
15
13
|
import { loadTestingModule, type TestingModule } from "./testing-loader.js";
|
|
14
|
+
import { readWidgetProperties } from "./widget-properties.js";
|
|
16
15
|
|
|
17
16
|
type HandlerContext = {
|
|
18
17
|
app: Gtk.Application;
|
|
@@ -21,6 +20,11 @@ type HandlerContext = {
|
|
|
21
20
|
|
|
22
21
|
type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
|
|
23
22
|
type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
|
|
23
|
+
type WidgetParams = { widgetId?: string | undefined };
|
|
24
|
+
type TargetedHandler<Params> = (ctx: HandlerContext, target: WidgetTarget, params: Params) => Promise<unknown>;
|
|
25
|
+
type QueryParams = ServerRequestParams<"widget.query">;
|
|
26
|
+
type QueryBy = QueryParams["by"];
|
|
27
|
+
type QueryRunner = (testing: TestingModule, app: Gtk.Application, params: QueryParams) => Promise<Gtk.Widget[]>;
|
|
24
28
|
|
|
25
29
|
const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
|
|
26
30
|
"app.getWindows": validated(ServerRequestParamsSchemas["app.getWindows"], ({ registry }) =>
|
|
@@ -43,20 +47,22 @@ const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
|
|
|
43
47
|
};
|
|
44
48
|
}),
|
|
45
49
|
"widget.query": validated(ServerRequestParamsSchemas["widget.query"], handleQuery),
|
|
46
|
-
"widget.getProps":
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
"widget.getProps": targeted(
|
|
51
|
+
ServerRequestParamsSchemas["widget.getProps"],
|
|
52
|
+
({ registry }, { testing, widget }, params) =>
|
|
53
|
+
Promise.resolve({
|
|
54
|
+
...(params.properties !== undefined && {
|
|
55
|
+
properties: readWidgetProperties(widget, params.properties, registry),
|
|
56
|
+
}),
|
|
57
|
+
...serializeWidget(widget, (target) => registry.getOrCreateId(target), testing, params.maxDepth),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
"widget.click": targeted(ServerRequestParamsSchemas["widget.click"], async (_ctx, { testing, widget }) => {
|
|
53
61
|
await testing.userEvent.click(widget);
|
|
54
62
|
|
|
55
63
|
return { success: true };
|
|
56
64
|
}),
|
|
57
|
-
"widget.type":
|
|
58
|
-
const { testing, widget } = await widgetTarget(registry, params.widgetId);
|
|
59
|
-
|
|
65
|
+
"widget.type": targeted(ServerRequestParamsSchemas["widget.type"], async (_ctx, { testing, widget }, params) => {
|
|
60
66
|
if (params.clear) {
|
|
61
67
|
await testing.userEvent.clear(widget);
|
|
62
68
|
}
|
|
@@ -65,16 +71,43 @@ const HANDLERS: Record<ServerInitiatedMethod, ValidatedHandler> = {
|
|
|
65
71
|
|
|
66
72
|
return { success: true };
|
|
67
73
|
}),
|
|
68
|
-
"widget.fireEvent":
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
"widget.fireEvent": targeted(
|
|
75
|
+
ServerRequestParamsSchemas["widget.fireEvent"],
|
|
76
|
+
async (_ctx, { testing, widget }, params) => {
|
|
77
|
+
const signalArgs = (params.args ?? []).map((arg) => extractSignalArg(arg));
|
|
78
|
+
const isRealized = widget.getRealized();
|
|
79
|
+
const isMapped = widget.getMapped();
|
|
80
|
+
const isSensitive = widget.getSensitive();
|
|
81
|
+
await testing.fireEvent(widget, params.signal, ...signalArgs);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
signal: params.signal,
|
|
85
|
+
isRealized,
|
|
86
|
+
isMapped,
|
|
87
|
+
isSensitive,
|
|
88
|
+
note: emissionNote(isRealized, isMapped),
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
),
|
|
75
92
|
"widget.screenshot": validated(ServerRequestParamsSchemas["widget.screenshot"], handleScreenshot),
|
|
76
93
|
};
|
|
77
94
|
|
|
95
|
+
const QUERY_RUNNERS: Record<QueryBy, QueryRunner> = {
|
|
96
|
+
role: runRoleQuery,
|
|
97
|
+
text: runTextQuery,
|
|
98
|
+
name: runNameQuery,
|
|
99
|
+
labelText: runLabelTextQuery,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const SEARCHED_BY: Record<QueryBy, string> = {
|
|
103
|
+
role: "the accessible role, narrowed by any options given",
|
|
104
|
+
text: "the text the widget renders",
|
|
105
|
+
name:
|
|
106
|
+
"the widget name (gtk_widget_get_name, which reports the GType name such as \"GtkButton\" when no name " +
|
|
107
|
+
"was set), the accessible label, and the text the widget renders",
|
|
108
|
+
labelText: "the accessible label, the labelled-by relation, and the label whose mnemonic targets the widget",
|
|
109
|
+
};
|
|
110
|
+
|
|
78
111
|
function validated<Params>(
|
|
79
112
|
schema: ParamsSchema<Params>,
|
|
80
113
|
handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
|
|
@@ -90,6 +123,14 @@ function validated<Params>(
|
|
|
90
123
|
};
|
|
91
124
|
}
|
|
92
125
|
|
|
126
|
+
function targeted<Params extends WidgetParams>(
|
|
127
|
+
schema: ParamsSchema<Params>,
|
|
128
|
+
handler: TargetedHandler<Params>,
|
|
129
|
+
): ValidatedHandler {
|
|
130
|
+
return validated(schema, async (ctx, params) =>
|
|
131
|
+
handler(ctx, await widgetTarget(ctx.registry, params.widgetId), params));
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
const widgetTarget = async (registry: WidgetRegistry, widgetId: string | undefined): Promise<WidgetTarget> => ({
|
|
94
135
|
testing: await loadTestingModule(),
|
|
95
136
|
widget: requireWidget(registry, widgetId),
|
|
@@ -115,6 +156,21 @@ const extractSignalArg = (arg: unknown): unknown => {
|
|
|
115
156
|
return isTypedArg ? (arg as { value: unknown }).value : arg;
|
|
116
157
|
};
|
|
117
158
|
|
|
159
|
+
const emissionNote = (isRealized: boolean, isMapped: boolean): string => {
|
|
160
|
+
if (isRealized && isMapped) {
|
|
161
|
+
return (
|
|
162
|
+
"The signal was emitted. Default handlers that animate, such as a button activate, settle " +
|
|
163
|
+
"asynchronously, so read the widget again rather than assuming the effect has landed."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
"The signal was emitted on a widget that is not realized and mapped, so a default handler that " +
|
|
169
|
+
"requires a drawn widget does nothing. Present the containing window or open the containing " +
|
|
170
|
+
"popover first, then fire the signal again."
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
118
174
|
const resolveRole = (value: string | number): Gtk.AccessibleRole | undefined => {
|
|
119
175
|
if (typeof value === "number") {
|
|
120
176
|
return value;
|
|
@@ -133,42 +189,56 @@ const matchesOrEmpty = async (find: () => Promise<Gtk.Widget[]>): Promise<Gtk.Wi
|
|
|
133
189
|
}
|
|
134
190
|
};
|
|
135
191
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
params: ServerRequestParams<"widget.query">,
|
|
139
|
-
): Promise<unknown> {
|
|
140
|
-
const testing = await loadTestingModule();
|
|
141
|
-
let widgets: Gtk.Widget[] = [];
|
|
192
|
+
function runRoleQuery(testing: TestingModule, app: Gtk.Application, params: QueryParams): Promise<Gtk.Widget[]> {
|
|
193
|
+
const roleValue = resolveRole(params.value);
|
|
142
194
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
195
|
+
if (roleValue === undefined) {
|
|
196
|
+
throw invalidRequestError(
|
|
197
|
+
`Unknown accessible role "${String(params.value)}"; use the lowercase role shown in the ` +
|
|
198
|
+
"widget tree, e.g. \"button\", \"list\", \"list_item\", or \"checkbox\".",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
146
201
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
`Unknown accessible role "${String(params.value)}"; use the lowercase role shown in the ` +
|
|
150
|
-
"widget tree, e.g. \"button\", \"list\", \"list_item\", or \"checkbox\".",
|
|
151
|
-
);
|
|
152
|
-
}
|
|
202
|
+
return matchesOrEmpty(() => testing.findAllByRole(app, roleValue, params.options));
|
|
203
|
+
}
|
|
153
204
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
case "text": {
|
|
158
|
-
widgets = await matchesOrEmpty(() => testing.findAllByText(app, String(params.value), params.options));
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case "name": {
|
|
162
|
-
widgets = await matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options));
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
case "labelText": {
|
|
166
|
-
widgets = await matchesOrEmpty(() => testing.findAllByLabelText(app, String(params.value), params.options));
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
205
|
+
function runTextQuery(testing: TestingModule, app: Gtk.Application, params: QueryParams): Promise<Gtk.Widget[]> {
|
|
206
|
+
return matchesOrEmpty(() => testing.findAllByText(app, String(params.value), params.options));
|
|
207
|
+
}
|
|
170
208
|
|
|
171
|
-
|
|
209
|
+
function runLabelTextQuery(testing: TestingModule, app: Gtk.Application, params: QueryParams): Promise<Gtk.Widget[]> {
|
|
210
|
+
return matchesOrEmpty(() => testing.findAllByLabelText(app, String(params.value), params.options));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function runNameQuery(
|
|
214
|
+
testing: TestingModule,
|
|
215
|
+
app: Gtk.Application,
|
|
216
|
+
params: QueryParams,
|
|
217
|
+
): Promise<Gtk.Widget[]> {
|
|
218
|
+
const matches = await Promise.all([
|
|
219
|
+
matchesOrEmpty(() => testing.findAllByName(app, String(params.value), params.options)),
|
|
220
|
+
runLabelTextQuery(testing, app, params),
|
|
221
|
+
runTextQuery(testing, app, params),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
return [...new Set(matches.flat())];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const emptyQueryHint = (params: QueryParams): string =>
|
|
228
|
+
`Nothing matched by:"${params.by}" value:"${String(params.value)}", which compared ${SEARCHED_BY[params.by]}. ` +
|
|
229
|
+
"Call gtkx_get_widget_tree to see what is mounted; by:\"name\" is the widest match, and by:\"role\" accepts " +
|
|
230
|
+
"options.name to match the accessible name of a known role.";
|
|
231
|
+
|
|
232
|
+
async function handleQuery({ app, registry }: HandlerContext, params: QueryParams): Promise<unknown> {
|
|
233
|
+
const testing = await loadTestingModule();
|
|
234
|
+
const widgets = await QUERY_RUNNERS[params.by](testing, app, params);
|
|
235
|
+
const resolveId = (widget: Gtk.Widget): string => registry.getOrCreateId(widget);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
widgets: widgets.map((widget) => serializeWidget(widget, resolveId, testing, 0)),
|
|
239
|
+
searched: SEARCHED_BY[params.by],
|
|
240
|
+
...(widgets.length === 0 && { hint: emptyQueryHint(params) }),
|
|
241
|
+
};
|
|
172
242
|
}
|
|
173
243
|
|
|
174
244
|
const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
|
|
@@ -187,12 +257,9 @@ async function handleScreenshot(
|
|
|
187
257
|
): Promise<unknown> {
|
|
188
258
|
const testing = await loadTestingModule();
|
|
189
259
|
const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
|
|
190
|
-
const result = await testing.screenshot(target);
|
|
260
|
+
const result = await testing.screenshot(target, { path: params.path });
|
|
191
261
|
|
|
192
262
|
if (params.path) {
|
|
193
|
-
mkdirSync(dirname(params.path), { recursive: true });
|
|
194
|
-
writeFileSync(params.path, Buffer.from(result.data, "base64"));
|
|
195
|
-
|
|
196
263
|
return { data: result.data, mimeType: result.mimeType, savedPath: params.path };
|
|
197
264
|
}
|
|
198
265
|
|
|
@@ -210,4 +277,4 @@ const dispatch = async (method: string, params: unknown, ctx: HandlerContext): P
|
|
|
210
277
|
return HANDLERS[method](ctx, params);
|
|
211
278
|
};
|
|
212
279
|
|
|
213
|
-
export { dispatch
|
|
280
|
+
export { dispatch };
|
package/src/mcp/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ type McpClientController = {
|
|
|
7
7
|
|
|
8
8
|
const { start: startMcpClient, stop: stopMcpClient } = createMcpClientController();
|
|
9
9
|
|
|
10
|
+
/* eslint-disable-next-line unicorn/consistent-boolean-name -- the boolean reports whether connecting succeeded */
|
|
10
11
|
async function connectQuietly(client: McpClient): Promise<boolean> {
|
|
11
12
|
try {
|
|
12
13
|
await client.connect();
|