@rigkit/provider-vscode 0.1.8

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/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@rigkit/provider-vscode",
3
+ "version": "0.1.8",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/freestyle-sh/rigkit.git",
8
+ "directory": "packages/provider-vscode"
9
+ },
10
+ "main": "./dist/extension.cjs",
11
+ "engines": {
12
+ "vscode": "^1.95.0"
13
+ },
14
+ "activationEvents": [
15
+ "workspaceContains:rig.config.ts",
16
+ "onView:rigkitOperations",
17
+ "onView:rigkitWorkspaces",
18
+ "onCommand:rigkit.refresh",
19
+ "onCommand:rigkit.runOperation",
20
+ "onCommand:rigkit.openWorkspace"
21
+ ],
22
+ "contributes": {
23
+ "commands": [
24
+ {
25
+ "command": "rigkit.refresh",
26
+ "title": "rigkit: Refresh"
27
+ },
28
+ {
29
+ "command": "rigkit.runOperation",
30
+ "title": "rigkit: Run Operation"
31
+ },
32
+ {
33
+ "command": "rigkit.openWorkspace",
34
+ "title": "rigkit: Run Workspace Action"
35
+ }
36
+ ],
37
+ "views": {
38
+ "explorer": [
39
+ {
40
+ "id": "rigkitOperations",
41
+ "name": "rigkit Operations"
42
+ },
43
+ {
44
+ "id": "rigkitWorkspaces",
45
+ "name": "rigkit Workspaces"
46
+ }
47
+ ]
48
+ }
49
+ },
50
+ "exports": {
51
+ ".": "./dist/extension.cjs",
52
+ "./package.json": "./package.json"
53
+ },
54
+ "files": [
55
+ "dist",
56
+ "src",
57
+ "README.md"
58
+ ],
59
+ "dependencies": {
60
+ "@rigkit/runtime-client": "0.1.8"
61
+ },
62
+ "devDependencies": {
63
+ "@types/bun": "latest",
64
+ "typescript": "latest"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "scripts": {
70
+ "build": "bun build src/extension.ts --target=node --format=cjs --external vscode --outfile dist/extension.cjs",
71
+ "typecheck": "tsc --noEmit",
72
+ "test": "bun test"
73
+ }
74
+ }
@@ -0,0 +1,424 @@
1
+ import * as vscode from "vscode";
2
+ import {
3
+ getOrStartRuntime,
4
+ type RuntimeClient,
5
+ type RuntimeControlOperation,
6
+ type RuntimeControlWorkspace,
7
+ } from "@rigkit/runtime-client";
8
+ import { collectOperationInput } from "./input.ts";
9
+ import { resolveRigkitProject, type RigkitProject } from "./project.ts";
10
+ import { RIGKIT_PROVIDER_VSCODE_VERSION } from "./version.ts";
11
+
12
+ const VSCODE_HOST_METHODS: Array<{ id: string; modes?: string[] }> = [
13
+ { id: "message.show" },
14
+ { id: "prompt.text" },
15
+ { id: "prompt.confirm" },
16
+ { id: "prompt.select" },
17
+ { id: "open.external" },
18
+ ];
19
+
20
+ type HostRequestMessage = Record<string, unknown> & {
21
+ type: "host.request";
22
+ id: string;
23
+ method: string;
24
+ params?: unknown;
25
+ };
26
+
27
+ type HostCapabilityRequestMessage = Record<string, unknown> & {
28
+ type: "host.capability.request";
29
+ id: string;
30
+ capability: string;
31
+ params?: unknown;
32
+ };
33
+
34
+ export function activate(context: vscode.ExtensionContext): void {
35
+ const host = new RigkitVsCodeHost();
36
+ const operations = new OperationsProvider(host);
37
+ const workspaces = new WorkspacesProvider(host);
38
+
39
+ context.subscriptions.push(
40
+ vscode.window.registerTreeDataProvider("rigkitOperations", operations),
41
+ vscode.window.registerTreeDataProvider("rigkitWorkspaces", workspaces),
42
+ vscode.commands.registerCommand("rigkit.refresh", async () => {
43
+ host.clearCache();
44
+ operations.refresh();
45
+ workspaces.refresh();
46
+ }),
47
+ vscode.commands.registerCommand("rigkit.runOperation", async (operation?: RuntimeControlOperation) => {
48
+ await host.runOperation(operation);
49
+ operations.refresh();
50
+ workspaces.refresh();
51
+ }),
52
+ vscode.commands.registerCommand("rigkit.openWorkspace", async (workspace?: RuntimeControlWorkspace) => {
53
+ await host.runWorkspaceAction(workspace);
54
+ workspaces.refresh();
55
+ }),
56
+ );
57
+ }
58
+
59
+ export function deactivate(): void {}
60
+
61
+ class RigkitVsCodeHost {
62
+ private output = vscode.window.createOutputChannel("rigkit");
63
+ private runtimePromise: Promise<RuntimeClient> | undefined;
64
+
65
+ clearCache(): void {
66
+ this.runtimePromise = undefined;
67
+ }
68
+
69
+ async listOperations(): Promise<RuntimeControlOperation[]> {
70
+ const runtime = await this.runtime();
71
+ return [...(await runtime.control.operations()).operations];
72
+ }
73
+
74
+ async listWorkspaces(): Promise<RuntimeControlWorkspace[]> {
75
+ const runtime = await this.runtime();
76
+ return [...(await runtime.control.workspaces()).workspaces];
77
+ }
78
+
79
+ async runOperation(operation?: RuntimeControlOperation, presetWorkspace?: RuntimeControlWorkspace): Promise<void> {
80
+ const runtime = await this.runtime();
81
+ const operations = await this.listOperations();
82
+ const selected = operation ?? await pickOperation(operations);
83
+ if (!selected) return;
84
+
85
+ const unsupported = unsupportedRequirements(selected);
86
+ if (unsupported) {
87
+ await vscode.window.showErrorMessage(unsupported);
88
+ return;
89
+ }
90
+
91
+ const workspaces = await this.listWorkspaces();
92
+ const input = await collectOperationInput(selected, workspaces, operationPrompt(presetWorkspace));
93
+ if (!input) return;
94
+
95
+ await vscode.window.withProgress({
96
+ location: vscode.ProgressLocation.Notification,
97
+ title: `rigkit ${selected.title}`,
98
+ }, async () => {
99
+ const started = await runtime.control.startRun({ operation: selected.id, input });
100
+ await runtime.runSession(started.runId, {
101
+ hello: {
102
+ type: "hello",
103
+ transportVersion: 1,
104
+ host: {
105
+ name: "provider-vscode",
106
+ version: RIGKIT_PROVIDER_VSCODE_VERSION,
107
+ },
108
+ hostMethods: [...VSCODE_HOST_METHODS],
109
+ hostCapabilities: [],
110
+ },
111
+ onMessage: async (message, session) => {
112
+ if (isHostRequestMessage(message)) {
113
+ await answerHostRequest(message, session);
114
+ return;
115
+ }
116
+ if (isHostCapabilityRequestMessage(message)) {
117
+ session.send({
118
+ type: "response",
119
+ id: message.id,
120
+ error: {
121
+ code: "UNSUPPORTED_CAPABILITY",
122
+ message: `VS Code host does not support host capability ${message.capability}`,
123
+ },
124
+ });
125
+ return;
126
+ }
127
+ this.logRunMessage(message);
128
+ },
129
+ });
130
+
131
+ const completed = await runtime.control.run(started.runId);
132
+ if (completed.status === "failed") {
133
+ throw new Error(completed.error?.message ?? `Rigkit operation ${selected.id} failed`);
134
+ }
135
+ });
136
+ }
137
+
138
+ async runWorkspaceAction(workspace?: RuntimeControlWorkspace): Promise<void> {
139
+ const operations = await this.listOperations();
140
+ const workspaces = await this.listWorkspaces();
141
+ const selectedWorkspace = workspace ?? await pickWorkspace("Workspace", workspaces);
142
+ if (!selectedWorkspace) return;
143
+
144
+ const workspaceActions = operations.filter((operation) => operation.kind === "workspace-action");
145
+ const selectedOperation = await pickOperation(workspaceActions, `Run action for ${selectedWorkspace.name}`);
146
+ if (!selectedOperation) return;
147
+
148
+ await this.runOperation(selectedOperation, selectedWorkspace);
149
+ }
150
+
151
+ private runtime(): Promise<RuntimeClient> {
152
+ this.runtimePromise ??= this.resolveRuntime();
153
+ return this.runtimePromise;
154
+ }
155
+
156
+ private async resolveRuntime(): Promise<RuntimeClient> {
157
+ const project = resolveWorkspaceProject();
158
+ const config = vscode.workspace.getConfiguration("rigkit");
159
+ const statePath = config.get<string | undefined>("statePath", undefined);
160
+ return await getOrStartRuntime({
161
+ projectDir: project.projectDir,
162
+ configPath: project.configPath,
163
+ ...(statePath ? { statePath } : {}),
164
+ });
165
+ }
166
+
167
+ private logRunMessage(message: unknown): void {
168
+ if (!isRecord(message)) return;
169
+ if (message.type === "hello.ack" || message.type === "heartbeat.ack") return;
170
+ this.output.appendLine(JSON.stringify(message));
171
+ }
172
+ }
173
+
174
+ class OperationsProvider implements vscode.TreeDataProvider<RuntimeControlOperation> {
175
+ private readonly changed = new vscode.EventEmitter<RuntimeControlOperation | undefined>();
176
+ readonly onDidChangeTreeData = this.changed.event;
177
+
178
+ constructor(private readonly host: RigkitVsCodeHost) {}
179
+
180
+ refresh(): void {
181
+ this.changed.fire(undefined);
182
+ }
183
+
184
+ getTreeItem(operation: RuntimeControlOperation): vscode.TreeItem {
185
+ const item = new vscode.TreeItem(operation.title || operation.id, vscode.TreeItemCollapsibleState.None);
186
+ item.description = operation.id;
187
+ item.tooltip = operation.description || operation.id;
188
+ item.iconPath = new vscode.ThemeIcon(operation.kind === "workspace-action" ? "tools" : "play");
189
+ item.contextValue = "rigkitOperation";
190
+ item.command = {
191
+ command: "rigkit.runOperation",
192
+ title: "Run Operation",
193
+ arguments: [operation],
194
+ };
195
+ return item;
196
+ }
197
+
198
+ async getChildren(): Promise<RuntimeControlOperation[]> {
199
+ return await this.host.listOperations();
200
+ }
201
+ }
202
+
203
+ class WorkspacesProvider implements vscode.TreeDataProvider<RuntimeControlWorkspace> {
204
+ private readonly changed = new vscode.EventEmitter<RuntimeControlWorkspace | undefined>();
205
+ readonly onDidChangeTreeData = this.changed.event;
206
+
207
+ constructor(private readonly host: RigkitVsCodeHost) {}
208
+
209
+ refresh(): void {
210
+ this.changed.fire(undefined);
211
+ }
212
+
213
+ getTreeItem(workspace: RuntimeControlWorkspace): vscode.TreeItem {
214
+ const item = new vscode.TreeItem(workspace.name, vscode.TreeItemCollapsibleState.None);
215
+ item.description = workspace.workflow;
216
+ item.tooltip = workspace.resourceId || workspace.name;
217
+ item.iconPath = new vscode.ThemeIcon("server");
218
+ item.contextValue = "rigkitWorkspace";
219
+ item.command = {
220
+ command: "rigkit.openWorkspace",
221
+ title: "Run Workspace Action",
222
+ arguments: [workspace],
223
+ };
224
+ return item;
225
+ }
226
+
227
+ async getChildren(): Promise<RuntimeControlWorkspace[]> {
228
+ return await this.host.listWorkspaces();
229
+ }
230
+ }
231
+
232
+ const vscodePrompt = {
233
+ async inputText(input: { name: string; description?: string; defaultValue?: string }) {
234
+ return await vscode.window.showInputBox({
235
+ title: input.name,
236
+ prompt: input.description,
237
+ value: input.defaultValue,
238
+ ignoreFocusOut: true,
239
+ });
240
+ },
241
+ async confirm(input: { name: string; description?: string; defaultValue?: boolean }) {
242
+ const answer = await vscode.window.showQuickPick([
243
+ { label: "Yes" },
244
+ { label: "No" },
245
+ ], {
246
+ title: input.name,
247
+ placeHolder: input.description,
248
+ ignoreFocusOut: true,
249
+ });
250
+ if (!answer) return undefined;
251
+ return answer.label === "Yes";
252
+ },
253
+ async pickWorkspace(input: { name: string; description?: string; workspaces: RuntimeControlWorkspace[] }) {
254
+ return await pickWorkspace(input.description ?? input.name, input.workspaces);
255
+ },
256
+ };
257
+
258
+ function operationPrompt(presetWorkspace: RuntimeControlWorkspace | undefined) {
259
+ if (!presetWorkspace) return vscodePrompt;
260
+ return {
261
+ ...vscodePrompt,
262
+ async pickWorkspace() {
263
+ return presetWorkspace;
264
+ },
265
+ };
266
+ }
267
+
268
+ async function pickOperation(
269
+ operations: RuntimeControlOperation[],
270
+ title = "Rigkit operation",
271
+ ): Promise<RuntimeControlOperation | undefined> {
272
+ const items = operations.map((operation) => ({
273
+ label: operation.title || operation.id,
274
+ description: operation.id,
275
+ detail: operation.description,
276
+ operation,
277
+ }));
278
+ const picked = await vscode.window.showQuickPick(items, { title, ignoreFocusOut: true });
279
+ return picked?.operation;
280
+ }
281
+
282
+ async function pickWorkspace(
283
+ title: string,
284
+ workspaces: RuntimeControlWorkspace[],
285
+ ): Promise<RuntimeControlWorkspace | undefined> {
286
+ const items = workspaces.map((workspace) => ({
287
+ label: workspace.name,
288
+ description: workspace.workflow,
289
+ detail: workspace.resourceId,
290
+ workspace,
291
+ }));
292
+ const picked = await vscode.window.showQuickPick(items, { title, ignoreFocusOut: true });
293
+ return picked?.workspace;
294
+ }
295
+
296
+ function resolveWorkspaceProject(): RigkitProject {
297
+ const folder = vscode.workspace.workspaceFolders?.[0];
298
+ if (!folder) throw new Error("Open a workspace folder before using Rigkit.");
299
+ return resolveRigkitProject(folder.uri.fsPath);
300
+ }
301
+
302
+ async function answerHostRequest(
303
+ message: HostRequestMessage,
304
+ session: { send(message: unknown): void },
305
+ ): Promise<void> {
306
+ try {
307
+ session.send({
308
+ type: "response",
309
+ id: message.id,
310
+ result: await handleHostRequest(message.method, message.params),
311
+ });
312
+ } catch (error) {
313
+ session.send({
314
+ type: "response",
315
+ id: message.id,
316
+ error: {
317
+ code: "HOST_REQUEST_FAILED",
318
+ message: error instanceof Error ? error.message : String(error),
319
+ },
320
+ });
321
+ }
322
+ }
323
+
324
+ async function handleHostRequest(method: string, params: unknown): Promise<unknown> {
325
+ switch (method) {
326
+ case "message.show":
327
+ await showMessage(params);
328
+ return null;
329
+ case "prompt.text":
330
+ return await vscodePrompt.inputText({
331
+ name: stringField(params, "message") ?? "Input",
332
+ defaultValue: stringField(params, "defaultValue"),
333
+ });
334
+ case "prompt.confirm":
335
+ return await vscodePrompt.confirm({
336
+ name: stringField(params, "message") ?? "Confirm",
337
+ defaultValue: booleanField(params, "defaultValue"),
338
+ });
339
+ case "prompt.select":
340
+ return await promptSelect(params);
341
+ case "open.external":
342
+ return await openExternal(params);
343
+ case "host.command.run":
344
+ throw new Error("VS Code host does not support host.command.run");
345
+ default:
346
+ throw new Error(`Unsupported host method ${method}`);
347
+ }
348
+ }
349
+
350
+ async function showMessage(params: unknown): Promise<void> {
351
+ const message = stringField(params, "message") ?? "";
352
+ const level = stringField(params, "level") ?? "info";
353
+ if (level === "error") await vscode.window.showErrorMessage(message);
354
+ else if (level === "warning" || level === "warn") await vscode.window.showWarningMessage(message);
355
+ else await vscode.window.showInformationMessage(message);
356
+ }
357
+
358
+ async function promptSelect(params: unknown): Promise<string | undefined> {
359
+ const options = isRecord(params) && Array.isArray(params.options)
360
+ ? params.options.filter(isRecord).flatMap((item) => {
361
+ const value = typeof item.value === "string" ? item.value : undefined;
362
+ if (!value) return [];
363
+ return [{
364
+ label: typeof item.label === "string" ? item.label : value,
365
+ description: typeof item.description === "string" ? item.description : undefined,
366
+ value,
367
+ }];
368
+ })
369
+ : [];
370
+ const picked = await vscode.window.showQuickPick(options, {
371
+ title: stringField(params, "message") ?? "Choose",
372
+ ignoreFocusOut: true,
373
+ });
374
+ return picked?.value;
375
+ }
376
+
377
+ async function openExternal(params: unknown): Promise<null> {
378
+ const target = stringField(params, "target");
379
+ if (!target) throw new Error("open.external requires target");
380
+ await vscode.env.openExternal(vscode.Uri.parse(target));
381
+ return null;
382
+ }
383
+
384
+ function unsupportedRequirements(operation: RuntimeControlOperation): string | undefined {
385
+ const unsupportedCapability = operation.requiredHostCapabilities?.[0];
386
+ if (unsupportedCapability) {
387
+ return `Operation "${operation.id}" requires host capability "${unsupportedCapability.id}". VS Code does not support that capability.`;
388
+ }
389
+ const unsupportedMethod = operation.requiredHostMethods?.find((method) =>
390
+ !VSCODE_HOST_METHODS.some((supported) =>
391
+ supported.id === method.id && (!method.modes?.length || method.modes.every((mode) => supported.modes?.includes(mode)))
392
+ )
393
+ );
394
+ if (unsupportedMethod) {
395
+ return `Operation "${operation.id}" requires host method "${unsupportedMethod.id}".`;
396
+ }
397
+ return undefined;
398
+ }
399
+
400
+ function isHostRequestMessage(value: unknown): value is HostRequestMessage {
401
+ return isRecord(value) &&
402
+ value.type === "host.request" &&
403
+ typeof value.id === "string" &&
404
+ typeof value.method === "string";
405
+ }
406
+
407
+ function isHostCapabilityRequestMessage(value: unknown): value is HostCapabilityRequestMessage {
408
+ return isRecord(value) &&
409
+ value.type === "host.capability.request" &&
410
+ typeof value.id === "string" &&
411
+ typeof value.capability === "string";
412
+ }
413
+
414
+ function stringField(value: unknown, key: string): string | undefined {
415
+ return isRecord(value) && typeof value[key] === "string" ? value[key] : undefined;
416
+ }
417
+
418
+ function booleanField(value: unknown, key: string): boolean | undefined {
419
+ return isRecord(value) && typeof value[key] === "boolean" ? value[key] : undefined;
420
+ }
421
+
422
+ function isRecord(value: unknown): value is Record<string, unknown> {
423
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
424
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { RuntimeControlOperation, RuntimeControlWorkspace } from "@rigkit/runtime-client";
3
+ import { collectOperationInput } from "./input.ts";
4
+
5
+ describe("VS Code operation input collection", () => {
6
+ test("collects workspace and scalar fields from runtime schema metadata", async () => {
7
+ const workspace = workspaceRecord("demo");
8
+ const operation: RuntimeControlOperation = {
9
+ id: "fork",
10
+ kind: "workspace-action",
11
+ source: "config",
12
+ title: "Fork",
13
+ description: "",
14
+ cli: {
15
+ positionals: [
16
+ { name: "from", index: 0 },
17
+ { name: "name", index: 1 },
18
+ ],
19
+ },
20
+ inputSchema: {
21
+ type: "object",
22
+ required: ["from", "name"],
23
+ properties: {
24
+ from: {
25
+ type: "string",
26
+ description: "Workspace to fork",
27
+ "x-rigkit-input": { kind: "workspace" },
28
+ },
29
+ name: {
30
+ type: "string",
31
+ description: "New workspace name",
32
+ },
33
+ },
34
+ },
35
+ };
36
+
37
+ const input = await collectOperationInput(operation, [workspace], {
38
+ inputText: async () => "copy",
39
+ confirm: async () => false,
40
+ pickWorkspace: async () => workspace,
41
+ });
42
+
43
+ expect(input).toEqual({ from: "demo", name: "copy" });
44
+ });
45
+
46
+ test("returns undefined when a required prompt is cancelled", async () => {
47
+ const operation: RuntimeControlOperation = {
48
+ id: "create",
49
+ kind: "command",
50
+ source: "core",
51
+ title: "Create",
52
+ description: "",
53
+ inputSchema: {
54
+ type: "object",
55
+ required: ["name"],
56
+ properties: {
57
+ name: { type: "string" },
58
+ },
59
+ },
60
+ };
61
+
62
+ await expect(collectOperationInput(operation, [], {
63
+ inputText: async () => undefined,
64
+ confirm: async () => undefined,
65
+ pickWorkspace: async () => undefined,
66
+ })).resolves.toBeUndefined();
67
+ });
68
+ });
69
+
70
+ function workspaceRecord(name: string): RuntimeControlWorkspace {
71
+ return {
72
+ id: `ws-${name}`,
73
+ name,
74
+ providerId: "test",
75
+ workflow: "test",
76
+ resourceId: `resource-${name}`,
77
+ sourceRef: null,
78
+ context: {},
79
+ metadata: {},
80
+ data: {},
81
+ createdAt: "2026-05-10T00:00:00.000Z",
82
+ updatedAt: "2026-05-10T00:00:00.000Z",
83
+ };
84
+ }
package/src/input.ts ADDED
@@ -0,0 +1,111 @@
1
+ import type {
2
+ RuntimeControlOperation,
3
+ RuntimeControlWorkspace,
4
+ } from "@rigkit/runtime-client";
5
+
6
+ export type OperationInputPrompt = {
7
+ inputText(input: { name: string; description?: string; defaultValue?: string }): Promise<string | undefined>;
8
+ confirm(input: { name: string; description?: string; defaultValue?: boolean }): Promise<boolean | undefined>;
9
+ pickWorkspace(input: { name: string; description?: string; workspaces: RuntimeControlWorkspace[] }): Promise<RuntimeControlWorkspace | undefined>;
10
+ };
11
+
12
+ type JsonSchemaRecord = Record<string, unknown> & {
13
+ required?: string[];
14
+ properties?: Record<string, JsonSchemaProperty>;
15
+ };
16
+
17
+ type JsonSchemaProperty = Record<string, unknown> & {
18
+ type?: string;
19
+ description?: string;
20
+ default?: unknown;
21
+ };
22
+
23
+ export async function collectOperationInput(
24
+ operation: RuntimeControlOperation,
25
+ workspaces: RuntimeControlWorkspace[],
26
+ prompt: OperationInputPrompt,
27
+ ): Promise<Record<string, unknown> | undefined> {
28
+ const schema = asInputSchema(operation.inputSchema);
29
+ const properties = schema.properties ?? {};
30
+ const required = new Set(schema.required ?? []);
31
+ const input: Record<string, unknown> = {};
32
+
33
+ for (const [name, property] of orderedProperties(operation, properties)) {
34
+ if (isWorkspaceInput(property)) {
35
+ const workspace = await prompt.pickWorkspace({
36
+ name,
37
+ description: property.description,
38
+ workspaces,
39
+ });
40
+ if (!workspace && required.has(name)) return undefined;
41
+ if (workspace) input[name] = workspace.name;
42
+ continue;
43
+ }
44
+
45
+ if (property.type === "boolean") {
46
+ const confirmed = await prompt.confirm({
47
+ name,
48
+ description: property.description,
49
+ defaultValue: typeof property.default === "boolean" ? property.default : false,
50
+ });
51
+ if (confirmed === undefined && required.has(name)) return undefined;
52
+ if (confirmed !== undefined) input[name] = confirmed;
53
+ continue;
54
+ }
55
+
56
+ const value = await prompt.inputText({
57
+ name,
58
+ description: property.description,
59
+ defaultValue: typeof property.default === "string" ? property.default : undefined,
60
+ });
61
+ if ((value === undefined || value === "") && required.has(name)) return undefined;
62
+ if (value !== undefined && value !== "") input[name] = coerceTextInput(value, property.type);
63
+ }
64
+
65
+ return input;
66
+ }
67
+
68
+ function orderedProperties(
69
+ operation: RuntimeControlOperation,
70
+ properties: Record<string, JsonSchemaProperty>,
71
+ ): Array<[string, JsonSchemaProperty]> {
72
+ const cliOrder = [
73
+ ...[...(operation.cli?.positionals ?? [])].sort((a, b) => a.index - b.index).map((item) => item.name),
74
+ ...(operation.cli?.options ?? []).map((item) => item.name),
75
+ ];
76
+ const seen = new Set<string>();
77
+ const ordered: Array<[string, JsonSchemaProperty]> = [];
78
+ for (const name of cliOrder) {
79
+ const property = properties[name];
80
+ if (!property || seen.has(name)) continue;
81
+ seen.add(name);
82
+ ordered.push([name, property]);
83
+ }
84
+ for (const entry of Object.entries(properties)) {
85
+ if (seen.has(entry[0])) continue;
86
+ ordered.push(entry);
87
+ }
88
+ return ordered;
89
+ }
90
+
91
+ function asInputSchema(value: unknown): JsonSchemaRecord {
92
+ return isRecord(value) ? value as JsonSchemaRecord : {};
93
+ }
94
+
95
+ function isWorkspaceInput(property: JsonSchemaProperty): boolean {
96
+ const rigkitInput = property["x-rigkit-input"];
97
+ return isRecord(rigkitInput) && rigkitInput.kind === "workspace";
98
+ }
99
+
100
+ function coerceTextInput(value: string, type: string | undefined): unknown {
101
+ if (type === "number") {
102
+ const parsed = Number(value);
103
+ if (!Number.isFinite(parsed)) throw new Error(`${value} is not a number`);
104
+ return parsed;
105
+ }
106
+ return value;
107
+ }
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
111
+ }