@rigkit/cli 0.0.0-canary-20260518T014918-c5bc0c2
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 +34 -0
- package/package.json +42 -0
- package/src/cli.test.ts +419 -0
- package/src/cli.ts +2496 -0
- package/src/completion.test.ts +413 -0
- package/src/completion.ts +844 -0
- package/src/init.test.ts +90 -0
- package/src/init.ts +269 -0
- package/src/interaction.test.ts +28 -0
- package/src/interaction.ts +33 -0
- package/src/project.test.ts +81 -0
- package/src/project.ts +184 -0
- package/src/run-logger.test.ts +92 -0
- package/src/run-logger.ts +203 -0
- package/src/run-presenter.ts +250 -0
- package/src/ui.ts +159 -0
- package/src/version.ts +1 -0
- package/src/workspace-name.test.ts +17 -0
- package/src/workspace-name.ts +59 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { getOrStartRuntime } from "@rigkit/runtime-client";
|
|
4
|
+
import { isRigConfigFileName } from "./project.ts";
|
|
5
|
+
|
|
6
|
+
export type CompletionShell = "bash" | "fish" | "zsh";
|
|
7
|
+
|
|
8
|
+
export type CompletionItem = {
|
|
9
|
+
value: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
noSpace?: boolean;
|
|
12
|
+
group?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type CompleteRigInput = {
|
|
16
|
+
words: string[];
|
|
17
|
+
currentIndex?: number;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const GROUP_COMMANDS = "Commands";
|
|
22
|
+
const GROUP_SUBCOMMANDS = "Subcommands";
|
|
23
|
+
const GROUP_FLAGS = "Flags";
|
|
24
|
+
const GROUP_GLOBAL = "Global flags";
|
|
25
|
+
const GROUP_TARGETS = "Targets";
|
|
26
|
+
const GROUP_WORKSPACES = "Workspaces";
|
|
27
|
+
const GROUP_OPERATIONS = "Operations";
|
|
28
|
+
const GROUP_VALUES = "Values";
|
|
29
|
+
const GROUP_PATHS = "Paths";
|
|
30
|
+
const GROUP_SHELLS = "Shells";
|
|
31
|
+
|
|
32
|
+
const COMMANDS: CompletionItem[] = withGroup(GROUP_COMMANDS, [
|
|
33
|
+
{ value: "help", description: "show CLI help" },
|
|
34
|
+
{ value: "init", description: "initialize a Rigkit project" },
|
|
35
|
+
{ value: "plan", description: "plan project workflow changes" },
|
|
36
|
+
{ value: "apply", description: "apply project workflow changes" },
|
|
37
|
+
{ value: "create", description: "create a workspace" },
|
|
38
|
+
{ value: "rm", description: "remove a workspace" },
|
|
39
|
+
{ value: "run", description: "run a workspace operation" },
|
|
40
|
+
{ value: "ls", description: "list project workspaces" },
|
|
41
|
+
{ value: "cache", description: "inspect and clear Rigkit cache" },
|
|
42
|
+
{ value: "projects", description: "discover Rigkit projects" },
|
|
43
|
+
{ value: "doctor", description: "show runtime diagnostics" },
|
|
44
|
+
{ value: "version", description: "show CLI version" },
|
|
45
|
+
{ value: "completion", description: "generate shell completion" },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const COMMAND_ALIASES = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
const GLOBAL_OPTIONS: CompletionItem[] = withGroup(GROUP_GLOBAL, [
|
|
51
|
+
{ value: "-chdir=", description: "working directory", noSpace: true },
|
|
52
|
+
{ value: "-config=", description: "config file", noSpace: true },
|
|
53
|
+
{ value: "-state=", description: "state database path", noSpace: true },
|
|
54
|
+
{ value: "-json", description: "print JSON" },
|
|
55
|
+
{ value: "-help", description: "show help" },
|
|
56
|
+
{ value: "-version", description: "show version" },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const COMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
60
|
+
init: withGroup(GROUP_FLAGS, [
|
|
61
|
+
{ value: "--name", description: "project and workflow name" },
|
|
62
|
+
{ value: "--api-key", description: "Freestyle API key" },
|
|
63
|
+
{ value: "--package-manager", description: "npm, bun, pnpm, or skip" },
|
|
64
|
+
{ value: "--force", description: "overwrite existing config" },
|
|
65
|
+
{ value: "--json", description: "print JSON" },
|
|
66
|
+
]),
|
|
67
|
+
plan: withGroup(GROUP_FLAGS, [
|
|
68
|
+
{ value: "--all", description: "run against every discovered project" },
|
|
69
|
+
{ value: "--discover", description: "discover projects below the selected directory" },
|
|
70
|
+
{ value: "--json", description: "print JSON" },
|
|
71
|
+
]),
|
|
72
|
+
apply: withGroup(GROUP_FLAGS, [
|
|
73
|
+
{ value: "--all", description: "run against every discovered project" },
|
|
74
|
+
{ value: "--discover", description: "discover projects below the selected directory" },
|
|
75
|
+
{ value: "--json", description: "print JSON" },
|
|
76
|
+
]),
|
|
77
|
+
create: withGroup(GROUP_FLAGS, [
|
|
78
|
+
{ value: "--json", description: "print JSON" },
|
|
79
|
+
]),
|
|
80
|
+
rm: withGroup(GROUP_FLAGS, [
|
|
81
|
+
{ value: "-y", description: "skip confirmation" },
|
|
82
|
+
{ value: "--yes", description: "skip confirmation" },
|
|
83
|
+
{ value: "--json", description: "print JSON" },
|
|
84
|
+
]),
|
|
85
|
+
run: withGroup(GROUP_FLAGS, [
|
|
86
|
+
{ value: "--json", description: "print JSON" },
|
|
87
|
+
]),
|
|
88
|
+
ls: [
|
|
89
|
+
...withGroup(GROUP_TARGETS, [
|
|
90
|
+
{ value: "workspaces", description: "list workspaces" },
|
|
91
|
+
{ value: "snapshots", description: "list snapshots" },
|
|
92
|
+
{ value: "config", description: "show project config" },
|
|
93
|
+
]),
|
|
94
|
+
...withGroup(GROUP_FLAGS, [
|
|
95
|
+
{ value: "--json", description: "print JSON" },
|
|
96
|
+
]),
|
|
97
|
+
],
|
|
98
|
+
projects: withGroup(GROUP_FLAGS, [
|
|
99
|
+
{ value: "--json", description: "print JSON" },
|
|
100
|
+
]),
|
|
101
|
+
cache: withGroup(GROUP_SUBCOMMANDS, [
|
|
102
|
+
{ value: "ls", description: "list cache entries" },
|
|
103
|
+
{ value: "clear", description: "clear cache entries" },
|
|
104
|
+
]),
|
|
105
|
+
completion: withGroup(GROUP_SHELLS, [
|
|
106
|
+
{ value: "bash", description: "Bash completion" },
|
|
107
|
+
{ value: "fish", description: "fish completion" },
|
|
108
|
+
{ value: "zsh", description: "zsh completion" },
|
|
109
|
+
]),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const CACHE_SUBCOMMAND_OPTIONS: Record<string, CompletionItem[]> = {
|
|
113
|
+
ls: withGroup(GROUP_FLAGS, [
|
|
114
|
+
{ value: "--json", description: "print JSON" },
|
|
115
|
+
]),
|
|
116
|
+
clear: withGroup(GROUP_FLAGS, [
|
|
117
|
+
{ value: "--local", description: "clear local cache entries" },
|
|
118
|
+
{ value: "--global", description: "clear global cache fragments" },
|
|
119
|
+
{ value: "--all", description: "clear every global fragment" },
|
|
120
|
+
{ value: "--json", description: "print JSON" },
|
|
121
|
+
]),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function withGroup(group: string, items: Omit<CompletionItem, "group">[]): CompletionItem[] {
|
|
125
|
+
return items.map((item) => ({ ...item, group }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const PROJECT_OPERATION_COMMANDS = new Set(["plan", "apply", "create"]);
|
|
129
|
+
|
|
130
|
+
const OPTIONS_WITH_VALUES = new Set([
|
|
131
|
+
"-chdir",
|
|
132
|
+
"--chdir",
|
|
133
|
+
"-config",
|
|
134
|
+
"--config",
|
|
135
|
+
"-state",
|
|
136
|
+
"--state",
|
|
137
|
+
"--name",
|
|
138
|
+
"--api-key",
|
|
139
|
+
"--package-manager",
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
type RuntimeOperationManifest = {
|
|
143
|
+
operations: RuntimeOperationDefinition[];
|
|
144
|
+
workspaceOperations?: RuntimeOperationDefinition[];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type RuntimeOperationDefinition = {
|
|
148
|
+
id: string;
|
|
149
|
+
aliases?: string[];
|
|
150
|
+
description?: string;
|
|
151
|
+
cli?: {
|
|
152
|
+
positionals?: Array<{ name: string; index: number }>;
|
|
153
|
+
options?: Array<{ name: string; flag: string; aliases?: string[]; runtime?: boolean; type?: string }>;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type RuntimeWorkspaceCompletion = {
|
|
158
|
+
name: string;
|
|
159
|
+
workflow: string;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
updatedAt: string;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export async function completeRig(input: CompleteRigInput): Promise<CompletionItem[]> {
|
|
165
|
+
const cwd = input.cwd ?? process.cwd();
|
|
166
|
+
const words = input.words.length > 0 ? input.words : ["rig"];
|
|
167
|
+
const currentIndex = input.currentIndex ?? Math.max(0, words.length - 1);
|
|
168
|
+
const current = words[currentIndex] ?? "";
|
|
169
|
+
const before = words.slice(1, currentIndex);
|
|
170
|
+
const command = findCommand(before);
|
|
171
|
+
|
|
172
|
+
const inlineOption = parseInlineValueOption(current);
|
|
173
|
+
if (inlineOption) {
|
|
174
|
+
return await completeOptionValue({
|
|
175
|
+
option: inlineOption.option,
|
|
176
|
+
current: inlineOption.value,
|
|
177
|
+
cwd,
|
|
178
|
+
words,
|
|
179
|
+
inlinePrefix: inlineOption.prefix,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const valueOption = optionExpectingValue(before);
|
|
184
|
+
if (valueOption) {
|
|
185
|
+
return await completeOptionValue({
|
|
186
|
+
option: valueOption,
|
|
187
|
+
current,
|
|
188
|
+
cwd,
|
|
189
|
+
words,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!command) {
|
|
194
|
+
return filterItems(
|
|
195
|
+
current.startsWith("-")
|
|
196
|
+
? GLOBAL_OPTIONS
|
|
197
|
+
: [...COMMANDS, ...GLOBAL_OPTIONS],
|
|
198
|
+
current,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (current.startsWith("-")) {
|
|
203
|
+
if (command === "rm") {
|
|
204
|
+
const remove = parseRemoveCommand(before);
|
|
205
|
+
if (remove.workspace) {
|
|
206
|
+
const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), "remove");
|
|
207
|
+
return filterItems([
|
|
208
|
+
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
209
|
+
{ value: option.flag, description: option.name, group: GROUP_FLAGS },
|
|
210
|
+
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name, group: GROUP_FLAGS })),
|
|
211
|
+
]),
|
|
212
|
+
...COMMAND_OPTIONS.rm,
|
|
213
|
+
...GLOBAL_OPTIONS,
|
|
214
|
+
], current);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (command === "run") {
|
|
218
|
+
const run = parseWorkspaceRunCommand(before);
|
|
219
|
+
if (run.workspace && run.operation) {
|
|
220
|
+
const operation = await safeResolveWorkspaceOperation(resolveProjectDir(words, cwd), run.operation);
|
|
221
|
+
return filterItems([
|
|
222
|
+
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
223
|
+
{ value: option.flag, description: option.name, group: GROUP_FLAGS },
|
|
224
|
+
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name, group: GROUP_FLAGS })),
|
|
225
|
+
]),
|
|
226
|
+
...COMMAND_OPTIONS.run,
|
|
227
|
+
...GLOBAL_OPTIONS,
|
|
228
|
+
], current);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (PROJECT_OPERATION_COMMANDS.has(command)) {
|
|
232
|
+
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), command);
|
|
233
|
+
return filterItems([
|
|
234
|
+
...(operation?.cli?.options ?? []).flatMap((option) => [
|
|
235
|
+
{ value: option.flag, description: option.name },
|
|
236
|
+
...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
|
|
237
|
+
]),
|
|
238
|
+
...(COMMAND_OPTIONS[command] ?? []),
|
|
239
|
+
...GLOBAL_OPTIONS,
|
|
240
|
+
], current);
|
|
241
|
+
}
|
|
242
|
+
if (command === "cache") {
|
|
243
|
+
return filterItems([
|
|
244
|
+
...cacheOptionTargets(before),
|
|
245
|
+
...GLOBAL_OPTIONS,
|
|
246
|
+
], current);
|
|
247
|
+
}
|
|
248
|
+
return filterItems([...(COMMAND_OPTIONS[command] ?? []), ...GLOBAL_OPTIONS], current);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const positionalCount = countPositionals(before, command);
|
|
252
|
+
|
|
253
|
+
if (command === "run") {
|
|
254
|
+
const run = parseWorkspaceRunCommand(before);
|
|
255
|
+
if (!run.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
|
|
256
|
+
if (!run.operation) return filterItems(await safeWorkspaceOperationTargets(resolveProjectDir(words, cwd)), current);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (command === "rm") {
|
|
260
|
+
const remove = parseRemoveCommand(before);
|
|
261
|
+
if (!remove.workspace) return filterItems(await workspaceTargets(resolveProjectDir(words, cwd)), current);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (command === "completion" && positionalCount === 0) {
|
|
265
|
+
return filterItems(COMMAND_OPTIONS.completion, current);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (command === "ls" && positionalCount === 0) {
|
|
269
|
+
return filterItems(COMMAND_OPTIONS.ls, current);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (command === "cache") {
|
|
273
|
+
const cache = parseCacheCommand(before);
|
|
274
|
+
if (!cache.subcommand) return filterItems(COMMAND_OPTIONS.cache ?? [], current);
|
|
275
|
+
return filterItems(cacheOptionTargets(before), current);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
|
|
282
|
+
const lines = items.map((item) => {
|
|
283
|
+
if (shell === "bash") return item.value;
|
|
284
|
+
if (shell === "zsh") {
|
|
285
|
+
const description = item.description ?? "";
|
|
286
|
+
const marker = item.noSpace ? "nospace" : "";
|
|
287
|
+
const group = item.group ?? "";
|
|
288
|
+
return `${item.value}\t${description}\t${marker}\t${group}`;
|
|
289
|
+
}
|
|
290
|
+
// fish: legacy two-column format works fine; descriptions render dim by default
|
|
291
|
+
return item.description ? `${item.value}\t${item.description}` : item.value;
|
|
292
|
+
});
|
|
293
|
+
return lines.join("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function resolveCompletionShell(value: string | undefined, env: NodeJS.ProcessEnv = process.env): CompletionShell {
|
|
297
|
+
if (value === "bash" || value === "fish" || value === "zsh") return value;
|
|
298
|
+
if (value) throw new Error(`Unsupported shell ${value}. Expected bash, fish, or zsh.`);
|
|
299
|
+
|
|
300
|
+
const shell = env.SHELL ?? "";
|
|
301
|
+
if (shell.endsWith("/fish")) return "fish";
|
|
302
|
+
if (shell.endsWith("/bash")) return "bash";
|
|
303
|
+
return "zsh";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function renderCompletionScript(shell: CompletionShell): string {
|
|
307
|
+
if (shell === "bash") {
|
|
308
|
+
return `# rig bash completion
|
|
309
|
+
_rig_completion() {
|
|
310
|
+
local completions
|
|
311
|
+
completions="$(command rig __complete --shell bash --index "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)"
|
|
312
|
+
COMPREPLY=($(compgen -W "$completions" -- "\${COMP_WORDS[COMP_CWORD]}"))
|
|
313
|
+
}
|
|
314
|
+
complete -F _rig_completion rig
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (shell === "fish") {
|
|
319
|
+
return `# rig fish completion
|
|
320
|
+
function __rig_complete
|
|
321
|
+
set -l tokens (commandline -opc)
|
|
322
|
+
set -l current (commandline -ct)
|
|
323
|
+
set -l index (count $tokens)
|
|
324
|
+
command rig __complete --shell fish --index $index -- $tokens $current 2>/dev/null
|
|
325
|
+
end
|
|
326
|
+
complete -c rig -f -a "(__rig_complete)"
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return `#compdef rig
|
|
331
|
+
# rig zsh completion — auto-generated by \`rig completion zsh\`.
|
|
332
|
+
# Visual defaults are scoped to :completion:*:rig:* so they don't override your
|
|
333
|
+
# global completion theme. Group headers render bold blue; descriptions inherit
|
|
334
|
+
# your usual style.
|
|
335
|
+
|
|
336
|
+
() {
|
|
337
|
+
zstyle ':completion:*:rig:*:descriptions' format $'\\e[1;34m%d\\e[0m'
|
|
338
|
+
zstyle ':completion:*:rig:*' group-name ''
|
|
339
|
+
zstyle ':completion:*:rig:*' verbose true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_rig() {
|
|
343
|
+
local raw line value description marker group key tag
|
|
344
|
+
local -A bucket_specs bucket_groups bucket_data
|
|
345
|
+
raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
|
|
346
|
+
|
|
347
|
+
for line in "\${raw[@]}"; do
|
|
348
|
+
[[ -z "$line" ]] && continue
|
|
349
|
+
local -a parts
|
|
350
|
+
parts=("\${(@s: :)line}")
|
|
351
|
+
value="\${parts[1]}"
|
|
352
|
+
description="\${parts[2]:-}"
|
|
353
|
+
marker="\${parts[3]:-}"
|
|
354
|
+
group="\${parts[4]:-}"
|
|
355
|
+
[[ -z "$group" ]] && group="rig"
|
|
356
|
+
key="\${group}|\${marker}"
|
|
357
|
+
bucket_groups[$key]="$group"
|
|
358
|
+
bucket_specs[$key]="$marker"
|
|
359
|
+
if [[ -n "$description" ]]; then
|
|
360
|
+
bucket_data[$key]+="\${value}:\${description}"$'\\n'
|
|
361
|
+
else
|
|
362
|
+
bucket_data[$key]+="\${value}"$'\\n'
|
|
363
|
+
fi
|
|
364
|
+
done
|
|
365
|
+
|
|
366
|
+
for key in "\${(@k)bucket_data}"; do
|
|
367
|
+
local -a matches
|
|
368
|
+
matches=("\${(@f)bucket_data[$key]}")
|
|
369
|
+
matches=("\${(@)matches:#}")
|
|
370
|
+
tag="\${bucket_groups[$key]//[^A-Za-z0-9]/_}"
|
|
371
|
+
[[ -z "$tag" ]] && tag="rig"
|
|
372
|
+
if [[ "\${bucket_specs[$key]}" == "nospace" ]]; then
|
|
373
|
+
_describe -t "$tag" "\${bucket_groups[$key]}" matches -S ''
|
|
374
|
+
else
|
|
375
|
+
_describe -t "$tag" "\${bucket_groups[$key]}" matches
|
|
376
|
+
fi
|
|
377
|
+
done
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
compdef _rig rig
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function findCommand(words: string[]): string | undefined {
|
|
385
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
386
|
+
const word = words[index]!;
|
|
387
|
+
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
388
|
+
index += 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (word.startsWith("--") && word.includes("=")) continue;
|
|
392
|
+
if (word.startsWith("-")) continue;
|
|
393
|
+
|
|
394
|
+
const canonical = COMMAND_ALIASES.get(word) ?? word;
|
|
395
|
+
if (COMMANDS.some((command) => command.value === canonical)) return canonical;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function countPositionals(words: string[], command: string): number {
|
|
401
|
+
let foundCommand = false;
|
|
402
|
+
let count = 0;
|
|
403
|
+
|
|
404
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
405
|
+
const word = words[index]!;
|
|
406
|
+
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
407
|
+
index += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (word.startsWith("--") && word.includes("=")) continue;
|
|
411
|
+
if (word.startsWith("-")) continue;
|
|
412
|
+
|
|
413
|
+
const canonical = COMMAND_ALIASES.get(word) ?? word;
|
|
414
|
+
if (!foundCommand && canonical === command) {
|
|
415
|
+
foundCommand = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (foundCommand) count += 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return count;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parseWorkspaceRunCommand(words: string[]): { workspace?: string; operation?: string; args: string[] } {
|
|
425
|
+
let foundRun = false;
|
|
426
|
+
const args: string[] = [];
|
|
427
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
428
|
+
const word = words[index]!;
|
|
429
|
+
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
430
|
+
index += 1;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (word.startsWith("--") && word.includes("=")) continue;
|
|
434
|
+
if (word.startsWith("-")) continue;
|
|
435
|
+
if (!foundRun) {
|
|
436
|
+
if (word === "run") foundRun = true;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
args.push(word);
|
|
440
|
+
}
|
|
441
|
+
return { workspace: args[0], operation: args[1], args: args.slice(2) };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function parseRemoveCommand(words: string[]): { workspace?: string; args: string[] } {
|
|
445
|
+
let foundRemove = false;
|
|
446
|
+
const args: string[] = [];
|
|
447
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
448
|
+
const word = words[index]!;
|
|
449
|
+
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
450
|
+
index += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (word.includes("=") && OPTIONS_WITH_VALUES.has(word.slice(0, word.indexOf("=")))) continue;
|
|
454
|
+
if (word.startsWith("-")) continue;
|
|
455
|
+
if (!foundRemove) {
|
|
456
|
+
if (word === "rm") foundRemove = true;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
args.push(word);
|
|
460
|
+
}
|
|
461
|
+
return { workspace: args[0], args: args.slice(1) };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function parseCacheCommand(words: string[]): { subcommand?: string; args: string[] } {
|
|
465
|
+
let foundCache = false;
|
|
466
|
+
const args: string[] = [];
|
|
467
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
468
|
+
const word = words[index]!;
|
|
469
|
+
if (OPTIONS_WITH_VALUES.has(word)) {
|
|
470
|
+
index += 1;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (word.startsWith("--") && word.includes("=")) continue;
|
|
474
|
+
if (word.startsWith("-")) continue;
|
|
475
|
+
if (!foundCache) {
|
|
476
|
+
if (word === "cache") foundCache = true;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
args.push(word);
|
|
480
|
+
}
|
|
481
|
+
return { subcommand: args[0], args: args.slice(1) };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function cacheOptionTargets(words: string[]): CompletionItem[] {
|
|
485
|
+
const subcommand = parseCacheCommand(words).subcommand;
|
|
486
|
+
return subcommand ? CACHE_SUBCOMMAND_OPTIONS[subcommand] ?? [] : [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function optionExpectingValue(words: string[]): string | undefined {
|
|
490
|
+
const previous = words.at(-1);
|
|
491
|
+
return previous && OPTIONS_WITH_VALUES.has(previous) ? previous : undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function completeOptionValue(input: {
|
|
495
|
+
option: string;
|
|
496
|
+
current: string;
|
|
497
|
+
cwd: string;
|
|
498
|
+
words: string[];
|
|
499
|
+
inlinePrefix?: string;
|
|
500
|
+
}): Promise<CompletionItem[]> {
|
|
501
|
+
let items: CompletionItem[];
|
|
502
|
+
switch (input.option) {
|
|
503
|
+
case "-chdir":
|
|
504
|
+
case "--chdir":
|
|
505
|
+
items = completeDirectories(input.cwd, input.current);
|
|
506
|
+
break;
|
|
507
|
+
case "-config":
|
|
508
|
+
case "--config":
|
|
509
|
+
items = completeConfigPaths(projectBaseDir(input.words, input.cwd), input.current);
|
|
510
|
+
break;
|
|
511
|
+
case "--package-manager":
|
|
512
|
+
items = filterItems([
|
|
513
|
+
{ value: "npm", group: GROUP_VALUES },
|
|
514
|
+
{ value: "bun", group: GROUP_VALUES },
|
|
515
|
+
{ value: "pnpm", group: GROUP_VALUES },
|
|
516
|
+
{ value: "skip", group: GROUP_VALUES },
|
|
517
|
+
], input.current);
|
|
518
|
+
break;
|
|
519
|
+
case "-state":
|
|
520
|
+
case "--state":
|
|
521
|
+
items = completeFilesystemPaths(input.cwd, input.current);
|
|
522
|
+
break;
|
|
523
|
+
default:
|
|
524
|
+
items = [];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (!input.inlinePrefix) return items;
|
|
528
|
+
return items.map((item) => ({
|
|
529
|
+
...item,
|
|
530
|
+
value: `${input.inlinePrefix}${item.value}`,
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function parseInlineValueOption(current: string): { option: string; value: string; prefix: string } | undefined {
|
|
535
|
+
const index = current.indexOf("=");
|
|
536
|
+
if (index < 0) return undefined;
|
|
537
|
+
const option = current.slice(0, index);
|
|
538
|
+
if (!OPTIONS_WITH_VALUES.has(option)) return undefined;
|
|
539
|
+
return {
|
|
540
|
+
option,
|
|
541
|
+
value: current.slice(index + 1),
|
|
542
|
+
prefix: current.slice(0, index + 1),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function resolveProjectDir(words: string[], cwd: string): { projectDir: string; configPath: string } {
|
|
547
|
+
let chdir: string | undefined;
|
|
548
|
+
let config: string | undefined;
|
|
549
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
550
|
+
const word = words[index]!;
|
|
551
|
+
if (word === "-chdir" || word === "--chdir") {
|
|
552
|
+
chdir = words[index + 1];
|
|
553
|
+
index += 1;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (word.startsWith("-chdir=")) {
|
|
557
|
+
chdir = word.slice("-chdir=".length);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (word.startsWith("--chdir=")) {
|
|
561
|
+
chdir = word.slice("--chdir=".length);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (word === "-config" || word === "--config") {
|
|
565
|
+
config = words[index + 1];
|
|
566
|
+
index += 1;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (word.startsWith("-config=")) {
|
|
570
|
+
config = word.slice("-config=".length);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (word.startsWith("--config=")) {
|
|
574
|
+
config = word.slice("--config=".length);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const baseDir = resolve(cwd, chdir ?? ".");
|
|
580
|
+
if (config) {
|
|
581
|
+
const configPath = resolve(baseDir, config);
|
|
582
|
+
return { projectDir: dirname(configPath), configPath };
|
|
583
|
+
}
|
|
584
|
+
return projectPaths(baseDir);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function projectBaseDir(words: string[], cwd: string): string {
|
|
588
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
589
|
+
const word = words[index]!;
|
|
590
|
+
if (word === "-chdir" || word === "--chdir") {
|
|
591
|
+
const value = words[index + 1];
|
|
592
|
+
if (value) return resolve(cwd, value);
|
|
593
|
+
}
|
|
594
|
+
if (word.startsWith("-chdir=")) {
|
|
595
|
+
return resolve(cwd, word.slice("-chdir=".length));
|
|
596
|
+
}
|
|
597
|
+
if (word.startsWith("--chdir=")) {
|
|
598
|
+
return resolve(cwd, word.slice("--chdir=".length));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return cwd;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function projectPaths(projectDir: string): { projectDir: string; configPath: string } {
|
|
605
|
+
return { projectDir, configPath: join(projectDir, "rig.config.ts") };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function completeDirectories(baseDir: string, current: string): CompletionItem[] {
|
|
609
|
+
return completePathEntries(baseDir, current, {
|
|
610
|
+
includeFiles: false,
|
|
611
|
+
includeDirectories: true,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function completeConfigPaths(baseDir: string, current: string): CompletionItem[] {
|
|
616
|
+
return completePathEntries(baseDir, current, {
|
|
617
|
+
includeFiles: true,
|
|
618
|
+
includeDirectories: true,
|
|
619
|
+
fileFilter: isRigConfigFileName,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function completeFilesystemPaths(baseDir: string, current: string): CompletionItem[] {
|
|
624
|
+
return completePathEntries(baseDir, current, {
|
|
625
|
+
includeFiles: true,
|
|
626
|
+
includeDirectories: true,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function completePathEntries(
|
|
631
|
+
baseDir: string,
|
|
632
|
+
current: string,
|
|
633
|
+
options: {
|
|
634
|
+
includeFiles: boolean;
|
|
635
|
+
includeDirectories: boolean;
|
|
636
|
+
fileFilter?: (name: string) => boolean;
|
|
637
|
+
},
|
|
638
|
+
): CompletionItem[] {
|
|
639
|
+
const { dirPart, namePrefix, dir } = splitCompletionPath(baseDir, current);
|
|
640
|
+
let entries;
|
|
641
|
+
try {
|
|
642
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
643
|
+
} catch {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const items = entries
|
|
648
|
+
.filter((entry) => entry.name.startsWith(namePrefix))
|
|
649
|
+
.flatMap((entry): CompletionItem[] => {
|
|
650
|
+
if (entry.isDirectory()) {
|
|
651
|
+
if (!options.includeDirectories) return [];
|
|
652
|
+
if (shouldSkipCompletionDirectory(entry.name, namePrefix)) return [];
|
|
653
|
+
return [{
|
|
654
|
+
value: `${dirPart}${entry.name}/`,
|
|
655
|
+
description: "directory",
|
|
656
|
+
noSpace: true,
|
|
657
|
+
group: GROUP_PATHS,
|
|
658
|
+
}];
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (!entry.isFile() || !options.includeFiles) return [];
|
|
662
|
+
if (options.fileFilter && !options.fileFilter(entry.name)) return [];
|
|
663
|
+
return [{
|
|
664
|
+
value: `${dirPart}${entry.name}`,
|
|
665
|
+
description: "config",
|
|
666
|
+
group: GROUP_PATHS,
|
|
667
|
+
}];
|
|
668
|
+
})
|
|
669
|
+
.sort((left, right) => {
|
|
670
|
+
if (left.noSpace && !right.noSpace) return -1;
|
|
671
|
+
if (!left.noSpace && right.noSpace) return 1;
|
|
672
|
+
return left.value.localeCompare(right.value);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
return dedupeItems(items);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function shouldSkipCompletionDirectory(name: string, namePrefix: string): boolean {
|
|
679
|
+
if (
|
|
680
|
+
name === ".git" ||
|
|
681
|
+
name === ".rigkit" ||
|
|
682
|
+
name === ".turbo" ||
|
|
683
|
+
name === "node_modules" ||
|
|
684
|
+
name === "dist" ||
|
|
685
|
+
name === "build"
|
|
686
|
+
) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
return name.startsWith(".") && !namePrefix.startsWith(".");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function splitCompletionPath(baseDir: string, current: string): {
|
|
693
|
+
dirPart: string;
|
|
694
|
+
namePrefix: string;
|
|
695
|
+
dir: string;
|
|
696
|
+
} {
|
|
697
|
+
const slashIndex = current.lastIndexOf("/");
|
|
698
|
+
const dirPart = slashIndex >= 0 ? current.slice(0, slashIndex + 1) : "";
|
|
699
|
+
const namePrefix = slashIndex >= 0 ? current.slice(slashIndex + 1) : current;
|
|
700
|
+
return {
|
|
701
|
+
dirPart,
|
|
702
|
+
namePrefix,
|
|
703
|
+
dir: resolve(baseDir, dirPart || "."),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function workspaceTargets(
|
|
708
|
+
paths: { projectDir: string; configPath: string },
|
|
709
|
+
): Promise<CompletionItem[]> {
|
|
710
|
+
const workspaces = await readWorkspaces(paths);
|
|
711
|
+
const items = workspaces.map((workspace) => ({
|
|
712
|
+
value: workspace.name,
|
|
713
|
+
description: workspaceDescription(workspace),
|
|
714
|
+
group: GROUP_WORKSPACES,
|
|
715
|
+
}));
|
|
716
|
+
|
|
717
|
+
return dedupeItems(items);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<RuntimeWorkspaceCompletion[]> {
|
|
721
|
+
const runtime = await getOrStartRuntime(paths);
|
|
722
|
+
const { workspaces } = await runtime.control.workspaces();
|
|
723
|
+
return workspaces.map((workspace) => ({
|
|
724
|
+
name: workspace.name,
|
|
725
|
+
workflow: workspace.workflow,
|
|
726
|
+
createdAt: workspace.createdAt,
|
|
727
|
+
updatedAt: workspace.updatedAt,
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function safeWorkspaceOperationTargets(
|
|
732
|
+
paths: { projectDir: string; configPath: string },
|
|
733
|
+
): Promise<CompletionItem[]> {
|
|
734
|
+
try {
|
|
735
|
+
const manifest = await readOperations(paths);
|
|
736
|
+
return workspaceOperationTargets(manifest);
|
|
737
|
+
} catch {
|
|
738
|
+
return [];
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function workspaceOperationTargets(manifest: RuntimeOperationManifest): CompletionItem[] {
|
|
743
|
+
return (manifest.workspaceOperations ?? []).flatMap((operation) => [
|
|
744
|
+
{
|
|
745
|
+
value: operation.id,
|
|
746
|
+
description: operation.description ?? "workspace operation",
|
|
747
|
+
group: GROUP_OPERATIONS,
|
|
748
|
+
},
|
|
749
|
+
...(operation.aliases ?? []).map((alias) => ({
|
|
750
|
+
value: alias,
|
|
751
|
+
description: operation.description ?? "workspace operation",
|
|
752
|
+
group: GROUP_OPERATIONS,
|
|
753
|
+
})),
|
|
754
|
+
]);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function resolveRuntimeOperation(
|
|
758
|
+
paths: { projectDir: string; configPath: string },
|
|
759
|
+
operationId: string,
|
|
760
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
761
|
+
const manifest = await readOperations(paths);
|
|
762
|
+
return manifest.operations.find((operation) =>
|
|
763
|
+
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function safeResolveRuntimeOperation(
|
|
768
|
+
paths: { projectDir: string; configPath: string },
|
|
769
|
+
operationId: string,
|
|
770
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
771
|
+
try {
|
|
772
|
+
return await resolveRuntimeOperation(paths, operationId);
|
|
773
|
+
} catch {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function resolveWorkspaceOperation(
|
|
779
|
+
paths: { projectDir: string; configPath: string },
|
|
780
|
+
operationId: string,
|
|
781
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
782
|
+
const manifest = await readOperations(paths);
|
|
783
|
+
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
784
|
+
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function safeResolveWorkspaceOperation(
|
|
789
|
+
paths: { projectDir: string; configPath: string },
|
|
790
|
+
operationId: string,
|
|
791
|
+
): Promise<RuntimeOperationDefinition | undefined> {
|
|
792
|
+
try {
|
|
793
|
+
return await resolveWorkspaceOperation(paths, operationId);
|
|
794
|
+
} catch {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function readOperations(paths: { projectDir: string; configPath: string }): Promise<RuntimeOperationManifest> {
|
|
800
|
+
const runtime = await getOrStartRuntime(paths);
|
|
801
|
+
return await runtime.control.operations() as unknown as RuntimeOperationManifest;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function filterItems(items: CompletionItem[], current: string): CompletionItem[] {
|
|
805
|
+
return dedupeItems(items).filter((item) => item.value.startsWith(current));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function dedupeItems(items: CompletionItem[]): CompletionItem[] {
|
|
809
|
+
const seen = new Set<string>();
|
|
810
|
+
const deduped: CompletionItem[] = [];
|
|
811
|
+
for (const item of items) {
|
|
812
|
+
if (seen.has(item.value)) continue;
|
|
813
|
+
seen.add(item.value);
|
|
814
|
+
deduped.push(item);
|
|
815
|
+
}
|
|
816
|
+
return deduped;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function workspaceDescription(workspace: RuntimeWorkspaceCompletion): string {
|
|
820
|
+
const age = formatWorkspaceAge(workspace.createdAt);
|
|
821
|
+
return age ? `created ${age}` : "created date unknown";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function formatWorkspaceAge(createdAt: string, nowMs = Date.now()): string | undefined {
|
|
825
|
+
const createdAtMs = Date.parse(createdAt);
|
|
826
|
+
if (!Number.isFinite(createdAtMs)) return undefined;
|
|
827
|
+
|
|
828
|
+
const elapsedSeconds = Math.max(0, Math.floor((nowMs - createdAtMs) / 1000));
|
|
829
|
+
if (elapsedSeconds < 60) return "just now";
|
|
830
|
+
|
|
831
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
832
|
+
if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`;
|
|
833
|
+
|
|
834
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
835
|
+
if (elapsedHours < 48) return `${elapsedHours}h ago`;
|
|
836
|
+
|
|
837
|
+
const elapsedDays = Math.floor(elapsedHours / 24);
|
|
838
|
+
if (elapsedDays < 30) return `${elapsedDays}d ago`;
|
|
839
|
+
|
|
840
|
+
const elapsedMonths = Math.floor(elapsedDays / 30);
|
|
841
|
+
if (elapsedMonths < 24) return `${elapsedMonths}mo ago`;
|
|
842
|
+
|
|
843
|
+
return `${Math.floor(elapsedMonths / 12)}y ago`;
|
|
844
|
+
}
|