@rigkit/cli 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.
@@ -0,0 +1,204 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { projectIdFor, runtimePaths, SUPPORTED_RUNTIME_API_VERSION } from "@rigkit/runtime-client";
6
+ import { completeRig, formatCompletionItems, renderCompletionScript } from "./completion.ts";
7
+
8
+ describe("CLI completion", () => {
9
+ test("completes ssh workspace targets from the runtime", async () => {
10
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
11
+ await withWorkspaceRuntime({ projectDir }, async () => {
12
+ const items = await completeRig({
13
+ cwd: projectDir,
14
+ words: ["rig", "ssh", ""],
15
+ currentIndex: 2,
16
+ });
17
+
18
+ expect(items.map((item) => item.value)).toEqual(["api", "web"]);
19
+ expect(items[0]?.description).toBe("vm-api");
20
+ });
21
+ });
22
+
23
+ test("completes ssh resource ids when the current token starts like a resource id", async () => {
24
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
25
+ await withWorkspaceRuntime({ projectDir }, async () => {
26
+ const items = await completeRig({
27
+ cwd: projectDir,
28
+ words: ["rig", "ssh", "vm-"],
29
+ currentIndex: 2,
30
+ });
31
+
32
+ expect(items.map((item) => item.value)).toEqual(["vm-api", "vm-web"]);
33
+ });
34
+ });
35
+
36
+ test("respects -C when completing workspace targets", async () => {
37
+ const parentDir = mkdtempSync(join(tmpdir(), "rigkit-completion-parent-"));
38
+ const projectDir = join(parentDir, "project");
39
+ await withWorkspaceRuntime({ projectDir, cleanupDir: parentDir }, async () => {
40
+ const items = await completeRig({
41
+ cwd: parentDir,
42
+ words: ["rig", "-C", "project", "ssh", ""],
43
+ currentIndex: 4,
44
+ });
45
+
46
+ expect(items.map((item) => item.value)).toEqual(["api", "web"]);
47
+ });
48
+ });
49
+
50
+ test("formats shell completion items", () => {
51
+ const items = [{ value: "api", description: "vm-api" }];
52
+
53
+ expect(formatCompletionItems(items, "bash")).toBe("api");
54
+ expect(formatCompletionItems(items, "zsh")).toBe("api\tvm-api");
55
+ expect(renderCompletionScript("zsh")).toContain("rig __complete");
56
+ });
57
+
58
+ test("completes ls targets", async () => {
59
+ const items = await completeRig({
60
+ cwd: process.cwd(),
61
+ words: ["rig", "ls", ""],
62
+ currentIndex: 2,
63
+ });
64
+
65
+ expect(items.map((item) => item.value)).toEqual(["workspaces", "snapshots", "config", "--json"]);
66
+ });
67
+ });
68
+
69
+ async function withWorkspaceRuntime(
70
+ input: { projectDir: string; cleanupDir?: string },
71
+ run: () => Promise<void>,
72
+ ): Promise<void> {
73
+ const previousHome = process.env.RIGKIT_HOME;
74
+ const rigkitHome = mkdtempSync(join(tmpdir(), "rigkit-home-"));
75
+ const token = "test-token";
76
+ const configPath = join(input.projectDir, "rig.config.ts");
77
+ mkdirSync(input.projectDir, { recursive: true });
78
+ writeFileSync(configPath, "export default {}\n");
79
+ const projectId = projectIdFor({ projectDir: input.projectDir, configPath });
80
+ const paths = runtimePaths(projectId, rigkitHome);
81
+ mkdirSync(paths.root, { recursive: true });
82
+ writeFileSync(paths.tokenPath, `${token}\n`);
83
+
84
+ const server = Bun.serve({
85
+ hostname: "127.0.0.1",
86
+ port: 0,
87
+ fetch(request) {
88
+ if (request.headers.get("authorization") !== `Bearer ${token}`) {
89
+ return runtimeJson({ error: { message: "Unauthorized" } }, { status: 401 });
90
+ }
91
+
92
+ const { pathname } = new URL(request.url);
93
+ if (pathname === "/health") {
94
+ return runtimeJson({
95
+ ok: true,
96
+ projectId,
97
+ projectDir: input.projectDir,
98
+ configPath,
99
+ engineVersion: "engine-test",
100
+ runtimeVersion: "runtime-test",
101
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
102
+ });
103
+ }
104
+ if (pathname === "/workspaces") {
105
+ const now = new Date(0).toISOString();
106
+ return runtimeJson({
107
+ workspaces: [
108
+ {
109
+ id: "workspace-api",
110
+ name: "api",
111
+ providerId: "freestyle",
112
+ workflow: "smoke",
113
+ resourceId: "vm-api",
114
+ sourceRef: null,
115
+ context: {},
116
+ metadata: {},
117
+ data: {},
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ },
121
+ {
122
+ id: "workspace-web",
123
+ name: "web",
124
+ providerId: "freestyle",
125
+ workflow: "smoke",
126
+ resourceId: "vm-web",
127
+ sourceRef: null,
128
+ context: {},
129
+ metadata: {},
130
+ data: {},
131
+ createdAt: now,
132
+ updatedAt: now,
133
+ },
134
+ ],
135
+ });
136
+ }
137
+ if (pathname === "/operations") {
138
+ return runtimeJson({
139
+ hostMethods: {
140
+ known: [],
141
+ requiredByOperations: {},
142
+ },
143
+ hostCapabilities: {
144
+ optional: [],
145
+ requiredByOperations: {},
146
+ },
147
+ operations: [
148
+ {
149
+ id: "ssh",
150
+ kind: "command",
151
+ source: "core",
152
+ title: "SSH",
153
+ description: "open SSH",
154
+ cli: {
155
+ positionals: [{ name: "workspaceOrVmId", index: 0 }],
156
+ options: [{ name: "print", flag: "--print", type: "boolean", runtime: false }],
157
+ },
158
+ inputSchema: {
159
+ type: "object",
160
+ additionalProperties: false,
161
+ properties: {
162
+ workspaceOrVmId: { type: "string" },
163
+ },
164
+ },
165
+ },
166
+ ],
167
+ });
168
+ }
169
+ return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
170
+ },
171
+ });
172
+
173
+ writeFileSync(
174
+ paths.handlePath,
175
+ `${JSON.stringify({
176
+ projectId,
177
+ projectDir: input.projectDir,
178
+ configPath,
179
+ pid: process.pid,
180
+ url: `http://127.0.0.1:${server.port}`,
181
+ tokenPath: paths.tokenPath,
182
+ }, null, 2)}\n`,
183
+ );
184
+
185
+ process.env.RIGKIT_HOME = rigkitHome;
186
+ try {
187
+ await run();
188
+ } finally {
189
+ if (previousHome === undefined) {
190
+ delete process.env.RIGKIT_HOME;
191
+ } else {
192
+ process.env.RIGKIT_HOME = previousHome;
193
+ }
194
+ server.stop(true);
195
+ rmSync(rigkitHome, { recursive: true, force: true });
196
+ rmSync(input.cleanupDir ?? input.projectDir, { recursive: true, force: true });
197
+ }
198
+ }
199
+
200
+ function runtimeJson(body: unknown, init: ResponseInit = {}): Response {
201
+ const headers = new Headers(init.headers);
202
+ headers.set("x-rigkit-api-version", String(SUPPORTED_RUNTIME_API_VERSION));
203
+ return Response.json(body, { ...init, headers });
204
+ }
@@ -0,0 +1,444 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { getOrStartRuntime } from "@rigkit/runtime-client";
3
+
4
+ export type CompletionShell = "bash" | "fish" | "zsh";
5
+
6
+ export type CompletionItem = {
7
+ value: string;
8
+ description?: string;
9
+ };
10
+
11
+ type CompleteRigInput = {
12
+ words: string[];
13
+ currentIndex?: number;
14
+ cwd?: string;
15
+ };
16
+
17
+ const COMMANDS: CompletionItem[] = [
18
+ { value: "help", description: "show CLI help" },
19
+ { value: "init", description: "initialize a Rigkit project" },
20
+ { value: "ls", description: "list project workspaces" },
21
+ { value: "projects", description: "discover Rigkit projects" },
22
+ { value: "doctor", description: "show runtime diagnostics" },
23
+ { value: "version", description: "show CLI version" },
24
+ { value: "completion", description: "generate shell completion" },
25
+ ];
26
+
27
+ const COMMAND_ALIASES = new Map<string, string>();
28
+
29
+ const GLOBAL_OPTIONS: CompletionItem[] = [
30
+ { value: "-C", description: "project directory" },
31
+ { value: "--project", description: "project directory" },
32
+ { value: "--config", description: "exact config file" },
33
+ { value: "--state", description: "local state database path" },
34
+ { value: "--json", description: "print JSON" },
35
+ { value: "--help", description: "show help" },
36
+ { value: "--version", description: "show version" },
37
+ ];
38
+
39
+ const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
40
+ init: [
41
+ { value: "--name", description: "project and workflow name" },
42
+ { value: "--api-key", description: "Freestyle API key" },
43
+ { value: "--package-manager", description: "npm, bun, pnpm, or skip" },
44
+ { value: "--force", description: "overwrite existing config" },
45
+ { value: "--json", description: "print JSON" },
46
+ ],
47
+ operation: [
48
+ { value: "--all", description: "run against every discovered project" },
49
+ { value: "--discover", description: "discover projects below the selected directory" },
50
+ { value: "--json", description: "print JSON" },
51
+ ],
52
+ ls: [
53
+ { value: "workspaces", description: "list workspaces" },
54
+ { value: "snapshots", description: "list snapshots" },
55
+ { value: "config", description: "show project config" },
56
+ { value: "--json", description: "print JSON" },
57
+ ],
58
+ projects: [
59
+ { value: "--json", description: "print JSON" },
60
+ ],
61
+ completion: [
62
+ { value: "bash", description: "Bash completion" },
63
+ { value: "fish", description: "fish completion" },
64
+ { value: "zsh", description: "zsh completion" },
65
+ ],
66
+ };
67
+
68
+ const OPTIONS_WITH_VALUES = new Set([
69
+ "-C",
70
+ "--project",
71
+ "--config",
72
+ "--state",
73
+ "--name",
74
+ "--api-key",
75
+ "--package-manager",
76
+ ]);
77
+
78
+ type RuntimeOperationManifest = {
79
+ operations: RuntimeOperationDefinition[];
80
+ };
81
+
82
+ type RuntimeOperationDefinition = {
83
+ id: string;
84
+ aliases?: string[];
85
+ description?: string;
86
+ cli?: {
87
+ positionals?: Array<{ name: string; index: number }>;
88
+ options?: Array<{ name: string; flag: string; aliases?: string[]; runtime?: boolean; type?: string }>;
89
+ };
90
+ };
91
+
92
+ export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
93
+ const cwd = input.cwd ?? process.cwd();
94
+ const words = input.words.length > 0 ? input.words : ["rig"];
95
+ const currentIndex = input.currentIndex ?? Math.max(0, words.length - 1);
96
+ const current = words[currentIndex] ?? "";
97
+ const before = words.slice(1, currentIndex);
98
+ const command = findCommand(before);
99
+
100
+ if (expectsOptionValue(before)) return [];
101
+
102
+ if (!command) {
103
+ const rootOperation = parseRootOperation(before);
104
+ if (rootOperation.operation) {
105
+ if (current.startsWith("-")) {
106
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
107
+ return filterItems([
108
+ ...(operation?.cli?.options ?? []).flatMap((option) => [
109
+ { value: option.flag, description: option.name },
110
+ ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
111
+ ]),
112
+ ...COMMAND_OPTIONS.operation,
113
+ ...GLOBAL_OPTIONS,
114
+ ], current);
115
+ }
116
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
117
+ const operationPositionalCount = countRunOperationPositionals(rootOperation.args);
118
+ const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
119
+ if (positional && /workspace|vm/i.test(positional.name)) {
120
+ return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
121
+ }
122
+ return [];
123
+ }
124
+ return filterItems(current.startsWith("-") ? GLOBAL_OPTIONS : [...COMMANDS, ...await safeOperationTargets(resolveProjectDir(words, cwd)), ...GLOBAL_OPTIONS], current);
125
+ }
126
+
127
+ if (current.startsWith("-")) {
128
+ if (command === "run") {
129
+ const run = parseRunCommand(before);
130
+ if (run.operation) {
131
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
132
+ return filterItems([
133
+ ...(operation?.cli?.options ?? []).flatMap((option) => [
134
+ { value: option.flag, description: option.name },
135
+ ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
136
+ ]),
137
+ ...COMMAND_OPTIONS.operation,
138
+ ...GLOBAL_OPTIONS,
139
+ ], current);
140
+ }
141
+ }
142
+ return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
143
+ }
144
+
145
+ const positionalCount = countPositionals(before, command);
146
+
147
+ if (command === "run") {
148
+ const run = parseRunCommand(before);
149
+ if (!run.operation) {
150
+ return filterItems(await safeOperationTargets(resolveProjectDir(words, cwd)), current);
151
+ }
152
+ const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
153
+ const operationPositionalCount = countRunOperationPositionals(run.args);
154
+ const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
155
+ if (positional && /workspace|vm/i.test(positional.name)) {
156
+ return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
157
+ }
158
+ }
159
+
160
+ if (command === "completion" && positionalCount === 0) {
161
+ return filterItems(COMMAND_OPTIONS.completion, current);
162
+ }
163
+
164
+ if (command === "ls" && positionalCount === 0) {
165
+ return filterItems(COMMAND_OPTIONS.ls, current);
166
+ }
167
+
168
+ return [];
169
+ }
170
+
171
+ export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
172
+ const lines = items.map((item) => {
173
+ if (shell === "bash") return item.value;
174
+ return item.description ? `${item.value}\t${item.description}` : item.value;
175
+ });
176
+ return lines.join("\n");
177
+ }
178
+
179
+ export function resolveCompletionShell(value: string | undefined, env: NodeJS.ProcessEnv = process.env): CompletionShell {
180
+ if (value === "bash" || value === "fish" || value === "zsh") return value;
181
+ if (value) throw new Error(`Unsupported shell ${value}. Expected bash, fish, or zsh.`);
182
+
183
+ const shell = env.SHELL ?? "";
184
+ if (shell.endsWith("/fish")) return "fish";
185
+ if (shell.endsWith("/bash")) return "bash";
186
+ return "zsh";
187
+ }
188
+
189
+ export function renderCompletionScript(shell: CompletionShell): string {
190
+ if (shell === "bash") {
191
+ return `# rig bash completion
192
+ _rig_completion() {
193
+ local completions
194
+ completions="$(command rig __complete --shell bash --index "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)"
195
+ COMPREPLY=($(compgen -W "$completions" -- "\${COMP_WORDS[COMP_CWORD]}"))
196
+ }
197
+ complete -F _rig_completion rig
198
+ `;
199
+ }
200
+
201
+ if (shell === "fish") {
202
+ return `# rig fish completion
203
+ function __rig_complete
204
+ set -l tokens (commandline -opc)
205
+ set -l current (commandline -ct)
206
+ set -l index (count $tokens)
207
+ command rig __complete --shell fish --index $index -- $tokens $current 2>/dev/null
208
+ end
209
+ complete -c rig -f -a "(__rig_complete)"
210
+ `;
211
+ }
212
+
213
+ return `#compdef rig
214
+ # rig zsh completion
215
+ _rig() {
216
+ local -a raw completions
217
+ local line value description
218
+ raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
219
+ for line in "\${raw[@]}"; do
220
+ value="\${line%%$'\\t'*}"
221
+ if [[ "$line" == *$'\\t'* ]]; then
222
+ description="\${line#*$'\\t'}"
223
+ completions+=("\${value}:\${description}")
224
+ else
225
+ completions+=("\${value}")
226
+ fi
227
+ done
228
+ _describe 'rig' completions
229
+ }
230
+ compdef _rig rig
231
+ `;
232
+ }
233
+
234
+ function findCommand(words: string[]): string | undefined {
235
+ for (let index = 0; index < words.length; index += 1) {
236
+ const word = words[index]!;
237
+ if (OPTIONS_WITH_VALUES.has(word)) {
238
+ index += 1;
239
+ continue;
240
+ }
241
+ if (word.startsWith("--") && word.includes("=")) continue;
242
+ if (word.startsWith("-")) continue;
243
+
244
+ const canonical = COMMAND_ALIASES.get(word) ?? word;
245
+ if (COMMANDS.some((command) => command.value === canonical)) return canonical;
246
+ }
247
+ return undefined;
248
+ }
249
+
250
+ function countPositionals(words: string[], command: string): number {
251
+ let foundCommand = false;
252
+ let count = 0;
253
+
254
+ for (let index = 0; index < words.length; index += 1) {
255
+ const word = words[index]!;
256
+ if (OPTIONS_WITH_VALUES.has(word)) {
257
+ index += 1;
258
+ continue;
259
+ }
260
+ if (word.startsWith("--") && word.includes("=")) continue;
261
+ if (word.startsWith("-")) continue;
262
+
263
+ const canonical = COMMAND_ALIASES.get(word) ?? word;
264
+ if (!foundCommand && canonical === command) {
265
+ foundCommand = true;
266
+ continue;
267
+ }
268
+ if (foundCommand) count += 1;
269
+ }
270
+
271
+ return count;
272
+ }
273
+
274
+ function parseRunCommand(words: string[]): { operation?: string; args: string[] } {
275
+ let foundRun = false;
276
+ const args: string[] = [];
277
+ for (let index = 0; index < words.length; index += 1) {
278
+ const word = words[index]!;
279
+ if (OPTIONS_WITH_VALUES.has(word)) {
280
+ index += 1;
281
+ continue;
282
+ }
283
+ if (word.startsWith("--") && word.includes("=")) continue;
284
+ if (word.startsWith("-")) continue;
285
+ if (!foundRun) {
286
+ if (word === "run") foundRun = true;
287
+ continue;
288
+ }
289
+ args.push(word);
290
+ }
291
+ return { operation: args[0], args: args.slice(1) };
292
+ }
293
+
294
+ function parseRootOperation(words: string[]): { operation?: string; args: string[] } {
295
+ const args: string[] = [];
296
+ for (let index = 0; index < words.length; index += 1) {
297
+ const word = words[index]!;
298
+ if (OPTIONS_WITH_VALUES.has(word)) {
299
+ index += 1;
300
+ continue;
301
+ }
302
+ if (word.startsWith("--") && word.includes("=")) continue;
303
+ if (word.startsWith("-")) continue;
304
+ args.push(word);
305
+ }
306
+ return { operation: args[0], args: args.slice(1) };
307
+ }
308
+
309
+ function countRunOperationPositionals(args: string[]): number {
310
+ let count = 0;
311
+ for (let index = 0; index < args.length; index += 1) {
312
+ const arg = args[index]!;
313
+ if (OPTIONS_WITH_VALUES.has(arg)) {
314
+ index += 1;
315
+ continue;
316
+ }
317
+ if (arg.startsWith("--") && arg.includes("=")) continue;
318
+ if (arg.startsWith("-")) continue;
319
+ count += 1;
320
+ }
321
+ return count;
322
+ }
323
+
324
+ function expectsOptionValue(words: string[]): boolean {
325
+ const previous = words.at(-1);
326
+ return Boolean(previous && OPTIONS_WITH_VALUES.has(previous));
327
+ }
328
+
329
+ function resolveProjectDir(words: string[], cwd: string): { projectDir: string; configPath: string } {
330
+ for (let index = 0; index < words.length; index += 1) {
331
+ const word = words[index]!;
332
+ if (word === "-C" || word === "--project") {
333
+ const value = words[index + 1];
334
+ if (value) return projectPaths(resolve(cwd, value));
335
+ }
336
+ if (word.startsWith("--project=")) {
337
+ return projectPaths(resolve(cwd, word.slice("--project=".length)));
338
+ }
339
+ if (word === "--config") {
340
+ const value = words[index + 1];
341
+ if (value) return { projectDir: dirname(resolve(cwd, value)), configPath: resolve(cwd, value) };
342
+ }
343
+ if (word.startsWith("--config=")) {
344
+ const configPath = resolve(cwd, word.slice("--config=".length));
345
+ return { projectDir: dirname(configPath), configPath };
346
+ }
347
+ }
348
+
349
+ return projectPaths(cwd);
350
+ }
351
+
352
+ function projectPaths(projectDir: string): { projectDir: string; configPath: string } {
353
+ return { projectDir, configPath: join(projectDir, "rig.config.ts") };
354
+ }
355
+
356
+ async function workspaceTargets(
357
+ paths: { projectDir: string; configPath: string },
358
+ current: string,
359
+ includeVmIds: boolean,
360
+ ): Promise<CompletionItem[]> {
361
+ const workspaces = await readWorkspaces(paths);
362
+ const items = workspaces.map((workspace) => ({
363
+ value: workspace.name,
364
+ description: workspace.resourceId,
365
+ }));
366
+
367
+ if (includeVmIds && current.length > 0) {
368
+ for (const workspace of workspaces) {
369
+ if (!workspace.resourceId) continue;
370
+ items.push({
371
+ value: workspace.resourceId,
372
+ description: workspace.name,
373
+ });
374
+ }
375
+ }
376
+
377
+ return dedupeItems(items);
378
+ }
379
+
380
+ async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<Array<{ name: string; resourceId?: string }>> {
381
+ const runtime = await getOrStartRuntime(paths);
382
+ const { workspaces } = await runtime.control.workspaces();
383
+ return workspaces.map((workspace) => ({
384
+ name: workspace.name,
385
+ resourceId: workspace.resourceId,
386
+ }));
387
+ }
388
+
389
+ async function operationTargets(paths: { projectDir: string; configPath: string }): Promise<CompletionItem[]> {
390
+ const manifest = await readOperations(paths);
391
+ return manifest.operations.flatMap((operation) => [
392
+ { value: operation.id, description: operation.description },
393
+ ...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
394
+ ]);
395
+ }
396
+
397
+ async function safeOperationTargets(paths: { projectDir: string; configPath: string }): Promise<CompletionItem[]> {
398
+ try {
399
+ return await operationTargets(paths);
400
+ } catch {
401
+ return [];
402
+ }
403
+ }
404
+
405
+ async function resolveRuntimeOperation(
406
+ paths: { projectDir: string; configPath: string },
407
+ operationId: string,
408
+ ): Promise<RuntimeOperationDefinition | undefined> {
409
+ const manifest = await readOperations(paths);
410
+ return manifest.operations.find((operation) =>
411
+ operation.id === operationId || operation.aliases?.includes(operationId)
412
+ );
413
+ }
414
+
415
+ async function safeResolveRuntimeOperation(
416
+ paths: { projectDir: string; configPath: string },
417
+ operationId: string,
418
+ ): Promise<RuntimeOperationDefinition | undefined> {
419
+ try {
420
+ return await resolveRuntimeOperation(paths, operationId);
421
+ } catch {
422
+ return undefined;
423
+ }
424
+ }
425
+
426
+ async function readOperations(paths: { projectDir: string; configPath: string }): Promise<RuntimeOperationManifest> {
427
+ const runtime = await getOrStartRuntime(paths);
428
+ return await runtime.control.operations() as unknown as RuntimeOperationManifest;
429
+ }
430
+
431
+ function filterItems(items: CompletionItem[], current: string): CompletionItem[] {
432
+ return dedupeItems(items).filter((item) => item.value.startsWith(current));
433
+ }
434
+
435
+ function dedupeItems(items: CompletionItem[]): CompletionItem[] {
436
+ const seen = new Set<string>();
437
+ const deduped: CompletionItem[] = [];
438
+ for (const item of items) {
439
+ if (seen.has(item.value)) continue;
440
+ seen.add(item.value);
441
+ deduped.push(item);
442
+ }
443
+ return deduped;
444
+ }