@catladder/cli 0.0.0-new-cli-a65ec262 → 0.0.0-new-cli-52525b58

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 CHANGED
@@ -53,7 +53,7 @@
53
53
  }
54
54
  ],
55
55
  "license": "MIT",
56
- "version": "0.0.0-new-cli-a65ec262",
56
+ "version": "0.0.0-new-cli-52525b58",
57
57
  "scripts": {
58
58
  "lint": "eslint \"src/**/*.ts\"",
59
59
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
package/src/completion.ts CHANGED
@@ -16,27 +16,25 @@ export async function getCompletions(
16
16
  words: string[],
17
17
  cursorWord: string,
18
18
  ): Promise<string[]> {
19
- // Try to match words against command names (which can be multi-word)
20
- // Walk through words, consuming them as command name parts until we find
21
- // an exact command match or need to suggest the next subcommand level.
19
+ // Try to find a command by matching the longest prefix of words.
20
+ // E.g., for words ["project", "cloudsql", "restore-db", "--source"],
21
+ // we match "project cloudsql restore-db" and treat ["--source"] as args.
22
+ const matched = findCommand(allCommands, words);
22
23
 
23
- // Find all commands whose name starts with the words typed so far
24
- const typed = words.join(" ");
24
+ if (matched) {
25
+ const { command, argsAfterCommand } = matched;
25
26
 
26
- // Exact match: we've typed a full command name, now complete its inputs
27
- const exactCommand = allCommands.find((c) => c.name === typed);
28
- if (exactCommand) {
29
- // Check if cursor is a --flag
27
+ // Check if cursor is a --flag name
30
28
  if (cursorWord.startsWith("--")) {
31
- return getFlagCompletions(exactCommand, cursorWord);
29
+ return getFlagCompletions(command, cursorWord);
32
30
  }
33
31
 
34
32
  // Check if previous word was a --flag (complete its value)
35
- const prevWord = words[words.length - 1];
33
+ const prevWord = argsAfterCommand[argsAfterCommand.length - 1];
36
34
  if (prevWord?.startsWith("--")) {
37
35
  const flagName = prevWord.replace(/^--/, "");
38
36
  const inputName = flagNameToInputName(flagName);
39
- const inputDef = exactCommand.inputs[inputName];
37
+ const inputDef = command.inputs[inputName];
40
38
  if (inputDef && "choices" in inputDef && inputDef.choices) {
41
39
  return resolveChoicesForCompletion(inputDef, cursorWord);
42
40
  }
@@ -44,9 +42,7 @@ export async function getCompletions(
44
42
  }
45
43
 
46
44
  // Complete positional inputs
47
- const positionalInputs = getPositionalInputs(exactCommand);
48
- // Count non-flag args after the command name
49
- const argsAfterCommand = words.slice(exactCommand.name.split(" ").length);
45
+ const positionalInputs = getPositionalInputs(command);
50
46
  const positionalArgs = argsAfterCommand.filter((w) => !w.startsWith("--"));
51
47
  const positionalIndex = positionalArgs.length;
52
48
  const targetInput = positionalInputs[positionalIndex];
@@ -60,15 +56,13 @@ export async function getCompletions(
60
56
  return [];
61
57
  }
62
58
 
63
- // No exact match. Complete the next level of subcommand names.
64
- // The prefix is what's been typed so far (words joined with space).
59
+ // No command matched. Complete subcommand names at the current level.
60
+ const typed = words.join(" ");
65
61
  const prefix = typed ? typed + " " : "";
66
62
 
67
- // Find unique next-level subcommand names
68
63
  const nextParts = new Set<string>();
69
64
  for (const cmd of allCommands) {
70
65
  if (cmd.name.startsWith(prefix)) {
71
- // Get the next word after the prefix
72
66
  const rest = cmd.name.slice(prefix.length);
73
67
  const nextPart = rest.split(" ")[0];
74
68
  if (nextPart && nextPart.startsWith(cursorWord)) {
@@ -80,6 +74,25 @@ export async function getCompletions(
80
74
  return [...nextParts].sort();
81
75
  }
82
76
 
77
+ /**
78
+ * Find a command by matching the longest prefix of words against command names.
79
+ * Returns the matched command and the remaining words (args/flags after the command name).
80
+ */
81
+ function findCommand(
82
+ allCommands: CommandDef[],
83
+ words: string[],
84
+ ): { command: CommandDef; argsAfterCommand: string[] } | null {
85
+ // Try longest match first
86
+ for (let i = words.length; i >= 1; i--) {
87
+ const candidate = words.slice(0, i).join(" ");
88
+ const command = allCommands.find((c) => c.name === candidate);
89
+ if (command) {
90
+ return { command, argsAfterCommand: words.slice(i) };
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
83
96
  function getPositionalInputs(command: CommandDef): [string, InputDef][] {
84
97
  return Object.entries(command.inputs).filter(([, def]) => def.positional);
85
98
  }