From 3440a070f1fe8611461b79364320da1582bc1607 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 31 Dec 2024 00:00:58 +0330 Subject: [PATCH 01/22] init --- citty.ts | 172 -------------------------- demo.citty.ts | 244 ++++++++++++++++++++----------------- src/citty.ts | 121 ++++++++++++++++++ src/index.ts | 126 +++++++++++++++++++ shared.ts => src/shared.ts | 0 5 files changed, 376 insertions(+), 287 deletions(-) delete mode 100644 citty.ts create mode 100644 src/citty.ts create mode 100644 src/index.ts rename shared.ts => src/shared.ts (100%) diff --git a/citty.ts b/citty.ts deleted file mode 100644 index 4219475..0000000 --- a/citty.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { defineCommand } from "citty"; -import * as zsh from "./zsh"; -import * as bash from "./bash"; -import * as fish from "./fish"; -import * as powershell from "./powershell"; -import { - flagMap, - Positional, - positionalMap, - ShellCompDirective, -} from "./shared"; - -function quoteIfNeeded(path) { - return path.includes(" ") ? `'${path}'` : path; -} - -const execPath = process.execPath; -const processArgs = process.argv.slice(1); -const quotedExecPath = quoteIfNeeded(execPath); -const quotedProcessArgs = processArgs.map(quoteIfNeeded); -const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); -const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; - -export default function tab(mainCommand) { - mainCommand.subCommands["complete"] = defineCommand({ - meta: { - name: "complete", - description: "Generate shell completion scripts", - }, - args: { - shell: { - type: "positional", - required: false, - description: "Specify shell type", - }, - }, - async run(ctx) { - let shell: string | undefined = ctx.args.shell; - if (shell?.startsWith("--")) { - shell = undefined; - } - - const extra = ctx.args._ || []; - - switch (shell) { - case "zsh": { - const script = zsh.generate(mainCommand.meta.name, x); - console.log(script); - break; - } - case "bash": { - const script = bash.generate(mainCommand.meta.name, x); - console.log(script); - break; - } - case "fish": { - const script = fish.generate(mainCommand.meta.name, x); - console.log(script); - break; - } - case "powershell": { - const script = powershell.generate(mainCommand.meta.name, x); - console.log(script); - break; - } - default: { - const args = extra; - let directive = ShellCompDirective.ShellCompDirectiveDefault; - const completions: string[] = []; - const endsWithSpace = args[args.length - 1] === ""; - - if (endsWithSpace) args.pop(); - let toComplete = args[args.length - 1] || ""; - const previousArgs = args.slice(0, -1); - - let matchedCommand = mainCommand; - - if (previousArgs.length > 0) { - const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg.startsWith("--")) { - const flagCompletion = flagMap.get(lastPrevArg); - if (flagCompletion) { - const flagSuggestions = await flagCompletion(previousArgs, toComplete); - completions.push( - ...flagSuggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - completions.forEach((comp) => console.log(comp)); - console.log(`:${directive}`); - return; - } - } - } - - if (toComplete.startsWith("--")) { - if (toComplete === "--") { - const allFlags = [...flagMap.keys()]; - const specifiedFlags = previousArgs.filter(arg => arg.startsWith('--')); - const availableFlags = allFlags.filter(flag => !specifiedFlags.includes(flag)); - - completions.push( - ...availableFlags.map( - (flag) => - `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` - ) - ); - } else { - const flagNamePartial = toComplete.slice(2); - const flagKeyPartial = `--${flagNamePartial}`; - - if (flagMap.has(toComplete)) { - const flagCompletion = flagMap.get(toComplete); - if (flagCompletion) { - const flagSuggestions = await flagCompletion(previousArgs, ""); - completions.push( - ...flagSuggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - } - } else { - const matchingFlags = [...flagMap.keys()].filter((flag) => - flag.startsWith(flagKeyPartial) - ); - - completions.push( - ...matchingFlags.map( - (flag) => - `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` - ) - ); - } - } - - completions.forEach((comp) => console.log(comp)); - console.log(`:${directive}`); - return; - } - - if (previousArgs.length === 0) { - completions.push( - ...Object.keys(mainCommand.subCommands || {}) - .filter((cmd) => cmd !== "complete") - .map( - (cmd) => - `${cmd}\t${mainCommand.subCommands[cmd]?.meta.description ?? ""}` - ) - ); - } else { - const positionalCompletions = - positionalMap.get(matchedCommand.meta.name) || []; - for (const positional of positionalCompletions) { - const suggestions = await positional.completion( - previousArgs, - toComplete - ); - completions.push( - ...suggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - } - } - - completions.forEach((comp) => console.log(comp)); - console.log(`:${directive}`); - } - } - }, - }); -} diff --git a/demo.citty.ts b/demo.citty.ts index 4ff284d..7ca80ab 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -1,132 +1,146 @@ -import { defineCommand, createMain } from "citty"; -import tab from "./citty"; -import { flagMap, positionalMap } from "./shared"; +import { defineCommand, createMain, CommandDef, CommandMeta, ArgDef, PositionalArgDef } from "citty"; +import { flagMap, positionalMap } from "./src/shared"; +// import {tab} from "" -const main = defineCommand({ - meta: { - name: "vite", - description: "Vite CLI tool", - }, - args: { - config: { type: "string", description: "Use specified config file", alias: "c" }, - base: { type: "string", description: "Public base path (default: /)" }, - logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, - clearScreen: { type: "boolean", description: "Allow/disable clear screen when logging" }, - debug: { type: "string", description: "Show debug logs", alias: "d" }, - filter: { type: "string", description: "Filter debug logs", alias: "f" }, - mode: { type: "string", description: "Set env mode", alias: "m" }, - }, - run(ctx) { - const firstArg = ctx.args._?.[0]; - if (firstArg && devCommand?.run) { - devCommand.run({ ...ctx, args: { ...ctx.args, root: firstArg } }); +export const main = defineCommand({ + meta: { + name: "vite", + description: "Vite CLI tool", + }, + args: { + config: { type: "string", description: "Use specified config file", alias: "c" }, + base: { type: "string", description: "Public base path (default: /)" }, + logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, + clearScreen: { type: "boolean", description: "Allow/disable clear screen when logging" }, + debug: { type: "string", description: "Show debug logs", alias: "d" }, + filter: { type: "string", description: "Filter debug logs", alias: "f" }, + mode: { type: "string", description: "Set env mode", alias: "m" }, + }, + run(ctx) { + const firstArg = ctx.args._?.[0]; + if (firstArg && devCommand?.run) { + devCommand.run({ + rawArgs: ctx.rawArgs, + cmd: devCommand, + subCommand: undefined, + data: ctx.data, + args: { + root: firstArg, + host: "", + port: "", + open: false, + cors: false, + strictPort: false, + force: false, + _: ctx.args._, + }, + }); + } } - }, + }); const devCommand = defineCommand({ - meta: { - name: "dev", - description: "Start dev server", - }, - args: { - root: { type: "positional", description: "Root directory", default: "." }, - host: { type: "string", description: "Specify hostname" }, - port: { type: "string", description: "Specify port" }, - open: { type: "boolean", description: "Open browser on startup" }, - cors: { type: "boolean", description: "Enable CORS" }, - strictPort: { type: "boolean", description: "Exit if specified port is already in use" }, - force: { type: "boolean", description: "Force the optimizer to ignore the cache and re-bundle" }, - }, - run({ args }) { - const { root, port, ...options } = args; - const parsedPort = port ? parseInt(port, 10) : undefined; - - if (!root || root === ".") { - console.log("Suggested root directories:"); - console.log("- src/"); - console.log("- ./"); - } - }, + meta: { + name: "dev", + description: "Start dev server", + }, + args: { + root: { type: "positional", description: "Root directory", default: "." }, + host: { type: "string", description: "Specify hostname" }, + port: { type: "string", description: "Specify port" }, + open: { type: "boolean", description: "Open browser on startup" }, + cors: { type: "boolean", description: "Enable CORS" }, + strictPort: { type: "boolean", description: "Exit if specified port is already in use" }, + force: { type: "boolean", description: "Force the optimizer to ignore the cache and re-bundle" }, + }, + run({ args }) { }, }); -main.subCommands = { dev: devCommand }; +main.subCommands = { + dev: devCommand +} as Record>; -for (const command of [main, ...Object.values(main.subCommands)]) { - const commandName = command.meta.name; +for (const command of [main as CommandDef, ...Object.values(main.subCommands as Record>)]) { + const meta = command.meta as CommandMeta; + const commandName = meta.name; - for (const [argName, argConfig] of Object.entries(command.args || {})) { - const optionKey = `--${argName}`; + for (const [argName, argConfig] of Object.entries(command.args || {}) as [string, ArgDef][]) { + const optionKey = `--${argName}`; - if (argName === "port") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "3000", description: "Development server port" }, - { action: "8080", description: "Alternative port" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else if (argName === "host") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "localhost", description: "Localhost" }, - { action: "0.0.0.0", description: "All interfaces" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else if (argName === "config") { - flagMap.set(optionKey, async (_, toComplete) => { - const configFiles = ["vite.config.ts", "vite.config.js"].filter( - (file) => file.startsWith(toComplete) - ); - return configFiles.map((file) => ({ action: file })); - }); - } else if (argName === "mode") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "development", description: "Development mode" }, - { action: "production", description: "Production mode" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else { - flagMap.set(optionKey, async (_, toComplete) => { - const flag = optionKey.startsWith("--") ? optionKey.slice(2) : optionKey; - if (!toComplete || optionKey.startsWith(toComplete)) { - return [{ action: optionKey, description: argConfig.description }]; + if (argName === "port") { + flagMap.set(optionKey, async (_, toComplete) => { + const options = [ + { action: "3000", description: "Development server port" }, + { action: "8080", description: "Alternative port" }, + ]; + return toComplete + ? options.filter(comp => comp.action.startsWith(toComplete)) + : options; + }); + } else if (argName === "host") { + flagMap.set(optionKey, async (_, toComplete) => { + const options = [ + { action: "localhost", description: "Localhost" }, + { action: "0.0.0.0", description: "All interfaces" }, + ]; + return toComplete + ? options.filter(comp => comp.action.startsWith(toComplete)) + : options; + }); + } else if (argName === "config") { + flagMap.set(optionKey, async (_, toComplete) => { + const configFiles = ["vite.config.ts", "vite.config.js"].filter( + (file) => file.startsWith(toComplete) + ); + return configFiles.map((file) => ({ action: file })); + }); + } else if (argName === "mode") { + flagMap.set(optionKey, async (_, toComplete) => { + const options = [ + { action: "development", description: "Development mode" }, + { action: "production", description: "Production mode" }, + ]; + return toComplete + ? options.filter(comp => comp.action.startsWith(toComplete)) + : options; + }); + } else { + flagMap.set(optionKey, async (_, toComplete) => { + const flag = optionKey.startsWith("--") ? optionKey.slice(2) : optionKey; + if (!toComplete || optionKey.startsWith(toComplete)) { + return [{ action: optionKey, description: argConfig.description }]; + } + return []; + }); } - return []; - }); } - } - if (command.args) { - const positionals = Object.entries(command.args) - .filter(([, config]) => config.type === "positional") - .map(([argName, argConfig]) => ({ - value: argName, - required: !!argConfig.required, - completion: async (_, toComplete) => { - const options = [ - { action: "src/", description: "Source directory" }, - { action: "./", description: "Current directory" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }, - })); - positionalMap.set(commandName, positionals); - } + if (command.args) { + const positionals = Object.entries(command.args) + .filter(([, config]) => (config as any).type === "positional") + .map(([argName, argConfig]) => { + const conf = argConfig as PositionalArgDef; + return { + value: argName, + variadic: false, + required: !!conf.required, + completion: async (_, toComplete) => { + const options = [ + { action: "src/", description: "Source directory" }, + { action: "./", description: "Current directory" }, + ]; + return toComplete + ? options.filter(comp => comp.action.startsWith(toComplete)) + : options; + }, + }; + }); + positionalMap.set(commandName!, positionals); + } } -tab(main); -const cli = createMain(main); +export const cli = createMain(main); +// tab(main) cli(); diff --git a/src/citty.ts b/src/citty.ts new file mode 100644 index 0000000..dd16854 --- /dev/null +++ b/src/citty.ts @@ -0,0 +1,121 @@ +import { defineCommand } from "citty"; +import * as zsh from "../zsh"; +import * as bash from "../bash"; +import * as fish from "../fish"; +import * as powershell from "../powershell"; +import { Completion } from "."; +import { CommandDef, PositionalArgDef } from "citty"; + +function quoteIfNeeded(path) { + return path.includes(" ") ? `'${path}'` : path; +} + +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; +const completion = new Completion(); + +export default async function tab(instance: CommandDef) { + const meta = + typeof instance.meta === "function" ? await instance.meta() : await instance.meta; + + if (!meta) { + throw new Error() + } + const name = meta.name + if (!name) { + throw new Error() + } + + const subCommands = typeof instance.subCommands === "function" ? await instance.subCommands() : await instance.subCommands + if (!subCommands) { + throw new Error() + } + + completion.addCommand(meta.name!, meta?.description ?? "", () => { }); + // Object.values(subCommands).forEach((cmd) => { + //TODO: resolver function + for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { + const config = typeof resolvableConfig === "function" ? await resolvableConfig() : await resolvableConfig + completion.addCommand(cmd, config.meta?.description, config.run); + if (cmd.args) { + for (const [argName, argConfig] of Object.entries(cmd.args)) { + const conf = argConfig; + completion.addOption( + cmd.meta.name, + `--${argName}`, + conf.description ?? "", + () => { } + ); + } + } + } + // }); + + if (instance.args) { + for (const [argName, argConfig] of Object.entries(instance.args)) { + const conf = argConfig as PositionalArgDef; + completion.addOption( + meta.name!, + `--${argName}`, + conf.description ?? "", + () => { } + ); + } + } + + subCommands["complete"] = defineCommand({ + meta: { + name: "complete", + description: "Generate shell completion scripts", + }, + args: { + shell: { + type: "positional", + required: false, + description: "Specify shell type", + }, + }, + async run(ctx) { + let shell: string | undefined = ctx.args.shell; + if (shell?.startsWith("--")) { + shell = undefined; + } + + const extra = ctx.args._ || []; + + switch (shell) { + case "zsh": { + const script = zsh.generate(name, x); + console.log(script); + break; + } + case "bash": { + const script = bash.generate(name, x); + console.log(script); + break; + } + case "fish": { + const script = fish.generate(name, x); + console.log(script); + break; + } + case "powershell": { + const script = powershell.generate(name, x); + console.log(script); + break; + } + default: { + const extra = ctx.args._ || []; + // We simply call parse (which prints completions directly) + await completion.parse(extra, instance); + // parse() does its own console.logs, so no return object to destructure + return; + } + } + }, + }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e840953 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,126 @@ +import { flagMap, positionalMap, ShellCompDirective } from "./shared"; + +export class Completion { + private commands = new Map(); + + addCommand(name: string, description: string, handler: Function) { + this.commands.set(name, { description, handler, options: {} }); + } + + addOption(commandName: string, optionName: string, description: string, handler: Function) { + const cmd = this.commands.get(commandName); + if (!cmd) { + throw new Error(`Command ${commandName} not found.`); + } + cmd.options[optionName] = { description, handler }; + } + + async parse(args: string[], mainCommand: any) { + let directive = ShellCompDirective.ShellCompDirectiveDefault; + const completions: string[] = []; + + const endsWithSpace = args[args.length - 1] === ""; + if (endsWithSpace) { + args.pop(); + } + + let toComplete = args[args.length - 1] || ""; + const previousArgs = args.slice(0, -1); + + let matchedCommand = mainCommand; + + if (previousArgs.length > 0) { + const lastPrevArg = previousArgs[previousArgs.length - 1]; + if (lastPrevArg.startsWith("--")) { + const flagCompletion = flagMap.get(lastPrevArg); + if (flagCompletion) { + const flagSuggestions = await flagCompletion(previousArgs, toComplete); + + completions.push( + ...flagSuggestions.map( + (comp) => `${comp.action}\t${comp.description ?? ""}` + ) + ); + + completions.forEach((comp) => console.log(comp)); + console.log(`:${directive}`); + + return; + } + } + } + + if (toComplete.startsWith("--")) { + if (toComplete === "--") { + const allFlags = [...flagMap.keys()]; + const specifiedFlags = previousArgs.filter(arg => arg.startsWith("--")); + const availableFlags = allFlags.filter(flag => !specifiedFlags.includes(flag)); + + completions.push( + ...availableFlags.map( + (flag) => + `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` + ) + ); + } else { + const flagNamePartial = toComplete.slice(2); + const flagKeyPartial = `--${flagNamePartial}`; + + if (flagMap.has(toComplete)) { + const flagCompletion = flagMap.get(toComplete); + if (flagCompletion) { + const flagSuggestions = await flagCompletion(previousArgs, ""); + completions.push( + ...flagSuggestions.map( + (comp) => `${comp.action}\t${comp.description ?? ""}` + ) + ); + } + } else { + const matchingFlags = [...flagMap.keys()].filter((flag) => + flag.startsWith(flagKeyPartial) + ); + + completions.push( + ...matchingFlags.map( + (flag) => + `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` + ) + ); + } + } + + completions.forEach((comp) => console.log(comp)); + console.log(`:${directive}`); + return; + } + + // If user typed no flags yet (maybe we’re completing subcommands or positional) + if (previousArgs.length === 0) { + completions.push( + ...Object.keys(mainCommand.subCommands || {}) + .filter((cmd) => cmd !== "complete") + .map( + (cmd) => + `${cmd}\t${mainCommand.subCommands[cmd]?.meta.description ?? ""}` + ) + ); + } else { + // complete positional arguments + const positionalCompletions = + positionalMap.get(matchedCommand.meta.name) || []; + + for (const positional of positionalCompletions) { + const suggestions = await positional.completion(previousArgs, toComplete); + completions.push( + ...suggestions.map( + (comp) => `${comp.action}\t${comp.description ?? ""}` + ) + ); + } + } + + completions.forEach((comp) => console.log(comp)); + console.log(`:${directive}`); + } +} diff --git a/shared.ts b/src/shared.ts similarity index 100% rename from shared.ts rename to src/shared.ts From 3cbe2960f8e5dda9ae7e61ab4c99629dfcdb5ab5 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Fri, 3 Jan 2025 23:01:58 +0330 Subject: [PATCH 02/22] types --- src/citty.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/citty.ts b/src/citty.ts index dd16854..0a1a428 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -1,4 +1,4 @@ -import { defineCommand } from "citty"; +import { ArgDef, defineCommand } from "citty"; import * as zsh from "../zsh"; import * as bash from "../bash"; import * as fish from "../fish"; @@ -40,12 +40,16 @@ export default async function tab(instance: CommandDef) { //TODO: resolver function for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { const config = typeof resolvableConfig === "function" ? await resolvableConfig() : await resolvableConfig - completion.addCommand(cmd, config.meta?.description, config.run); - if (cmd.args) { - for (const [argName, argConfig] of Object.entries(cmd.args)) { - const conf = argConfig; + const meta = typeof config.meta === "function" ? await config.meta() : await config.meta; + if (!meta || typeof meta?.description !== "string") { + throw new Error("Invalid meta or missing description."); + } + completion.addCommand(cmd, meta.description, config.run ?? (() => { })); + if (config.args) { + for (const [argName, argConfig] of Object.entries(config.args)) { + const conf = argConfig as ArgDef; completion.addOption( - cmd.meta.name, + meta.name!, `--${argName}`, conf.description ?? "", () => { } From 40498be2398264f267953aabf5a3ce6a9c4d46e0 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Fri, 3 Jan 2025 23:12:06 +0330 Subject: [PATCH 03/22] cleaner --- src/citty.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/citty.ts b/src/citty.ts index 0a1a428..25ab724 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -19,32 +19,33 @@ const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessA const completion = new Completion(); export default async function tab(instance: CommandDef) { - const meta = - typeof instance.meta === "function" ? await instance.meta() : await instance.meta; + const meta = await resolve(instance.meta); if (!meta) { - throw new Error() + throw new Error("Invalid meta."); } - const name = meta.name + const name = meta.name; if (!name) { - throw new Error() + throw new Error("Invalid meta or missing name."); } - const subCommands = typeof instance.subCommands === "function" ? await instance.subCommands() : await instance.subCommands + const subCommands = await resolve(instance.subCommands); if (!subCommands) { - throw new Error() + throw new Error("Invalid or missing subCommands."); } completion.addCommand(meta.name!, meta?.description ?? "", () => { }); - // Object.values(subCommands).forEach((cmd) => { - //TODO: resolver function + for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { - const config = typeof resolvableConfig === "function" ? await resolvableConfig() : await resolvableConfig - const meta = typeof config.meta === "function" ? await config.meta() : await config.meta; + const config = await resolve(resolvableConfig); + const meta = await resolve(config.meta); + if (!meta || typeof meta?.description !== "string") { throw new Error("Invalid meta or missing description."); } + completion.addCommand(cmd, meta.description, config.run ?? (() => { })); + if (config.args) { for (const [argName, argConfig] of Object.entries(config.args)) { const conf = argConfig as ArgDef; @@ -57,7 +58,6 @@ export default async function tab(instance: CommandDef) { } } } - // }); if (instance.args) { for (const [argName, argConfig] of Object.entries(instance.args)) { @@ -114,12 +114,14 @@ export default async function tab(instance: CommandDef) { } default: { const extra = ctx.args._ || []; - // We simply call parse (which prints completions directly) await completion.parse(extra, instance); - // parse() does its own console.logs, so no return object to destructure return; } } }, }); } + +async function resolve(resolvable: T | (() => T | Promise)): Promise { + return typeof resolvable === "function" ? await (resolvable as () => T | Promise)() : resolvable; +} \ No newline at end of file From 8caf3814c4bb5d22fb31529b20464d12ba08056f Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Sun, 12 Jan 2025 20:52:31 +0330 Subject: [PATCH 04/22] some progress, tests next --- demo.citty.ts | 246 +++--- package.json | 3 +- pnpm-lock.yaml | 1170 +++++++++++++++------------- bash.ts => src/bash.ts | 0 src/citty.ts | 120 +-- fish.ts => src/fish.ts | 0 src/index.ts | 224 ++++-- powershell.ts => src/powershell.ts | 2 +- src/shared.ts | 74 -- zsh.ts => src/zsh.ts | 2 +- tests/cli.test.ts | 11 + 11 files changed, 990 insertions(+), 862 deletions(-) rename bash.ts => src/bash.ts (100%) rename fish.ts => src/fish.ts (100%) rename powershell.ts => src/powershell.ts (97%) rename zsh.ts => src/zsh.ts (99%) diff --git a/demo.citty.ts b/demo.citty.ts index 7ca80ab..d2d9f14 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -1,43 +1,18 @@ import { defineCommand, createMain, CommandDef, CommandMeta, ArgDef, PositionalArgDef } from "citty"; -import { flagMap, positionalMap } from "./src/shared"; -// import {tab} from "" +import tab from "./src/citty"; -export const main = defineCommand({ +const main = defineCommand({ meta: { name: "vite", description: "Vite CLI tool", }, args: { config: { type: "string", description: "Use specified config file", alias: "c" }, - base: { type: "string", description: "Public base path (default: /)" }, - logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, - clearScreen: { type: "boolean", description: "Allow/disable clear screen when logging" }, - debug: { type: "string", description: "Show debug logs", alias: "d" }, - filter: { type: "string", description: "Filter debug logs", alias: "f" }, mode: { type: "string", description: "Set env mode", alias: "m" }, + logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, }, run(ctx) { - const firstArg = ctx.args._?.[0]; - if (firstArg && devCommand?.run) { - devCommand.run({ - rawArgs: ctx.rawArgs, - cmd: devCommand, - subCommand: undefined, - data: ctx.data, - args: { - root: firstArg, - host: "", - port: "", - open: false, - cors: false, - strictPort: false, - force: false, - _: ctx.args._, - }, - }); - } } - }); const devCommand = defineCommand({ @@ -46,101 +21,156 @@ const devCommand = defineCommand({ description: "Start dev server", }, args: { - root: { type: "positional", description: "Root directory", default: "." }, host: { type: "string", description: "Specify hostname" }, port: { type: "string", description: "Specify port" }, - open: { type: "boolean", description: "Open browser on startup" }, - cors: { type: "boolean", description: "Enable CORS" }, - strictPort: { type: "boolean", description: "Exit if specified port is already in use" }, - force: { type: "boolean", description: "Force the optimizer to ignore the cache and re-bundle" }, }, run({ args }) { }, }); +devCommand.subCommands = { + build: defineCommand({ + meta: { + name: "build", + description: "Build project", + }, + run({ args }) { }, + }) +} + main.subCommands = { dev: devCommand } as Record>; -for (const command of [main as CommandDef, ...Object.values(main.subCommands as Record>)]) { - const meta = command.meta as CommandMeta; - const commandName = meta.name; +const completion = await tab(main) - for (const [argName, argConfig] of Object.entries(command.args || {}) as [string, ArgDef][]) { - const optionKey = `--${argName}`; +for (const command of completion.commands.values()) { - if (argName === "port") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "3000", description: "Development server port" }, - { action: "8080", description: "Alternative port" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else if (argName === "host") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "localhost", description: "Localhost" }, - { action: "0.0.0.0", description: "All interfaces" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else if (argName === "config") { - flagMap.set(optionKey, async (_, toComplete) => { - const configFiles = ["vite.config.ts", "vite.config.js"].filter( - (file) => file.startsWith(toComplete) - ); - return configFiles.map((file) => ({ action: file })); - }); - } else if (argName === "mode") { - flagMap.set(optionKey, async (_, toComplete) => { - const options = [ - { action: "development", description: "Development mode" }, - { action: "production", description: "Production mode" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }); - } else { - flagMap.set(optionKey, async (_, toComplete) => { - const flag = optionKey.startsWith("--") ? optionKey.slice(2) : optionKey; - if (!toComplete || optionKey.startsWith(toComplete)) { - return [{ action: optionKey, description: argConfig.description }]; - } - return []; - }); + for (const [o, config] of command.options.entries()) { + if (o === "--port") { + config.handler = () => { + return [ + { value: "3000", description: "Development server port" }, + { value: "8080", description: "Alternative port" }, + ] + } + } + if (o === "--host") { + config.handler = () => { + return [ + { value: "localhost", description: "Localhost" }, + { value: "0.0.0.0", description: "All interfaces" }, + ] + } + } + if (o === "--config") { + config.handler = () => { + return [ + { value: "vite.config.ts", description: "Vite config file" }, + { value: "vite.config.js", description: "Vite config file" }, + ] + } + } + if (o === "--mode") { + config.handler = () => { + return [ + { value: "development", description: "Development mode" }, + { value: "production", description: "Production mode" }, + ] + } + } + if (o === "--logLevel") { + config.handler = () => { + return [ + { value: "info", description: "Info level" }, + { value: "warn", description: "Warn level" }, + { value: "error", description: "Error level" }, + { value: "silent", description: "Silent level" }, + ] + } } - } - - if (command.args) { - const positionals = Object.entries(command.args) - .filter(([, config]) => (config as any).type === "positional") - .map(([argName, argConfig]) => { - const conf = argConfig as PositionalArgDef; - return { - value: argName, - variadic: false, - required: !!conf.required, - completion: async (_, toComplete) => { - const options = [ - { action: "src/", description: "Source directory" }, - { action: "./", description: "Current directory" }, - ]; - return toComplete - ? options.filter(comp => comp.action.startsWith(toComplete)) - : options; - }, - }; - }); - positionalMap.set(commandName!, positionals); } } +// for (const command of [main as CommandDef, ...Object.values(main.subCommands as Record>)]) { +// const meta = command.meta as CommandMeta; +// const commandName = meta.name; + +// for (const [argName, argConfig] of Object.entries(command.args || {}) as [string, ArgDef][]) { +// const optionKey = `--${argName}`; + +// if (argName === "port") { +// flagMap.set(optionKey, async (_, toComplete) => { +// const options = [ +// { action: "3000", description: "Development server port" }, +// { action: "8080", description: "Alternative port" }, +// ]; +// return toComplete +// ? options.filter(comp => comp.action.startsWith(toComplete)) +// : options; +// }); +// } else if (argName === "host") { +// flagMap.set(optionKey, async (_, toComplete) => { +// const options = [ +// { action: "localhost", description: "Localhost" }, +// { action: "0.0.0.0", description: "All interfaces" }, +// ]; +// return toComplete +// ? options.filter(comp => comp.action.startsWith(toComplete)) +// : options; +// }); +// } else if (argName === "config") { +// flagMap.set(optionKey, async (_, toComplete) => { +// const configFiles = ["vite.config.ts", "vite.config.js"].filter( +// (file) => file.startsWith(toComplete) +// ); +// return configFiles.map((file) => ({ action: file })); +// }); +// } else if (argName === "mode") { +// flagMap.set(optionKey, async (_, toComplete) => { +// const options = [ +// { action: "development", description: "Development mode" }, +// { action: "production", description: "Production mode" }, +// ]; +// return toComplete +// ? options.filter(comp => comp.action.startsWith(toComplete)) +// : options; +// }); +// } else { +// flagMap.set(optionKey, async (_, toComplete) => { +// const flag = optionKey.startsWith("--") ? optionKey.slice(2) : optionKey; +// if (!toComplete || optionKey.startsWith(toComplete)) { +// return [{ action: optionKey, description: argConfig.description }]; +// } +// return []; +// }); +// } +// } + +// if (command.args) { +// const positionals = Object.entries(command.args) +// .filter(([, config]) => (config as any).type === "positional") +// .map(([argName, argConfig]) => { +// const conf = argConfig as PositionalArgDef; +// return { +// value: argName, +// variadic: false, +// required: !!conf.required, +// completion: async (_, toComplete) => { +// const options = [ +// { action: "src/", description: "Source directory" }, +// { action: "./", description: "Current directory" }, +// ]; +// return toComplete +// ? options.filter(comp => comp.action.startsWith(toComplete)) +// : options; +// }, +// }; +// }); +// positionalMap.set(commandName!, positionals); +// } +// } + + +const cli = createMain(main); -export const cli = createMain(main); -// tab(main) cli(); diff --git a/package.json b/package.json index bb583ff..8cdf2e8 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,6 @@ }, "dependencies": { "mri": "^1.2.0" - } + }, + "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a12b2b..4a5dd87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,624 +1,420 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - mri: - specifier: ^1.2.0 - version: 1.2.0 - -devDependencies: - '@types/node': - specifier: ^22.7.4 - version: 22.7.5 - cac: - specifier: ^6.7.14 - version: 6.7.14 - citty: - specifier: ^0.1.6 - version: 0.1.6 - tsx: - specifier: ^4.19.1 - version: 4.19.1 - vitest: - specifier: ^2.1.3 - version: 2.1.3(@types/node@22.7.5) +importers: + + .: + dependencies: + mri: + specifier: ^1.2.0 + version: 1.2.0 + devDependencies: + '@types/node': + specifier: ^22.7.4 + version: 22.7.5 + cac: + specifier: ^6.7.14 + version: 6.7.14 + citty: + specifier: ^0.1.6 + version: 0.1.6 + tsx: + specifier: ^4.19.1 + version: 4.19.1 + vitest: + specifier: ^2.1.3 + version: 2.1.3(@types/node@22.7.5) packages: - /@esbuild/aix-ppc64@0.21.5: + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - requiresBuild: true - dev: true - optional: true - /@esbuild/aix-ppc64@0.23.1: + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.21.5: + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.23.1: + '@esbuild/android-arm64@0.23.1': resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.21.5: + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.23.1: + '@esbuild/android-arm@0.23.1': resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.21.5: + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.23.1: + '@esbuild/android-x64@0.23.1': resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} engines: {node: '>=18'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.21.5: + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.23.1: + '@esbuild/darwin-arm64@0.23.1': resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.21.5: + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.23.1: + '@esbuild/darwin-x64@0.23.1': resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.21.5: + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.23.1: + '@esbuild/freebsd-arm64@0.23.1': resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.21.5: + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.23.1: + '@esbuild/freebsd-x64@0.23.1': resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.21.5: + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.23.1: + '@esbuild/linux-arm64@0.23.1': resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.21.5: + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.23.1: + '@esbuild/linux-arm@0.23.1': resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.21.5: + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.23.1: + '@esbuild/linux-ia32@0.23.1': resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.21.5: + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.23.1: + '@esbuild/linux-loong64@0.23.1': resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.21.5: + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.23.1: + '@esbuild/linux-mips64el@0.23.1': resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.21.5: + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.23.1: + '@esbuild/linux-ppc64@0.23.1': resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.21.5: + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.23.1: + '@esbuild/linux-riscv64@0.23.1': resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.21.5: + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.23.1: + '@esbuild/linux-s390x@0.23.1': resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.21.5: + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.23.1: + '@esbuild/linux-x64@0.23.1': resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.21.5: + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.23.1: + '@esbuild/netbsd-x64@0.23.1': resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-arm64@0.23.1: + '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.21.5: + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.23.1: + '@esbuild/openbsd-x64@0.23.1': resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.21.5: + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.23.1: + '@esbuild/sunos-x64@0.23.1': resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.21.5: + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.23.1: + '@esbuild/win32-arm64@0.23.1': resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.21.5: + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.23.1: + '@esbuild/win32-ia32@0.23.1': resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.21.5: + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.23.1: + '@esbuild/win32-x64@0.23.1': resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@jridgewell/sourcemap-codec@1.5.0: + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - dev: true - /@rollup/rollup-android-arm-eabi@4.24.2: + '@rollup/rollup-android-arm-eabi@4.24.2': resolution: {integrity: sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-android-arm64@4.24.2: + '@rollup/rollup-android-arm64@4.24.2': resolution: {integrity: sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-darwin-arm64@4.24.2: + '@rollup/rollup-darwin-arm64@4.24.2': resolution: {integrity: sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-darwin-x64@4.24.2: + '@rollup/rollup-darwin-x64@4.24.2': resolution: {integrity: sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-freebsd-arm64@4.24.2: + '@rollup/rollup-freebsd-arm64@4.24.2': resolution: {integrity: sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-freebsd-x64@4.24.2: + '@rollup/rollup-freebsd-x64@4.24.2': resolution: {integrity: sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.24.2: + '@rollup/rollup-linux-arm-gnueabihf@4.24.2': resolution: {integrity: sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm-musleabihf@4.24.2: + '@rollup/rollup-linux-arm-musleabihf@4.24.2': resolution: {integrity: sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm64-gnu@4.24.2: + '@rollup/rollup-linux-arm64-gnu@4.24.2': resolution: {integrity: sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-arm64-musl@4.24.2: + '@rollup/rollup-linux-arm64-musl@4.24.2': resolution: {integrity: sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.24.2: + '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': resolution: {integrity: sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-riscv64-gnu@4.24.2: + '@rollup/rollup-linux-riscv64-gnu@4.24.2': resolution: {integrity: sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-s390x-gnu@4.24.2: + '@rollup/rollup-linux-s390x-gnu@4.24.2': resolution: {integrity: sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-x64-gnu@4.24.2: + '@rollup/rollup-linux-x64-gnu@4.24.2': resolution: {integrity: sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-linux-x64-musl@4.24.2: + '@rollup/rollup-linux-x64-musl@4.24.2': resolution: {integrity: sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-arm64-msvc@4.24.2: + '@rollup/rollup-win32-arm64-msvc@4.24.2': resolution: {integrity: sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-ia32-msvc@4.24.2: + '@rollup/rollup-win32-ia32-msvc@4.24.2': resolution: {integrity: sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@rollup/rollup-win32-x64-msvc@4.24.2: + '@rollup/rollup-win32-x64-msvc@4.24.2': resolution: {integrity: sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@types/estree@1.0.6: + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - dev: true - /@types/node@22.7.5: + '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} - dependencies: - undici-types: 6.19.8 - dev: true - /@vitest/expect@2.1.3: + '@vitest/expect@2.1.3': resolution: {integrity: sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==} - dependencies: - '@vitest/spy': 2.1.3 - '@vitest/utils': 2.1.3 - chai: 5.1.2 - tinyrainbow: 1.2.0 - dev: true - /@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10): + '@vitest/mocker@2.1.3': resolution: {integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==} peerDependencies: '@vitest/spy': 2.1.3 @@ -629,86 +425,46 @@ packages: optional: true vite: optional: true - dependencies: - '@vitest/spy': 2.1.3 - estree-walker: 3.0.3 - magic-string: 0.30.12 - vite: 5.4.10(@types/node@22.7.5) - dev: true - /@vitest/pretty-format@2.1.3: + '@vitest/pretty-format@2.1.3': resolution: {integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==} - dependencies: - tinyrainbow: 1.2.0 - dev: true - /@vitest/runner@2.1.3: + '@vitest/runner@2.1.3': resolution: {integrity: sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==} - dependencies: - '@vitest/utils': 2.1.3 - pathe: 1.1.2 - dev: true - /@vitest/snapshot@2.1.3: + '@vitest/snapshot@2.1.3': resolution: {integrity: sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==} - dependencies: - '@vitest/pretty-format': 2.1.3 - magic-string: 0.30.12 - pathe: 1.1.2 - dev: true - /@vitest/spy@2.1.3: + '@vitest/spy@2.1.3': resolution: {integrity: sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==} - dependencies: - tinyspy: 3.0.2 - dev: true - /@vitest/utils@2.1.3: + '@vitest/utils@2.1.3': resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==} - dependencies: - '@vitest/pretty-format': 2.1.3 - loupe: 3.1.2 - tinyrainbow: 1.2.0 - dev: true - /assertion-error@2.0.1: + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - dev: true - /cac@6.7.14: + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - dev: true - /chai@5.1.2: + chai@5.1.2: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.1.2 - pathval: 2.0.0 - dev: true - /check-error@2.1.1: + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} - dev: true - /citty@0.1.6: + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - dependencies: - consola: 3.2.3 - dev: true - /consola@3.2.3: + consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} - dev: true - /debug@4.3.7: + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} peerDependencies: @@ -716,254 +472,117 @@ packages: peerDependenciesMeta: supports-color: optional: true - dependencies: - ms: 2.1.3 - dev: true - /deep-eql@5.0.2: + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - dev: true - /esbuild@0.21.5: + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - dev: true - /esbuild@0.23.1: + esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 - dev: true - /estree-walker@3.0.3: + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.6 - dev: true - /fsevents@2.3.3: + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - requiresBuild: true - dev: true - optional: true - /get-tsconfig@4.8.1: + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} - dependencies: - resolve-pkg-maps: 1.0.0 - dev: true - /loupe@3.1.2: + loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - dev: true - /magic-string@0.30.12: + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - dev: true - /mri@1.2.0: + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - dev: false - /ms@2.1.3: + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - /nanoid@3.3.7: + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true - /pathe@1.1.2: + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true - /pathval@2.0.0: + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - dev: true - /picocolors@1.1.1: + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - dev: true - /postcss@8.4.47: + postcss@8.4.47: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.1 - source-map-js: 1.2.1 - dev: true - /resolve-pkg-maps@1.0.0: + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true - /rollup@4.24.2: + rollup@4.24.2: resolution: {integrity: sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.24.2 - '@rollup/rollup-android-arm64': 4.24.2 - '@rollup/rollup-darwin-arm64': 4.24.2 - '@rollup/rollup-darwin-x64': 4.24.2 - '@rollup/rollup-freebsd-arm64': 4.24.2 - '@rollup/rollup-freebsd-x64': 4.24.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.24.2 - '@rollup/rollup-linux-arm-musleabihf': 4.24.2 - '@rollup/rollup-linux-arm64-gnu': 4.24.2 - '@rollup/rollup-linux-arm64-musl': 4.24.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.24.2 - '@rollup/rollup-linux-riscv64-gnu': 4.24.2 - '@rollup/rollup-linux-s390x-gnu': 4.24.2 - '@rollup/rollup-linux-x64-gnu': 4.24.2 - '@rollup/rollup-linux-x64-musl': 4.24.2 - '@rollup/rollup-win32-arm64-msvc': 4.24.2 - '@rollup/rollup-win32-ia32-msvc': 4.24.2 - '@rollup/rollup-win32-x64-msvc': 4.24.2 - fsevents: 2.3.3 - dev: true - /siginfo@2.0.0: + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - /source-map-js@1.2.1: + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - dev: true - /stackback@0.0.2: + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true - /std-env@3.7.0: + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} - dev: true - /tinybench@2.9.0: + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - dev: true - /tinyexec@0.3.1: + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} - dev: true - /tinypool@1.0.1: + tinypool@1.0.1: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} engines: {node: ^18.0.0 || >=20.0.0} - dev: true - /tinyrainbow@1.2.0: + tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - dev: true - /tinyspy@3.0.2: + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - dev: true - /tsx@4.19.1: + tsx@4.19.1: resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} hasBin: true - dependencies: - esbuild: 0.23.1 - get-tsconfig: 4.8.1 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /undici-types@6.19.8: + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - dev: true - /vite-node@2.1.3(@types/node@22.7.5): + vite-node@2.1.3: resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.7 - pathe: 1.1.2 - vite: 5.4.10(@types/node@22.7.5) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - /vite@5.4.10(@types/node@22.7.5): + vite@5.4.10: resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -993,16 +612,8 @@ packages: optional: true terser: optional: true - dependencies: - '@types/node': 22.7.5 - esbuild: 0.21.5 - postcss: 8.4.47 - rollup: 4.24.2 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vitest@2.1.3(@types/node@22.7.5): + vitest@2.1.3: resolution: {integrity: sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -1026,44 +637,483 @@ packages: optional: true jsdom: optional: true - dependencies: - '@types/node': 22.7.5 - '@vitest/expect': 2.1.3 - '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10) - '@vitest/pretty-format': 2.1.3 - '@vitest/runner': 2.1.3 - '@vitest/snapshot': 2.1.3 - '@vitest/spy': 2.1.3 - '@vitest/utils': 2.1.3 - chai: 5.1.2 - debug: 4.3.7 - magic-string: 0.30.12 - pathe: 1.1.2 - std-env: 3.7.0 - tinybench: 2.9.0 - tinyexec: 0.3.1 - tinypool: 1.0.1 - tinyrainbow: 1.2.0 - vite: 5.4.10(@types/node@22.7.5) - vite-node: 2.1.3(@types/node@22.7.5) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - /why-is-node-running@2.3.0: + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@rollup/rollup-android-arm-eabi@4.24.2': + optional: true + + '@rollup/rollup-android-arm64@4.24.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.24.2': + optional: true + + '@rollup/rollup-darwin-x64@4.24.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.24.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.24.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.24.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.24.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.24.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.24.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.24.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.24.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.24.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.24.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.24.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.24.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.24.2': + optional: true + + '@types/estree@1.0.6': {} + + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@vitest/expect@2.1.3': + dependencies: + '@vitest/spy': 2.1.3 + '@vitest/utils': 2.1.3 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10)': + dependencies: + '@vitest/spy': 2.1.3 + estree-walker: 3.0.3 + magic-string: 0.30.12 + vite: 5.4.10(@types/node@22.7.5) + + '@vitest/pretty-format@2.1.3': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.3': + dependencies: + '@vitest/utils': 2.1.3 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.3': + dependencies: + '@vitest/pretty-format': 2.1.3 + magic-string: 0.30.12 + pathe: 1.1.2 + + '@vitest/spy@2.1.3': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.3': + dependencies: + '@vitest/pretty-format': 2.1.3 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + + check-error@2.1.1: {} + + citty@0.1.6: + dependencies: + consola: 3.2.3 + + consola@3.2.3: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + loupe@3.1.2: {} + + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.7: {} + + pathe@1.1.2: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: {} + + rollup@4.24.2: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.24.2 + '@rollup/rollup-android-arm64': 4.24.2 + '@rollup/rollup-darwin-arm64': 4.24.2 + '@rollup/rollup-darwin-x64': 4.24.2 + '@rollup/rollup-freebsd-arm64': 4.24.2 + '@rollup/rollup-freebsd-x64': 4.24.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.2 + '@rollup/rollup-linux-arm-musleabihf': 4.24.2 + '@rollup/rollup-linux-arm64-gnu': 4.24.2 + '@rollup/rollup-linux-arm64-musl': 4.24.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.2 + '@rollup/rollup-linux-riscv64-gnu': 4.24.2 + '@rollup/rollup-linux-s390x-gnu': 4.24.2 + '@rollup/rollup-linux-x64-gnu': 4.24.2 + '@rollup/rollup-linux-x64-musl': 4.24.2 + '@rollup/rollup-win32-arm64-msvc': 4.24.2 + '@rollup/rollup-win32-ia32-msvc': 4.24.2 + '@rollup/rollup-win32-x64-msvc': 4.24.2 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.7.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.1: {} + + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tsx@4.19.1: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + undici-types@6.19.8: {} + + vite-node@2.1.3(@types/node@22.7.5): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + pathe: 1.1.2 + vite: 5.4.10(@types/node@22.7.5) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.10(@types/node@22.7.5): + dependencies: + '@types/node': 22.7.5 + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.24.2 + optionalDependencies: + fsevents: 2.3.3 + + vitest@2.1.3(@types/node@22.7.5): + dependencies: + '@types/node': 22.7.5 + '@vitest/expect': 2.1.3 + '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10) + '@vitest/pretty-format': 2.1.3 + '@vitest/runner': 2.1.3 + '@vitest/snapshot': 2.1.3 + '@vitest/spy': 2.1.3 + '@vitest/utils': 2.1.3 + chai: 5.1.2 + debug: 4.3.7 + magic-string: 0.30.12 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.10(@types/node@22.7.5) + vite-node: 2.1.3(@types/node@22.7.5) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - dev: true diff --git a/bash.ts b/src/bash.ts similarity index 100% rename from bash.ts rename to src/bash.ts diff --git a/src/citty.ts b/src/citty.ts index 25ab724..213535e 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -1,10 +1,10 @@ -import { ArgDef, defineCommand } from "citty"; -import * as zsh from "../zsh"; -import * as bash from "../bash"; -import * as fish from "../fish"; -import * as powershell from "../powershell"; +import { ArgDef, defineCommand, parseArgs } from "citty"; +import * as zsh from "./zsh"; +import * as bash from "./bash"; +import * as fish from "./fish"; +import * as powershell from "./powershell"; import { Completion } from "."; -import { CommandDef, PositionalArgDef } from "citty"; +import type { ArgsDef, CommandDef, PositionalArgDef } from "citty"; function quoteIfNeeded(path) { return path.includes(" ") ? `'${path}'` : path; @@ -16,26 +16,12 @@ const quotedExecPath = quoteIfNeeded(execPath); const quotedProcessArgs = processArgs.map(quoteIfNeeded); const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; -const completion = new Completion(); - -export default async function tab(instance: CommandDef) { - const meta = await resolve(instance.meta); - - if (!meta) { - throw new Error("Invalid meta."); - } - const name = meta.name; - if (!name) { - throw new Error("Invalid meta or missing name."); - } - - const subCommands = await resolve(instance.subCommands); - if (!subCommands) { - throw new Error("Invalid or missing subCommands."); - } - - completion.addCommand(meta.name!, meta?.description ?? "", () => { }); +async function handleSubCommands( + completion: Completion, + subCommands: Record, + parentCmd?: string +) { for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { const config = await resolve(resolvableConfig); const meta = await resolve(config.meta); @@ -44,53 +30,84 @@ export default async function tab(instance: CommandDef) { throw new Error("Invalid meta or missing description."); } - completion.addCommand(cmd, meta.description, config.run ?? (() => { })); + const name = completion.addCommand(cmd, meta.description, async (previousArgs, toComplete, endsWithSpace) => { + return [] + }, parentCmd); + + // Handle nested subcommands recursively + if (config.subCommands) { + await handleSubCommands(completion, config.subCommands, name); + } + // Handle arguments if (config.args) { for (const [argName, argConfig] of Object.entries(config.args)) { const conf = argConfig as ArgDef; completion.addOption( - meta.name!, + name, `--${argName}`, conf.description ?? "", - () => { } + async (previousArgs, toComplete, endsWithSpace) => { + return [] + } ); } } } +} + +export default async function tab(instance: CommandDef) { + const completion = new Completion(); + + const meta = await resolve(instance.meta); + + if (!meta) { + throw new Error("Invalid meta."); + } + const name = meta.name; + if (!name) { + throw new Error("Invalid meta or missing name."); + } + + const subCommands = await resolve(instance.subCommands); + if (!subCommands) { + throw new Error("Invalid or missing subCommands."); + } + + const root = '' + completion.addCommand(root, meta?.description ?? "", async (previousArgs, toComplete, endsWithSpace) => { + return [] + }); + + await handleSubCommands(completion, subCommands); if (instance.args) { for (const [argName, argConfig] of Object.entries(instance.args)) { const conf = argConfig as PositionalArgDef; completion.addOption( - meta.name!, + root, `--${argName}`, conf.description ?? "", - () => { } + async (previousArgs, toComplete, endsWithSpace) => { + return [] + } ); } } - subCommands["complete"] = defineCommand({ + const completeCommand = defineCommand({ meta: { name: "complete", description: "Generate shell completion scripts", }, - args: { - shell: { - type: "positional", - required: false, - description: "Specify shell type", - }, - }, async run(ctx) { - let shell: string | undefined = ctx.args.shell; - if (shell?.startsWith("--")) { + let shell: string | undefined = ctx.rawArgs[0]; + const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf("--") + 1); + + if (shell === '--') { shell = undefined; } - const extra = ctx.args._ || []; - switch (shell) { case "zsh": { const script = zsh.generate(name, x); @@ -113,15 +130,24 @@ export default async function tab(instance: CommandDef) { break; } default: { - const extra = ctx.args._ || []; - await completion.parse(extra, instance); - return; + console.log(completion) + const args = (await resolve(instance.args))!; + const parsed = parseArgs(extra, args); + // TODO: this is not ideal at all + const matchedCommand = parsed._.join(' ') + return completion.parse(extra, matchedCommand); } } }, }); + + subCommands.complete = completeCommand + + return completion } -async function resolve(resolvable: T | (() => T | Promise)): Promise { - return typeof resolvable === "function" ? await (resolvable as () => T | Promise)() : resolvable; +type Resolvable = T | Promise | (() => T) | (() => Promise); + +async function resolve(resolvable: Resolvable): Promise { + return resolvable instanceof Function ? await resolvable() : await resolvable; } \ No newline at end of file diff --git a/fish.ts b/src/fish.ts similarity index 100% rename from fish.ts rename to src/fish.ts diff --git a/src/index.ts b/src/index.ts index e840953..634e0bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,102 @@ -import { flagMap, positionalMap, ShellCompDirective } from "./shared"; +// ShellCompRequestCmd is the name of the hidden command that is used to request +// completion results from the program. It is used by the shell completion scripts. +export const ShellCompRequestCmd: string = "__complete"; + +// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request +// completion results without their description. It is used by the shell completion scripts. +export const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; + +// ShellCompDirective is a bit map representing the different behaviors the shell +// can be instructed to have once completions have been provided. +export const ShellCompDirective = { + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. + ShellCompDirectiveError: 1 << 0, + + // ShellCompDirectiveNoSpace indicates that the shell should not add a space + // after the completion even if there is a single completion provided. + ShellCompDirectiveNoSpace: 1 << 1, + + // ShellCompDirectiveNoFileComp indicates that the shell should not provide + // file completion even when no completion is provided. + ShellCompDirectiveNoFileComp: 1 << 2, + + // ShellCompDirectiveFilterFileExt indicates that the provided completions + // should be used as file extension filters. + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() + // is a shortcut to using this directive explicitly. The BashCompFilenameExt + // annotation can also be used to obtain the same behavior for flags. + ShellCompDirectiveFilterFileExt: 1 << 3, + + // ShellCompDirectiveFilterDirs indicates that only directory names should + // be provided in file completion. To request directory names within another + // directory, the returned completions should specify the directory within + // which to search. The BashCompSubdirsInDir annotation can be used to + // obtain the same behavior but only for flags. + ShellCompDirectiveFilterDirs: 1 << 4, + + // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order + // in which the completions are provided. + ShellCompDirectiveKeepOrder: 1 << 5, + + // =========================================================================== + + // All directives using iota (or equivalent in Go) should be above this one. + // For internal use. + shellCompDirectiveMaxValue: 1 << 6, + + // ShellCompDirectiveDefault indicates to let the shell perform its default + // behavior after completions have been provided. + // This one must be last to avoid messing up the iota count. + ShellCompDirectiveDefault: 0, +}; + + +export type Positional = { + required: boolean; + variadic: boolean; + completion: Handler; +}; + +type Items = { + description: string; + value: string; +} + +type Handler = (previousArgs: string[], toComplete: string, endsWithSpace: boolean) => (Items[] | Promise); + +type Option = { + description: string; + handler: Handler; +} + +type Command = { + description: string; + handler: Handler; + options: Map; + parent?: Command; +} export class Completion { - private commands = new Map(); + commands = new Map(); - addCommand(name: string, description: string, handler: Function) { - this.commands.set(name, { description, handler, options: {} }); + addCommand(name: string, description: string, handler: Handler, parent?: string) { + const key = parent ? `${parent} ${name}` : name + this.commands.set(key, { description, handler, options: new Map(), parent: parent ? this.commands.get(parent)! : this.commands.get('')! }); + return key } - addOption(commandName: string, optionName: string, description: string, handler: Function) { - const cmd = this.commands.get(commandName); + addOption(command: string, option: string, description: string, handler: Handler) { + const cmd = this.commands.get(command); if (!cmd) { - throw new Error(`Command ${commandName} not found.`); + throw new Error(`Command ${command} not found.`); } - cmd.options[optionName] = { description, handler }; + cmd.options.set(option, { description, handler }); + return option } - async parse(args: string[], mainCommand: any) { + async parse(args: string[], potentialCommand: string) { + console.log(potentialCommand) + const matchedCommand = this.commands.get(potentialCommand) ?? this.commands.get('')!; let directive = ShellCompDirective.ShellCompDirectiveDefault; const completions: string[] = []; @@ -27,96 +108,99 @@ export class Completion { let toComplete = args[args.length - 1] || ""; const previousArgs = args.slice(0, -1); - let matchedCommand = mainCommand; - + console.log('here', previousArgs) if (previousArgs.length > 0) { const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg.startsWith("--")) { - const flagCompletion = flagMap.get(lastPrevArg); - if (flagCompletion) { - const flagSuggestions = await flagCompletion(previousArgs, toComplete); - + if (lastPrevArg.startsWith("--") && endsWithSpace) { + console.log('here') + const {handler} = matchedCommand.options.get(lastPrevArg)!; + if (handler) { + const flagSuggestions = await handler(previousArgs, toComplete, endsWithSpace); completions.push( - ...flagSuggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` + ...flagSuggestions.filter(comp => comp.value.startsWith(toComplete)).map( + (comp) => `${comp.value}\t${comp.description ?? ""}` ) ); - + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; completions.forEach((comp) => console.log(comp)); console.log(`:${directive}`); - return; } } } if (toComplete.startsWith("--")) { - if (toComplete === "--") { - const allFlags = [...flagMap.keys()]; - const specifiedFlags = previousArgs.filter(arg => arg.startsWith("--")); - const availableFlags = allFlags.filter(flag => !specifiedFlags.includes(flag)); + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + const equalsIndex = toComplete.indexOf("="); + + if (equalsIndex !== -1) { + const flagName = toComplete.slice(2, equalsIndex); + const valueToComplete = toComplete.slice(equalsIndex + 1); + const {handler} = matchedCommand.options.get(`--${flagName}`)!; + + if (handler) { + const suggestions = await handler(previousArgs, valueToComplete, endsWithSpace); + completions.push(...suggestions.map( + (comp) => `${comp.value}\t${comp.description ?? ""}` + )); + } + } else if (!endsWithSpace) { + const options = new Map(matchedCommand.options); + + let currentCommand = matchedCommand; + while (currentCommand.parent) { + for (const [key, value] of currentCommand.parent.options) { + if (!options.has(key)) { + options.set(key, value); + } + } + currentCommand = currentCommand.parent; + } + + const specifiedFlags = previousArgs + .filter(arg => arg.startsWith("-")) + .filter(arg => arg.startsWith("--")); + const availableFlags = [...options.keys()] + .filter(flag => !specifiedFlags.includes(flag)) + .filter(flag => flag.startsWith(toComplete)); completions.push( ...availableFlags.map( - (flag) => - `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` + (flag) => `${flag}\t${options.get(flag)!.description ?? ""}` ) ); } else { - const flagNamePartial = toComplete.slice(2); - const flagKeyPartial = `--${flagNamePartial}`; - - if (flagMap.has(toComplete)) { - const flagCompletion = flagMap.get(toComplete); - if (flagCompletion) { - const flagSuggestions = await flagCompletion(previousArgs, ""); - completions.push( - ...flagSuggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - } - } else { - const matchingFlags = [...flagMap.keys()].filter((flag) => - flag.startsWith(flagKeyPartial) - ); - - completions.push( - ...matchingFlags.map( - (flag) => - `${flag}\t${matchedCommand.args[flag.slice(2)]?.description ?? "Option"}` - ) - ); + console.log('here3') + const {handler} = matchedCommand.options.get(toComplete)!; + console.log(handler, toComplete) + + if (handler) { + const suggestions = await handler(previousArgs, toComplete, endsWithSpace); + console.log(suggestions) + completions.push(...suggestions.map( + (comp) => `${comp.value}\t${comp.description ?? ""}` + )); } - } - completions.forEach((comp) => console.log(comp)); - console.log(`:${directive}`); - return; - } + } + } else if (!toComplete && endsWithSpace) { + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - // If user typed no flags yet (maybe we’re completing subcommands or positional) - if (previousArgs.length === 0) { completions.push( - ...Object.keys(mainCommand.subCommands || {}) + ...Object.keys(this.commands) .filter((cmd) => cmd !== "complete") - .map( - (cmd) => - `${cmd}\t${mainCommand.subCommands[cmd]?.meta.description ?? ""}` - ) + .map((cmd) => `${cmd}\t${this.commands[cmd].description}`) ); - } else { - // complete positional arguments - const positionalCompletions = - positionalMap.get(matchedCommand.meta.name) || []; + const positionalCompletions = positionalMap.get(potentialCommand) || []; for (const positional of positionalCompletions) { - const suggestions = await positional.completion(previousArgs, toComplete); + const suggestions = await positional.completion(previousArgs, ""); completions.push( - ...suggestions.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) + ...suggestions.map((comp) => `${comp.action}\t${comp.description ?? ""}`) ); + if (suggestions.length > 0) { + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + } } } diff --git a/powershell.ts b/src/powershell.ts similarity index 97% rename from powershell.ts rename to src/powershell.ts index 5dc70b4..22ca425 100644 --- a/powershell.ts +++ b/src/powershell.ts @@ -1,4 +1,4 @@ -import { ShellCompDirective } from "./shared"; +import { ShellCompDirective } from "./"; // TODO: issue with -- -- completions diff --git a/src/shared.ts b/src/shared.ts index 5925d23..8b13789 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,75 +1 @@ -// ShellCompRequestCmd is the name of the hidden command that is used to request -// completion results from the program. It is used by the shell completion scripts. -export const ShellCompRequestCmd: string = "__complete"; - -// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request -// completion results without their description. It is used by the shell completion scripts. -export const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; - -// ShellCompDirective is a bit map representing the different behaviors the shell -// can be instructed to have once completions have been provided. -export const ShellCompDirective = { - // ShellCompDirectiveError indicates an error occurred and completions should be ignored. - ShellCompDirectiveError: 1 << 0, - - // ShellCompDirectiveNoSpace indicates that the shell should not add a space - // after the completion even if there is a single completion provided. - ShellCompDirectiveNoSpace: 1 << 1, - - // ShellCompDirectiveNoFileComp indicates that the shell should not provide - // file completion even when no completion is provided. - ShellCompDirectiveNoFileComp: 1 << 2, - - // ShellCompDirectiveFilterFileExt indicates that the provided completions - // should be used as file extension filters. - // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() - // is a shortcut to using this directive explicitly. The BashCompFilenameExt - // annotation can also be used to obtain the same behavior for flags. - ShellCompDirectiveFilterFileExt: 1 << 3, - - // ShellCompDirectiveFilterDirs indicates that only directory names should - // be provided in file completion. To request directory names within another - // directory, the returned completions should specify the directory within - // which to search. The BashCompSubdirsInDir annotation can be used to - // obtain the same behavior but only for flags. - ShellCompDirectiveFilterDirs: 1 << 4, - - // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order - // in which the completions are provided. - ShellCompDirectiveKeepOrder: 1 << 5, - - // =========================================================================== - - // All directives using iota (or equivalent in Go) should be above this one. - // For internal use. - shellCompDirectiveMaxValue: 1 << 6, - - // ShellCompDirectiveDefault indicates to let the shell perform its default - // behavior after completions have been provided. - // This one must be last to avoid messing up the iota count. - ShellCompDirectiveDefault: 0, -}; - - - -export type Completion = { - action: string; - description?: string; -}; - -export type Callback = ( - previousArgs: string[], - toComplete: string, -) => Completion[] | Promise; - -export type Positional = { - required: boolean; - variadic: boolean; - completion: Callback; -}; - - -export const positionalMap = new Map(); - -export const flagMap = new Map(); diff --git a/zsh.ts b/src/zsh.ts similarity index 99% rename from zsh.ts rename to src/zsh.ts index e7bcc5e..ae06f1a 100644 --- a/zsh.ts +++ b/src/zsh.ts @@ -1,4 +1,4 @@ -import { ShellCompDirective } from "./shared"; +import { ShellCompDirective } from "./"; export function generate(name: string, exec: string) { return `#compdef ${name} diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 412190e..bca8cf3 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -102,5 +102,16 @@ describe.each(cliTools)("cli completion tests for %s", (cliTool) => { console.log(`[${cliTool}] No Completion Available Output:`, output); expect(output.trim()).toMatch(/^(:\d+)?$/); }); + + // pnpm tsx demo.citty.ts complete -- dev --port "" + // if the user done writing --port (ends with space) then it should suggest the ports (3000, ...) + // if the user not done writing (no end with space), then it should keep suggesting the --port option + // if the user wrote --port= (not end with space) then it should suggest the ports too (3000, ...) + + // add test cases for positionals + // like `vite src/` (single positional argument) + // or `vite src/ ./` (multiple positionals, see https://www.npmjs.com/package/cac#variadic-arguments) + // for all the tests we should use inline snapshots (https://vitest.dev/guide/snapshot.html#inline-snapshots) instead of regex or anything else + }); }); From 08d634a67b1cfa43574674b3413c94aa87a9473f Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Mon, 13 Jan 2025 11:29:34 +0330 Subject: [PATCH 05/22] docs --- README.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++----- src/index.ts | 33 +++++++++++ 2 files changed, 180 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b1a3480..48c61c8 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,160 @@ # tab -- [x] zsh test in git +Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool. -```zsh -source <(pnpm tsx demo.ts complete zsh) +Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces. + +```ts +import { Completion, script } from "@bombsh/tab"; + +const name = "vite" +const completion = new Completion() + +completion.addCommand("start", "Start the application", async (previousArgs, toComplete, endsWithSpace) => { + // suggestions + return [ + { value: "dev", description: "Start in development mode" }, + { value: "prod", description: "Start in production mode" } + ] +}) -vite # rest of the completions +completion.addOption("start", "--port", "Specify the port number", async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: "3000", description: "Development port" }, + { value: "8080", description: "Production port" } + ] +}) -pnpm tsx demo.ts complete -- --po + +// a way of getting the executable path to pass to the shell autocompletion script +function quoteIfNeeded(path: string) { + return path.includes(" ") ? `'${path}'` : path; +} +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; + +if (process.argv[2] === "--") { + // autocompletion logic + await completion.parse(process.argv.slice(2), "start") // TODO: remove "start" +} else { + // process.argv[2] can be "zsh", "bash", "fish", "powershell" + script(process.argv[2], name, x) +} ``` -- [x] tests vitest (this should mostly test the completions array, e.g. logs) -- [x] powershell completions generation -- [x] citty support `@bomsh/tab/citty` -- [] `@bombsh/tab` +Now your user can run `source <(vite complete zsh)` and they will get completions for the `vite` command using the [autocompletion server](#autocompletion-server). -- [] fish -- [] bash +## Adapters + +Since we are heavy users of tools like `cac` and `citty`, we have created adapters for both of them. Ideally, tab would be integrated internally into these tools, but for now, this is a good compromise. + +### `@bombsh/tab/cac` ```ts -const completion = new Completion() -completion.addCommand() -completion.addOption() +import cac from "cac"; +import tab from "@bombsh/tab/cac"; + +const cli = cac("vite"); + +cli + .command("dev", "Start dev server") + .option("--port ", "Specify port"); + +const completion = tab(cli); + +// Get the dev command completion handler +const devCommandCompletion = completion.commands.get("dev"); + +// Get and configure the port option completion handler +const portOptionCompletion = devCommandCompletion.options.get("--port"); +portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: "3000", description: "Development port" }, + { value: "8080", description: "Production port" } + ] +} + +cli.parse(); +``` +Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `vite dev --po`, they will get suggestions for the `--port` option. Or if they write `vite d` they will get suggestions for the `dev` command. + +Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation. + +### `@bombsh/tab/citty` + +```ts +import citty, { defineCommand, createMain } from "citty"; +import tab from "@bombsh/tab/citty"; + +const main = defineCommand({ + meta: { + name: "vite", + description: "Vite CLI tool", + }, +}); + +const devCommand = defineCommand({ + meta: { + name: "dev", + description: "Start dev server", + }, + args: { + port: { type: "string", description: "Specify port" }, + } +}); + +main.subCommands = { + dev: devCommand +}; + +const completion = await tab(main); + +// TODO: addHandler function to export +const devCommandCompletion = completion.commands.get("dev"); -// better name -completion.parse() +const portOptionCompletion = devCommandCompletion.options.get("--port"); + +portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: "3000", description: "Development port" }, + { value: "8080", description: "Production port" } + ] +} + +const cli = createMain(main); +cli(); ``` + +## Autocompletion Server + +By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server. + +```zsh +vite complete -- --po +--port Specify the port number +:0 +``` + +The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`). + +In situations like `vite dev --po` you'd have autocompletions! But in the case of `pnpm vite dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. + +Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available. + +This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box. + +Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support. + +## Inspiration +- git +- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go) + + +## TODO +- [] fish +- [] bash diff --git a/src/index.ts b/src/index.ts index 634e0bf..ef33858 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ +import * as zsh from "./zsh"; +import * as bash from "./bash"; +import * as fish from "./fish"; +import * as powershell from "./powershell"; + // ShellCompRequestCmd is the name of the hidden command that is used to request // completion results from the program. It is used by the shell completion scripts. export const ShellCompRequestCmd: string = "__complete"; @@ -208,3 +213,31 @@ export class Completion { console.log(`:${directive}`); } } + +export function script(shell: "zsh" | "bash" | "fish" | "powershell", name: string, x: string) { + switch (shell) { + case "zsh": { + const script = zsh.generate(name, x); + console.log(script); + break; + } + case "bash": { + const script = bash.generate(name, x); + console.log(script); + break; + } + case "fish": { + const script = fish.generate(name, x); + console.log(script); + break; + } + case "powershell": { + const script = powershell.generate(name, x); + console.log(script); + break; + } + default: { + throw new Error(`Unsupported shell: ${shell}`); + } + } +} \ No newline at end of file From a75179d31ca6b39c6da59688f95306920ecebc80 Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Mon, 13 Jan 2025 11:46:41 +0330 Subject: [PATCH 06/22] docs --- README.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 48c61c8..f25aa21 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Tools like git and their autocompletion experience inspired us to build this too ```ts import { Completion, script } from "@bombsh/tab"; -const name = "vite" +const name = "my-cli" const completion = new Completion() completion.addCommand("start", "Start the application", async (previousArgs, toComplete, endsWithSpace) => { @@ -48,7 +48,7 @@ if (process.argv[2] === "--") { } ``` -Now your user can run `source <(vite complete zsh)` and they will get completions for the `vite` command using the [autocompletion server](#autocompletion-server). +Now your user can run `source <(my-cli complete zsh)` and they will get completions for the `my-cli` command using the [autocompletion server](#autocompletion-server). ## Adapters @@ -60,7 +60,7 @@ Since we are heavy users of tools like `cac` and `citty`, we have created adapte import cac from "cac"; import tab from "@bombsh/tab/cac"; -const cli = cac("vite"); +const cli = cac("my-cli"); cli .command("dev", "Start dev server") @@ -82,7 +82,7 @@ portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) = cli.parse(); ``` -Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `vite dev --po`, they will get suggestions for the `--port` option. Or if they write `vite d` they will get suggestions for the `dev` command. +Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `my-cli dev --po`, they will get suggestions for the `--port` option. Or if they write `my-cli d` they will get suggestions for the `dev` command. Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation. @@ -94,8 +94,8 @@ import tab from "@bombsh/tab/citty"; const main = defineCommand({ meta: { - name: "vite", - description: "Vite CLI tool", + name: "my-cli", + description: "My CLI tool", }, }); @@ -131,19 +131,30 @@ const cli = createMain(main); cli(); ``` +## Recipe + +`source <(my-cli complete zsh)` won't be enough since the user would have to run this command each time they spin up a new shell instance. + +We suggest this approach for the end user that you as a maintainer might want to push. + +``` +my-cli completion zsh > ~/completion-for-my-cli.zsh +echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc +``` + ## Autocompletion Server By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server. ```zsh -vite complete -- --po +my-cli complete -- --po --port Specify the port number :0 ``` The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`). -In situations like `vite dev --po` you'd have autocompletions! But in the case of `pnpm vite dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. +In situations like `my-cli dev --po` you'd have autocompletions! But in the case of `pnpm my-cli dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available. @@ -153,8 +164,7 @@ Other package managers like `npm` and `yarn` can decide whether to support this ## Inspiration - git -- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go) - +- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go), without cobra, tab would have took 10x longer to build ## TODO - [] fish From 914e4fc5bc032d8febe21cf19c94b4cc263fcc83 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Mon, 13 Jan 2025 16:08:12 +0330 Subject: [PATCH 07/22] tests --- tests/cli.test.ts | 53 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index bca8cf3..85a0654 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -18,7 +18,7 @@ const cliTools = ["cac", "citty"]; describe.each(cliTools)("cli completion tests for %s", (cliTool) => { const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; - // it("should complete vite commands", async () => { + // it("should complete positional commands", async () => { // const output = await runCommand(commandPrefix); // console.log(`[${cliTool}] Command Output:`, output); // expect(output).toContain("src/"); @@ -102,16 +102,49 @@ describe.each(cliTools)("cli completion tests for %s", (cliTool) => { console.log(`[${cliTool}] No Completion Available Output:`, output); expect(output.trim()).toMatch(/^(:\d+)?$/); }); + }); + + describe("edge case completions for end with space", () => { + + it("should suggest port values if user ends with space after `--port`", async () => { + const command = `${commandPrefix} dev --port ""`; + const output = await runCommand(command); + console.log(`[${cliTool}] End With Space (port) Output:`, output); + expect(output).toContain("3000"); + }); + + it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { + const command = `${commandPrefix} dev --po`; + const output = await runCommand(command); + console.log(`[${cliTool}] Partial Option (no space) Output:`, output); + expect(output).toContain("--port"); + }); + + it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { + const command = `${commandPrefix} dev --port ""`; + const output = await runCommand(command); + console.log(`[${cliTool}] --port= (no space) Output:`, output); + expect(output).toContain("3000"); + }); + + }); - // pnpm tsx demo.citty.ts complete -- dev --port "" - // if the user done writing --port (ends with space) then it should suggest the ports (3000, ...) - // if the user not done writing (no end with space), then it should keep suggesting the --port option - // if the user wrote --port= (not end with space) then it should suggest the ports too (3000, ...) + describe("positional argument completions", () => { + it("should complete single positional argument when ending with space (vite src/)", async () => { + const command = `${commandPrefix} vite src/ ""`; + const output = await runCommand(command); + console.log(`[${cliTool}] Single Positional Output:`, output); - // add test cases for positionals - // like `vite src/` (single positional argument) - // or `vite src/ ./` (multiple positionals, see https://www.npmjs.com/package/cac#variadic-arguments) - // for all the tests we should use inline snapshots (https://vitest.dev/guide/snapshot.html#inline-snapshots) instead of regex or anything else + expect(output).toContain("src/"); + expect(output).toContain("./"); + }); + it("should complete multiple positional arguments when ending with space (vite src/ ./)", async () => { + const command = `${commandPrefix} vite src/ ""`; + const output = await runCommand(command); + console.log(`[${cliTool}] Multiple Positional Output:`, output); + + expect(output).toContain("./"); + }); }); -}); +}); \ No newline at end of file From 4eb129aadad03b1f1dbc87a1cfe5b8e2d534edd0 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Mon, 13 Jan 2025 22:58:15 +0330 Subject: [PATCH 08/22] fix --- src/citty.ts | 1 - src/index.ts | 152 ++++++++++++++++++++++------------------------ tests/cli.test.ts | 1 + 3 files changed, 74 insertions(+), 80 deletions(-) diff --git a/src/citty.ts b/src/citty.ts index 213535e..fb40e94 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -130,7 +130,6 @@ export default async function tab(instance: Command break; } default: { - console.log(completion) const args = (await resolve(instance.args))!; const parsed = parseArgs(extra, args); // TODO: this is not ideal at all diff --git a/src/index.ts b/src/index.ts index ef33858..2f9730a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,52 +14,52 @@ export const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; // ShellCompDirective is a bit map representing the different behaviors the shell // can be instructed to have once completions have been provided. export const ShellCompDirective = { - // ShellCompDirectiveError indicates an error occurred and completions should be ignored. - ShellCompDirectiveError: 1 << 0, - - // ShellCompDirectiveNoSpace indicates that the shell should not add a space - // after the completion even if there is a single completion provided. - ShellCompDirectiveNoSpace: 1 << 1, - - // ShellCompDirectiveNoFileComp indicates that the shell should not provide - // file completion even when no completion is provided. - ShellCompDirectiveNoFileComp: 1 << 2, - - // ShellCompDirectiveFilterFileExt indicates that the provided completions - // should be used as file extension filters. - // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() - // is a shortcut to using this directive explicitly. The BashCompFilenameExt - // annotation can also be used to obtain the same behavior for flags. - ShellCompDirectiveFilterFileExt: 1 << 3, - - // ShellCompDirectiveFilterDirs indicates that only directory names should - // be provided in file completion. To request directory names within another - // directory, the returned completions should specify the directory within - // which to search. The BashCompSubdirsInDir annotation can be used to - // obtain the same behavior but only for flags. - ShellCompDirectiveFilterDirs: 1 << 4, - - // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order - // in which the completions are provided. - ShellCompDirectiveKeepOrder: 1 << 5, - - // =========================================================================== - - // All directives using iota (or equivalent in Go) should be above this one. - // For internal use. - shellCompDirectiveMaxValue: 1 << 6, - - // ShellCompDirectiveDefault indicates to let the shell perform its default - // behavior after completions have been provided. - // This one must be last to avoid messing up the iota count. - ShellCompDirectiveDefault: 0, + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. + ShellCompDirectiveError: 1 << 0, + + // ShellCompDirectiveNoSpace indicates that the shell should not add a space + // after the completion even if there is a single completion provided. + ShellCompDirectiveNoSpace: 1 << 1, + + // ShellCompDirectiveNoFileComp indicates that the shell should not provide + // file completion even when no completion is provided. + ShellCompDirectiveNoFileComp: 1 << 2, + + // ShellCompDirectiveFilterFileExt indicates that the provided completions + // should be used as file extension filters. + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() + // is a shortcut to using this directive explicitly. The BashCompFilenameExt + // annotation can also be used to obtain the same behavior for flags. + ShellCompDirectiveFilterFileExt: 1 << 3, + + // ShellCompDirectiveFilterDirs indicates that only directory names should + // be provided in file completion. To request directory names within another + // directory, the returned completions should specify the directory within + // which to search. The BashCompSubdirsInDir annotation can be used to + // obtain the same behavior but only for flags. + ShellCompDirectiveFilterDirs: 1 << 4, + + // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order + // in which the completions are provided. + ShellCompDirectiveKeepOrder: 1 << 5, + + // =========================================================================== + + // All directives using iota (or equivalent in Go) should be above this one. + // For internal use. + shellCompDirectiveMaxValue: 1 << 6, + + // ShellCompDirectiveDefault indicates to let the shell perform its default + // behavior after completions have been provided. + // This one must be last to avoid messing up the iota count. + ShellCompDirectiveDefault: 0, }; export type Positional = { - required: boolean; - variadic: boolean; - completion: Handler; + required: boolean; + variadic: boolean; + completion: Handler; }; type Items = { @@ -87,7 +87,7 @@ export class Completion { addCommand(name: string, description: string, handler: Handler, parent?: string) { const key = parent ? `${parent} ${name}` : name this.commands.set(key, { description, handler, options: new Map(), parent: parent ? this.commands.get(parent)! : this.commands.get('')! }); - return key + return key } addOption(command: string, option: string, description: string, handler: Handler) { @@ -100,7 +100,6 @@ export class Completion { } async parse(args: string[], potentialCommand: string) { - console.log(potentialCommand) const matchedCommand = this.commands.get(potentialCommand) ?? this.commands.get('')!; let directive = ShellCompDirective.ShellCompDirectiveDefault; const completions: string[] = []; @@ -113,12 +112,10 @@ export class Completion { let toComplete = args[args.length - 1] || ""; const previousArgs = args.slice(0, -1); - console.log('here', previousArgs) if (previousArgs.length > 0) { const lastPrevArg = previousArgs[previousArgs.length - 1]; if (lastPrevArg.startsWith("--") && endsWithSpace) { - console.log('here') - const {handler} = matchedCommand.options.get(lastPrevArg)!; + const { handler } = matchedCommand.options.get(lastPrevArg)!; if (handler) { const flagSuggestions = await handler(previousArgs, toComplete, endsWithSpace); completions.push( @@ -137,12 +134,12 @@ export class Completion { if (toComplete.startsWith("--")) { directive = ShellCompDirective.ShellCompDirectiveNoFileComp; const equalsIndex = toComplete.indexOf("="); - + if (equalsIndex !== -1) { const flagName = toComplete.slice(2, equalsIndex); const valueToComplete = toComplete.slice(equalsIndex + 1); - const {handler} = matchedCommand.options.get(`--${flagName}`)!; - + const { handler } = matchedCommand.options.get(`--${flagName}`)!; + if (handler) { const suggestions = await handler(previousArgs, valueToComplete, endsWithSpace); completions.push(...suggestions.map( @@ -151,7 +148,7 @@ export class Completion { } } else if (!endsWithSpace) { const options = new Map(matchedCommand.options); - + let currentCommand = matchedCommand; while (currentCommand.parent) { for (const [key, value] of currentCommand.parent.options) { @@ -175,13 +172,10 @@ export class Completion { ) ); } else { - console.log('here3') - const {handler} = matchedCommand.options.get(toComplete)!; - console.log(handler, toComplete) - + const { handler } = matchedCommand.options.get(toComplete)!; + if (handler) { const suggestions = await handler(previousArgs, toComplete, endsWithSpace); - console.log(suggestions) completions.push(...suggestions.map( (comp) => `${comp.value}\t${comp.description ?? ""}` )); @@ -216,28 +210,28 @@ export class Completion { export function script(shell: "zsh" | "bash" | "fish" | "powershell", name: string, x: string) { switch (shell) { - case "zsh": { - const script = zsh.generate(name, x); - console.log(script); - break; - } - case "bash": { - const script = bash.generate(name, x); - console.log(script); - break; - } - case "fish": { - const script = fish.generate(name, x); - console.log(script); - break; - } - case "powershell": { - const script = powershell.generate(name, x); - console.log(script); - break; - } - default: { - throw new Error(`Unsupported shell: ${shell}`); - } + case "zsh": { + const script = zsh.generate(name, x); + console.log(script); + break; + } + case "bash": { + const script = bash.generate(name, x); + console.log(script); + break; + } + case "fish": { + const script = fish.generate(name, x); + console.log(script); + break; + } + case "powershell": { + const script = powershell.generate(name, x); + console.log(script); + break; + } + default: { + throw new Error(`Unsupported shell: ${shell}`); + } } } \ No newline at end of file diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 85a0654..a62f75a 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,3 +1,4 @@ +// TODO: convert to inlineSnapShot import { exec } from "child_process"; import { describe, it, expect, test } from "vitest"; From f02de066812dc191d5d3067d280e3d663e5cbc89 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 14 Jan 2025 00:18:25 +0330 Subject: [PATCH 09/22] subCommands --- src/citty.ts | 2 ++ src/index.ts | 55 ++++++++++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/citty.ts b/src/citty.ts index fb40e94..5885c35 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -81,6 +81,8 @@ export default async function tab(instance: Command await handleSubCommands(completion, subCommands); + // console.log("LOOK HERE", subCommands) + if (instance.args) { for (const [argName, argConfig] of Object.entries(instance.args)) { const conf = argConfig as PositionalArgDef; diff --git a/src/index.ts b/src/index.ts index 2f9730a..771ca80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,9 +85,14 @@ export class Completion { commands = new Map(); addCommand(name: string, description: string, handler: Handler, parent?: string) { - const key = parent ? `${parent} ${name}` : name - this.commands.set(key, { description, handler, options: new Map(), parent: parent ? this.commands.get(parent)! : this.commands.get('')! }); - return key + const key = parent ? `${parent} ${name}` : name; + this.commands.set(key, { + description, + handler, + options: new Map(), + parent: parent ? this.commands.get(parent)! : undefined, + }); + return key; } addOption(command: string, option: string, description: string, handler: Handler) { @@ -96,7 +101,7 @@ export class Completion { throw new Error(`Command ${command} not found.`); } cmd.options.set(option, { description, handler }); - return option + return option; } async parse(args: string[], potentialCommand: string) { @@ -119,12 +124,12 @@ export class Completion { if (handler) { const flagSuggestions = await handler(previousArgs, toComplete, endsWithSpace); completions.push( - ...flagSuggestions.filter(comp => comp.value.startsWith(toComplete)).map( - (comp) => `${comp.value}\t${comp.description ?? ""}` - ) + ...flagSuggestions + .filter(comp => comp.value.startsWith(toComplete)) + .map(comp => `${comp.value}\t${comp.description ?? ""}`) ); directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - completions.forEach((comp) => console.log(comp)); + completions.forEach(comp => console.log(comp)); console.log(`:${directive}`); return; } @@ -143,7 +148,7 @@ export class Completion { if (handler) { const suggestions = await handler(previousArgs, valueToComplete, endsWithSpace); completions.push(...suggestions.map( - (comp) => `${comp.value}\t${comp.description ?? ""}` + comp => `${comp.value}\t${comp.description ?? ""}` )); } } else if (!endsWithSpace) { @@ -168,7 +173,7 @@ export class Completion { completions.push( ...availableFlags.map( - (flag) => `${flag}\t${options.get(flag)!.description ?? ""}` + flag => `${flag}\t${options.get(flag)!.description ?? ""}` ) ); } else { @@ -177,33 +182,27 @@ export class Completion { if (handler) { const suggestions = await handler(previousArgs, toComplete, endsWithSpace); completions.push(...suggestions.map( - (comp) => `${comp.value}\t${comp.description ?? ""}` + comp => `${comp.value}\t${comp.description ?? ""}` )); } - } - } else if (!toComplete && endsWithSpace) { - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + } else { + const availableSubcommands = [...this.commands.keys()] + .filter(cmd => cmd.startsWith(potentialCommand) && cmd !== potentialCommand) + .map(cmd => cmd.replace(`${potentialCommand} `, "").split(" ")[0]) + .filter((subcmd, index, self) => self.indexOf(subcmd) === index) // Remove duplicates + .filter(subcmd => subcmd.startsWith(toComplete)); completions.push( - ...Object.keys(this.commands) - .filter((cmd) => cmd !== "complete") - .map((cmd) => `${cmd}\t${this.commands[cmd].description}`) + ...availableSubcommands.map( + subcmd => `${subcmd}\t${this.commands.get(`${potentialCommand} ${subcmd}`)?.description ?? ""}` + ) ); - const positionalCompletions = positionalMap.get(potentialCommand) || []; - for (const positional of positionalCompletions) { - const suggestions = await positional.completion(previousArgs, ""); - completions.push( - ...suggestions.map((comp) => `${comp.action}\t${comp.description ?? ""}`) - ); - if (suggestions.length > 0) { - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } - } + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; } - completions.forEach((comp) => console.log(comp)); + completions.forEach(comp => console.log(comp)); console.log(`:${directive}`); } } From 0a10e18792116bdadbc29689b47166a19d7aab74 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Tue, 14 Jan 2025 17:19:02 +0330 Subject: [PATCH 10/22] tests --- tests/__snapshots__/cli.test.ts.snap | 75 ++++++++++++++++++++++++ tests/cli.test.ts | 85 ++++++++-------------------- 2 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 tests/__snapshots__/cli.test.ts.snap diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap new file mode 100644 index 0000000..cf64dc6 --- /dev/null +++ b/tests/__snapshots__/cli.test.ts.snap @@ -0,0 +1,75 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` +"--port Specify port +" +`; + +exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` +"--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--port', shouldNotContain: '--port' }' 1`] = ` +"--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`cli completion tests for citty > cli option value handling > should handle conflicting options appropriately 1`] = ` +"--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `""`; + +exports[`cli completion tests for citty > cli option value handling > should resolve config option values correctly 1`] = ` +"vite.config.js +vite.config.ts +" +`; + +exports[`cli completion tests for citty > cli option value handling > should resolve port value correctly 1`] = ` +"3000 Development server port +" +`; + +exports[`cli completion tests for citty > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` +"--port Specify port +" +`; + +exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = ` +"3000 Development server port +8080 Alternative port +" +`; + +exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` +"3000 Development server port +8080 Alternative port +:4 +" +`; + +exports[`cli completion tests for citty > positional argument completions > should complete multiple positional arguments when ending with space (vite src/ ./) 1`] = ` +"./ +src/ +" +`; + +exports[`cli completion tests for citty > positional argument completions > should complete single positional argument when ending with space (vite src/) 1`] = ` +"./ +" +`; + +exports[`cli completion tests for citty > should complete cli options 1`] = ` +"dev +devbuild +" +`; diff --git a/tests/cli.test.ts b/tests/cli.test.ts index a62f75a..12017fa 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,4 +1,3 @@ -// TODO: convert to inlineSnapShot import { exec } from "child_process"; import { describe, it, expect, test } from "vitest"; @@ -14,28 +13,14 @@ function runCommand(command: string): Promise { }); } -const cliTools = ["cac", "citty"]; +const cliTools = ["citty"]; describe.each(cliTools)("cli completion tests for %s", (cliTool) => { const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; - // it("should complete positional commands", async () => { - // const output = await runCommand(commandPrefix); - // console.log(`[${cliTool}] Command Output:`, output); - // expect(output).toContain("src/"); - // expect(output).toContain("./"); - // // expect(output).toContain('--base'); - // }); - it("should complete cli options", async () => { - const output = await runCommand(`${commandPrefix} --`); - console.log(`[${cliTool}] Command Output:`, output); - expect(output).toContain("--port"); - expect(output).toContain("--config"); - expect(output).toContain("--base"); - expect(output).toContain("--logLevel"); - expect(output).toContain("--filter"); - expect(output).toContain("--mode"); + const output = await runCommand(`${commandPrefix}`); + expect(output).toMatchSnapshot(); }); describe("cli option completion tests", () => { @@ -45,107 +30,87 @@ describe.each(cliTools)("cli completion tests for %s", (cliTool) => { test.each(optionTests)( "should complete option for partial input '%s'", - async ({ partial, expected }) => { - const command = `${commandPrefix} ${partial}`; + async ({ partial }) => { + const command = `${commandPrefix} dev ${partial}`; const output = await runCommand(command); - console.log(`[${cliTool}] Complete ${partial} Output:`, output); - expect(output).toContain(expected); + expect(output).toMatchSnapshot(); } ); }); describe("cli option exclusion tests", () => { const alreadySpecifiedTests = [ - { specified: "--port", shouldNotContain: "--port" }, + { specified: "--config", shouldNotContain: "--config" }, ]; test.each(alreadySpecifiedTests)( "should not suggest already specified option '%s'", - async ({ specified, shouldNotContain }) => { + async ({ specified }) => { const command = `${commandPrefix} ${specified} --`; const output = await runCommand(command); - console.log(`[${cliTool}] Already Specified ${specified} Output:`, output); - expect(output).not.toContain(shouldNotContain); - // expect(output).toContain("--base"); + expect(output).toMatchSnapshot(); } ); }); describe("cli option value handling", () => { - it("should resolve port value correctly", async () => { - const command = `${commandPrefix} --port 3`; + const command = `${commandPrefix} dev --port=3`; const output = await runCommand(command); - console.log(`[${cliTool}] Port Value Output:`, output); - expect(output).toContain("3000"); + expect(output).toMatchSnapshot(); }); it("should handle conflicting options appropriately", async () => { - const command = `${commandPrefix} --port 3000 --`; + const command = `${commandPrefix} --config vite.config.js --`; const output = await runCommand(command); - console.log(`[${cliTool}] Conflicting Options Output:`, output); - expect(output).not.toContain("--port"); - expect(output).toContain("--config"); - // expect(output).toContain("--base"); + expect(output).toMatchSnapshot(); }); it("should resolve config option values correctly", async () => { - const command = `${commandPrefix} --port 3000 --config vite.config`; + const command = `${commandPrefix} --config vite.config`; const output = await runCommand(command); - console.log(`[${cliTool}] Config Option Output:`, output); - expect(output).toContain("vite.config.ts"); - expect(output).toContain("vite.config.js"); + expect(output).toMatchSnapshot(); }); it("should handle unknown options with no completions", async () => { const command = `${commandPrefix} --unknownoption`; const output = await runCommand(command); - console.log(`[${cliTool}] No Completion Available Output:`, output); - expect(output.trim()).toMatch(/^(:\d+)?$/); + expect(output.trim()).toMatchSnapshot(); }); }); describe("edge case completions for end with space", () => { - + //TOOD: remove this it("should suggest port values if user ends with space after `--port`", async () => { const command = `${commandPrefix} dev --port ""`; const output = await runCommand(command); - console.log(`[${cliTool}] End With Space (port) Output:`, output); - expect(output).toContain("3000"); + expect(output).toMatchSnapshot(); }); it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { const command = `${commandPrefix} dev --po`; const output = await runCommand(command); - console.log(`[${cliTool}] Partial Option (no space) Output:`, output); - expect(output).toContain("--port"); + expect(output).toMatchSnapshot(); }); it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { - const command = `${commandPrefix} dev --port ""`; + const command = `${commandPrefix} dev --port=`; const output = await runCommand(command); - console.log(`[${cliTool}] --port= (no space) Output:`, output); - expect(output).toContain("3000"); + expect(output).toMatchSnapshot(); }); - }); describe("positional argument completions", () => { it("should complete single positional argument when ending with space (vite src/)", async () => { const command = `${commandPrefix} vite src/ ""`; const output = await runCommand(command); - console.log(`[${cliTool}] Single Positional Output:`, output); - - expect(output).toContain("src/"); - expect(output).toContain("./"); + expect(output).toMatchSnapshot(); }); it("should complete multiple positional arguments when ending with space (vite src/ ./)", async () => { - const command = `${commandPrefix} vite src/ ""`; + const command = `${commandPrefix} vite ""`; const output = await runCommand(command); - console.log(`[${cliTool}] Multiple Positional Output:`, output); - - expect(output).toContain("./"); + expect(output).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); From efb9d2f2c51267e727aaf03a8722c6a308c821d8 Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Tue, 14 Jan 2025 23:19:13 +0330 Subject: [PATCH 11/22] docs --- src/citty.ts | 1 + src/index.ts | 76 +++++++++++++++++++++++++++++++++-------------- tests/cli.test.ts | 18 +++++++++++ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/citty.ts b/src/citty.ts index 5885c35..fd9e63f 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -136,6 +136,7 @@ export default async function tab(instance: Command const parsed = parseArgs(extra, args); // TODO: this is not ideal at all const matchedCommand = parsed._.join(' ') + console.log(completion) return completion.parse(extra, matchedCommand); } } diff --git a/src/index.ts b/src/index.ts index 771ca80..c7868e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,12 +62,12 @@ export type Positional = { completion: Handler; }; -type Items = { +type Item = { description: string; value: string; } -type Handler = (previousArgs: string[], toComplete: string, endsWithSpace: boolean) => (Items[] | Promise); +type Handler = (previousArgs: string[], toComplete: string, endsWithSpace: boolean) => (Item[] | Promise); type Option = { description: string; @@ -107,7 +107,7 @@ export class Completion { async parse(args: string[], potentialCommand: string) { const matchedCommand = this.commands.get(potentialCommand) ?? this.commands.get('')!; let directive = ShellCompDirective.ShellCompDirectiveDefault; - const completions: string[] = []; + const completions: Item[] = []; const endsWithSpace = args[args.length - 1] === ""; if (endsWithSpace) { @@ -126,7 +126,6 @@ export class Completion { completions.push( ...flagSuggestions .filter(comp => comp.value.startsWith(toComplete)) - .map(comp => `${comp.value}\t${comp.description ?? ""}`) ); directive = ShellCompDirective.ShellCompDirectiveNoFileComp; completions.forEach(comp => console.log(comp)); @@ -147,9 +146,7 @@ export class Completion { if (handler) { const suggestions = await handler(previousArgs, valueToComplete, endsWithSpace); - completions.push(...suggestions.map( - comp => `${comp.value}\t${comp.description ?? ""}` - )); + completions.push(...suggestions) } } else if (!endsWithSpace) { const options = new Map(matchedCommand.options); @@ -173,7 +170,7 @@ export class Completion { completions.push( ...availableFlags.map( - flag => `${flag}\t${options.get(flag)!.description ?? ""}` + flag => ({ value: flag, description: options.get(flag)!.description ?? "" }) ) ); } else { @@ -181,28 +178,61 @@ export class Completion { if (handler) { const suggestions = await handler(previousArgs, toComplete, endsWithSpace); - completions.push(...suggestions.map( - comp => `${comp.value}\t${comp.description ?? ""}` - )); + completions.push(...suggestions) } } } else { - const availableSubcommands = [...this.commands.keys()] - .filter(cmd => cmd.startsWith(potentialCommand) && cmd !== potentialCommand) - .map(cmd => cmd.replace(`${potentialCommand} `, "").split(" ")[0]) - .filter((subcmd, index, self) => self.indexOf(subcmd) === index) // Remove duplicates - .filter(subcmd => subcmd.startsWith(toComplete)); - - completions.push( - ...availableSubcommands.map( - subcmd => `${subcmd}\t${this.commands.get(`${potentialCommand} ${subcmd}`)?.description ?? ""}` - ) - ); + const potentialCommandParts = potentialCommand.split(' ') + for (const [k, v] of this.commands) { + // if the command is root, skip it + if (k === '') { + continue + } + + const parts = k.split(' ') + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const potentialPart = potentialCommandParts[i] || '' + + // Skip if we've already added this suggestion + const alreadyExists = completions.findIndex(item => item.value === part) !== -1 + if (alreadyExists) { + break + } + + // If we're at the current word being completed + if (i === potentialCommandParts.length - 1) { + // Only add if it matches the current partial input + if (part.startsWith(potentialPart)) { + completions.push({ value: part, description: v.description }) + } + break + } + + // For previous parts, they must match exactly + if (part !== potentialPart) { + break + } + } + } + completions.push({ value: 'dhere1', description: '' }) directive = ShellCompDirective.ShellCompDirectiveNoFileComp; } + // vite [...items] + // vite dev + // vite lint [item] + // vite dev build + + // TODO: prettier (plus check in ci) + // TODO: ci type check + + // TODO: positionals (tomorrow night) + // TODO: cac (tomorrow night) + // TODO: check behaviour of the tests (tomorrow night) + - completions.forEach(comp => console.log(comp)); + completions.forEach(comp => console.log(`${comp.value}\t${comp.description ?? ""}`)); console.log(`:${directive}`); } } diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 12017fa..f0fd64c 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -100,6 +100,24 @@ describe.each(cliTools)("cli completion tests for %s", (cliTool) => { }); }); + // single positional command: `lint [file]` + // vite "" + // -> src/ + // -> ./ + + // vite src/ "" + // -> nothing + // should not suggest anything + + // multiple postiionals command `lint [...files]` + // vite "" + // -> src/ + // -> ./ + + // vite src/ "" + // -> src/ + // -> ./ + describe("positional argument completions", () => { it("should complete single positional argument when ending with space (vite src/)", async () => { const command = `${commandPrefix} vite src/ ""`; From 0613f8456f06ad56ce09fcd6dde601230f1d4c43 Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Wed, 15 Jan 2025 23:28:18 +0330 Subject: [PATCH 12/22] positionals wip --- demo.citty.ts | 32 ++++++++++++++++++++++++++++++-- src/citty.ts | 25 +++++++++++++++++-------- src/index.ts | 11 ++++++++--- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/demo.citty.ts b/demo.citty.ts index d2d9f14..bd56cb9 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -24,7 +24,9 @@ const devCommand = defineCommand({ host: { type: "string", description: "Specify hostname" }, port: { type: "string", description: "Specify port" }, }, - run({ args }) { }, + run(ctx) { + console.log('dev', ctx) + }, }); devCommand.subCommands = { @@ -37,13 +39,39 @@ devCommand.subCommands = { }) } +const lintCommand = defineCommand({ + meta: { + name: "lint", + description: "Lint project", + }, + args: { + files: { type: "positional", description: "Files to lint" }, + }, + run(ctx) { + console.log('lint', ctx.cmd.args) + }, +}); + main.subCommands = { - dev: devCommand + dev: devCommand, + lint: lintCommand + } as Record>; const completion = await tab(main) for (const command of completion.commands.values()) { + console.log(command) + + if (command.name === 'lint') { + console.log('lint') + command.handler = () => { + return [ + { value: "main.ts", description: "Main file" }, + { value: "index.ts", description: "Index file" }, + ] + } + } for (const [o, config] of command.options.entries()) { if (o === "--port") { diff --git a/src/citty.ts b/src/citty.ts index fd9e63f..2ef4c4f 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -4,7 +4,7 @@ import * as bash from "./bash"; import * as fish from "./fish"; import * as powershell from "./powershell"; import { Completion } from "."; -import type { ArgsDef, CommandDef, PositionalArgDef } from "citty"; +import type { ArgsDef, CommandDef, PositionalArgDef, SubCommandsDef } from "citty"; function quoteIfNeeded(path) { return path.includes(" ") ? `'${path}'` : path; @@ -17,32 +17,40 @@ const quotedProcessArgs = processArgs.map(quoteIfNeeded); const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; -async function handleSubCommands( +function isConfigPositional(config: CommandDef) { + return config.args && Object.values(config.args).some(arg => arg.type === 'positional') +} + +async function handleSubCommands( completion: Completion, - subCommands: Record, + subCommands: SubCommandsDef, parentCmd?: string ) { for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { const config = await resolve(resolvableConfig); const meta = await resolve(config.meta); + const subCommands = await resolve(config.subCommands) if (!meta || typeof meta?.description !== "string") { throw new Error("Invalid meta or missing description."); } - - const name = completion.addCommand(cmd, meta.description, async (previousArgs, toComplete, endsWithSpace) => { + const isPositional = isConfigPositional(config) + const name = completion.addCommand(cmd, meta.description, isPositional ? [false] : [], async (previousArgs, toComplete, endsWithSpace) => { return [] }, parentCmd); // Handle nested subcommands recursively - if (config.subCommands) { - await handleSubCommands(completion, config.subCommands, name); + if (subCommands) { + await handleSubCommands(completion, subCommands, name); } // Handle arguments if (config.args) { for (const [argName, argConfig] of Object.entries(config.args)) { const conf = argConfig as ArgDef; + if (conf.type === 'positional') { + continue + } completion.addOption( name, `--${argName}`, @@ -75,7 +83,8 @@ export default async function tab(instance: Command } const root = '' - completion.addCommand(root, meta?.description ?? "", async (previousArgs, toComplete, endsWithSpace) => { + const isPositional = isConfigPositional(instance) + completion.addCommand(root, meta?.description ?? "", isPositional ? [false] : [], async (previousArgs, toComplete, endsWithSpace) => { return [] }); diff --git a/src/index.ts b/src/index.ts index c7868e7..3f1526a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,6 +75,7 @@ type Option = { } type Command = { + name: string; description: string; handler: Handler; options: Map; @@ -84,9 +85,12 @@ type Command = { export class Completion { commands = new Map(); - addCommand(name: string, description: string, handler: Handler, parent?: string) { + // vite [...files] + // args: [false, false, true], only the last argument can be variadic + addCommand(name: string, description: string, args: boolean[], handler: Handler, parent?: string) { const key = parent ? `${parent} ${name}` : name; this.commands.set(key, { + name: key, description, handler, options: new Map(), @@ -95,6 +99,7 @@ export class Completion { return key; } + // --port addOption(command: string, option: string, description: string, handler: Handler) { const cmd = this.commands.get(command); if (!cmd) { @@ -215,7 +220,6 @@ export class Completion { } } } - completions.push({ value: 'dhere1', description: '' }) directive = ShellCompDirective.ShellCompDirectiveNoFileComp; } @@ -227,7 +231,8 @@ export class Completion { // TODO: prettier (plus check in ci) // TODO: ci type check - // TODO: positionals (tomorrow night) + // TODO: positionals (tomorrow night), this is nearly there! + // TODO: cac (tomorrow night) // TODO: check behaviour of the tests (tomorrow night) From 456e630080f46ae4f30a4b14a6a99d1d19df810b Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 16 Jan 2025 13:58:03 +0330 Subject: [PATCH 13/22] types, type-check --- .github/workflows/ci.yml | 27 ++++ cac.ts | 304 --------------------------------------- demo.cac.ts | 256 ++++++++++++++++----------------- demo.citty.ts | 88 +----------- package.json | 3 +- shadcn.cac.ts | 182 +++++++++++------------ src/cac.ts | 304 +++++++++++++++++++++++++++++++++++++++ src/citty.ts | 2 +- tsconfig.json | 111 ++++++++++++++ 9 files changed, 668 insertions(+), 609 deletions(-) delete mode 100644 cac.ts create mode 100644 src/cac.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3473f9c..052e0fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: test: + name: Tests strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -36,3 +37,29 @@ jobs: - name: Run tests run: pnpm test + + typecheck: + name: Type Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + with: + version: 8 + + - name: Set node version to 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: pnpm + + - name: Install deps + run: pnpm install + + - name: Type-check + run: pnpm type-check diff --git a/cac.ts b/cac.ts deleted file mode 100644 index 6050b75..0000000 --- a/cac.ts +++ /dev/null @@ -1,304 +0,0 @@ -// @bombsh/tab/cac -import { CAC } from "cac"; -import * as zsh from "./zsh"; -import * as bash from "./bash"; -import * as fish from "./fish"; -import * as powershell from "./powershell"; -import { - flagMap, - Positional, - positionalMap, - ShellCompDirective, -} from "./shared"; - -function quoteIfNeeded(path: string): string { - return path.includes(" ") ? `'${path}'` : path; -} - -const execPath = process.execPath; -const processArgs = process.argv.slice(1); - -// Apply the quoting function to each part of x -// This ensures that paths like "Program Files" are quoted for PowerShell execution. -const quotedExecPath = quoteIfNeeded(execPath); -const quotedProcessArgs = processArgs.map(quoteIfNeeded); -const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); - -const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${ - quotedProcessArgs[0] -}`; - -export default function tab(instance: CAC): void { - instance.command("complete [shell]").action(async (shell, extra) => { - switch (shell) { - case "zsh": { - const script = zsh.generate(instance.name, x); - console.log(script); - break; - } - case "bash": { - const script = bash.generate(instance.name, x); - console.log(script); - break; - } - case "fish": { - const script = fish.generate(instance.name, x); - console.log(script); - break; - } - case "powershell": { - const script = powershell.generate(instance.name, x); - console.log(script); - break; - } - default: { - const args: string[] = extra["--"]; - - instance.showHelpOnExit = false; - let directive = ShellCompDirective.ShellCompDirectiveDefault; - - const endsWithSpace = args[args.length - 1] === ""; - if (endsWithSpace) { - args.pop(); - } - - let toComplete = args[args.length - 1] || ""; - const previousArgs = args.slice(0, -1); - - const completions: string[] = []; - - instance.unsetMatchedCommand(); - instance.parse([execPath, processArgs[0], ...previousArgs], { - run: false, - }); - - const command = instance.matchedCommand ?? instance.globalCommand; - - const options = [ - ...new Set([ - ...(command?.options ?? []), - ...instance.globalCommand.options, - ]), - ]; - - let isCompletingFlagValue = false; - let flagName = ""; - let option: (typeof options)[number] | null = null; - const lastArg = previousArgs[previousArgs.length - 1]; - - function processOption() { - const matchedOption = options.find((o) => - o.names.some((name) => name === flagName) - ); - - if (matchedOption && !matchedOption.isBoolean) { - isCompletingFlagValue = true; - option = matchedOption; - if (endsWithSpace) { - toComplete = ""; - } - } else { - isCompletingFlagValue = false; - option = null; - } - } - - if (toComplete.startsWith("--")) { - // Long option - flagName = toComplete.slice(2); - const equalsIndex = flagName.indexOf("="); - if (equalsIndex !== -1 && !endsWithSpace) { - // Option with '=', get the name before '=' - flagName = flagName.slice(0, equalsIndex); - toComplete = toComplete.slice(toComplete.indexOf("=") + 1); - processOption(); - } else if (!endsWithSpace) { - // If not ending with space, still typing option name - flagName = ""; - } else { - // User pressed space after typing the option name - processOption(); - toComplete = ""; - } - } else if (toComplete.startsWith("-") && toComplete.length > 1) { - // Short option - flagName = toComplete.slice(1); - if (!endsWithSpace) { - // Still typing option name - flagName = ""; - } else { - processOption(); - toComplete = ""; - } - } else if (lastArg?.startsWith("--") && !endsWithSpace) { - flagName = lastArg.slice(2); - processOption(); - } else if ( - lastArg?.startsWith("-") && - lastArg.length > 1 && - !endsWithSpace - ) { - flagName = lastArg.slice(2); - processOption(); - } - - if (isCompletingFlagValue) { - const flagCompletionFn = flagMap.get( - `${command.name} ${option?.name}` - ); - - if (flagCompletionFn) { - // Call custom completion function for the flag - const comps = await flagCompletionFn(previousArgs, toComplete); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } else { - // Default completion (e.g., file completion) - directive = ShellCompDirective.ShellCompDirectiveDefault; - } - } else if (toComplete.startsWith("-") && !endsWithSpace) { - const flag = toComplete.replace(/^-+/, ""); // Remove leading '-' - - // Determine options to suggest - let optionsToSuggest = options.filter((o) => { - const equalToDefault = - "default" in o.config && - instance.options[o.name] === o.config.default; - return ( - o.names.some((name) => name.startsWith(flag)) && - !(instance.options[o.name] && !equalToDefault) - ); - }); - - const requiredOptions = optionsToSuggest.filter((o) => o.required); - - if (requiredOptions.length) { - // Required options not yet specified - optionsToSuggest = requiredOptions; - } - - if (optionsToSuggest.length > 0) { - completions.push( - ...optionsToSuggest.map( - (o) => `--${o.name}\t${o.description ?? ""}` - ) - ); - } - - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } else { - instance.parse( - [execPath, processArgs[0], ...previousArgs, toComplete], - { - run: false, - } - ); - const fullCommandName = args - .filter((arg) => !arg.startsWith("-")) - .join(" "); - - for (const c of instance.commands) { - if (c.name === "complete") { - // avoid showing completions for the completion server - continue; - } - const fullCommandParts = fullCommandName.split(" "); - const commandParts: { type: "command"; value: string }[] = c.name - .split(" ") - .map((part) => ({ type: "command", value: part })); - const args: { - type: "positional"; - position: number; - value: Positional; - }[] = - positionalMap.get(c.name)?.map((arg, i) => ({ - type: "positional", - position: i, - value: arg, - })) ?? []; - const parts = [...commandParts, ...args]; - - for (let i = 0; i < parts.length; i++) { - const fullCommandPart = fullCommandParts[i]; - const part = parts[i]; - - if (part.type === "command") { - // Command part matching - if (part.value === fullCommandPart) { - // Command part matches user input, continue to next part - continue; - } else if ( - !fullCommandPart || - part.value.startsWith(fullCommandPart) - ) { - // User is typing this command part, provide completion - completions.push(`${part.value}\t${c.description ?? ""}`); - } - // Command part does not match, break - break; - } else if (part.type === "positional") { - const positional = part.value; - // Positional argument handling - if (part.value.variadic) { - const comps = await positional.completion( - previousArgs, - toComplete - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - break; - } - if (typeof fullCommandPart !== "undefined") { - // User has provided input for this positional argument - if (i === fullCommandParts.length - 1 && !endsWithSpace) { - // User is still typing this positional argument, provide completions - const comps = await positional.completion( - previousArgs, - toComplete - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - break; - } else { - // Positional argument is already provided, move to next - previousArgs.push(fullCommandPart); - continue; - } - } else { - // User has not provided input for this positional argument - const comps = await positional.completion( - previousArgs, - toComplete - ); - completions.push( - ...comps.map( - (comp) => `${comp.action}\t${comp.description ?? ""}` - ) - ); - break; - } - } - } - } - } - - // Output completions - for (const comp of completions) { - console.log(comp.split("\n")[0].trim()); - } - console.log(`:${directive}`); - console.error(`Completion ended with directive: ${directive}`); - } - } - }); -} diff --git a/demo.cac.ts b/demo.cac.ts index fb68cd1..a7fb966 100644 --- a/demo.cac.ts +++ b/demo.cac.ts @@ -1,143 +1,143 @@ -import fs from "fs/promises"; -import cac from "cac"; -import { - Callback, - Completion, - flagMap, - Positional, - positionalMap, -} from "./shared"; -import path from "path"; -import tab from "./cac"; +// import fs from "fs/promises"; +// import cac from "cac"; +// import { +// Callback, +// Completion, +// flagMap, +// Positional, +// positionalMap, +// } from "./shared"; +// import path from "path"; +// import tab from "./cac"; -const cli = cac("vite"); // Using 'vite' as the CLI tool name +// const cli = cac("vite"); // Using 'vite' as the CLI tool name -// Custom converters (placeholders) -function convertBase(value) { - return value; -} +// // Custom converters (placeholders) +// function convertBase(value) { +// return value; +// } -function convertHost(value) { - return value; -} +// function convertHost(value) { +// return value; +// } -// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/cli.ts -// Global options -cli - .option("-c, --config ", `[string] use specified config file`) - .option("--base ", `[string] public base path (default: /)`, { - type: [convertBase], - }) - .option("-l, --logLevel ", `[string] info | warn | error | silent`) - .option("--clearScreen", `[boolean] allow/disable clear screen when logging`) - .option("-d, --debug [feat]", `[string | boolean] show debug logs`) - .option("-f, --filter ", `[string] filter debug logs`) - .option("-m, --mode ", `[string] set env mode`); +// // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/cli.ts +// // Global options +// cli +// .option("-c, --config ", `[string] use specified config file`) +// .option("--base ", `[string] public base path (default: /)`, { +// type: [convertBase], +// }) +// .option("-l, --logLevel ", `[string] info | warn | error | silent`) +// .option("--clearScreen", `[boolean] allow/disable clear screen when logging`) +// .option("-d, --debug [feat]", `[string | boolean] show debug logs`) +// .option("-f, --filter ", `[string] filter debug logs`) +// .option("-m, --mode ", `[string] set env mode`); -// Dev command -cli - .command("[root]", "start dev server") // default command - .alias("serve") // the command is called 'serve' in Vite's API - .alias("dev") // alias to align with the script name - .option("--host [host]", `[string] specify hostname`, { type: [convertHost] }) - .option("--port ", `[number] specify port`) - .option("--open [path]", `[boolean | string] open browser on startup`) - .option("--cors", `[boolean] enable CORS`) - .option("--strictPort", `[boolean] exit if specified port is already in use`) - .option( - "--force", - `[boolean] force the optimizer to ignore the cache and re-bundle` - ) - .action((root, options) => { - console.log(`Starting dev server at ${root || "."} with options:`, options); - }); -// Build positional completions for each command using command.args -for (const c of [cli.globalCommand, ...cli.commands]) { - // Handle options - for (const o of [...cli.globalCommand.options, ...c.options]) { - const optionKey = `${c.name} ${o.name}`; +// // Dev command +// cli +// .command("[root]", "start dev server") // default command +// .alias("serve") // the command is called 'serve' in Vite's API +// .alias("dev") // alias to align with the script name +// .option("--host [host]", `[string] specify hostname`, { type: [convertHost] }) +// .option("--port ", `[number] specify port`) +// .option("--open [path]", `[boolean | string] open browser on startup`) +// .option("--cors", `[boolean] enable CORS`) +// .option("--strictPort", `[boolean] exit if specified port is already in use`) +// .option( +// "--force", +// `[boolean] force the optimizer to ignore the cache and re-bundle` +// ) +// .action((root, options) => { +// console.log(`Starting dev server at ${root || "."} with options:`, options); +// }); +// // Build positional completions for each command using command.args +// for (const c of [cli.globalCommand, ...cli.commands]) { +// // Handle options +// for (const o of [...cli.globalCommand.options, ...c.options]) { +// const optionKey = `${c.name} ${o.name}`; - if (o.rawName.includes("--logLevel ")) { - // Completion for --logLevel - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "info", description: "Info level logging" }, - { action: "warn", description: "Warning level logging" }, - { action: "error", description: "Error level logging" }, - { action: "silent", description: "No logging" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } +// if (o.rawName.includes("--logLevel ")) { +// // Completion for --logLevel +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "info", description: "Info level logging" }, +// { action: "warn", description: "Warning level logging" }, +// { action: "error", description: "Error level logging" }, +// { action: "silent", description: "No logging" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } - if (o.rawName.includes("--mode ")) { - // Completion for --mode - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "production", description: "Production mode" }, - { action: "development", description: "Development mode" }, - { action: "staging", description: "Staging mode" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } +// if (o.rawName.includes("--mode ")) { +// // Completion for --mode +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "production", description: "Production mode" }, +// { action: "development", description: "Development mode" }, +// { action: "staging", description: "Staging mode" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } - if (o.rawName.includes("--port ")) { - // Completion for --port - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "3000", description: "Development server port" }, - { action: "8080", description: "Alternative port" }, - { action: "80", description: "HTTP port" }, - { action: "443", description: "HTTPS port" }, - { action: "5000", description: "Common backend port" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } +// if (o.rawName.includes("--port ")) { +// // Completion for --port +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "3000", description: "Development server port" }, +// { action: "8080", description: "Alternative port" }, +// { action: "80", description: "HTTP port" }, +// { action: "443", description: "HTTPS port" }, +// { action: "5000", description: "Common backend port" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } - if (o.rawName.includes("--host [host]")) { - // Completion for --host - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "localhost", description: "Localhost" }, - { action: "0.0.0.0", description: "All interfaces" }, - { action: "127.0.0.1", description: "Loopback interface" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } +// if (o.rawName.includes("--host [host]")) { +// // Completion for --host +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "localhost", description: "Localhost" }, +// { action: "0.0.0.0", description: "All interfaces" }, +// { action: "127.0.0.1", description: "Loopback interface" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } - if (o.rawName.includes("--config ")) { - // Completion for --config - flagMap.set(optionKey, async (previousArgs, toComplete) => { - const configFiles = ["vite.config.ts", "vite.config.js"].filter( - (file) => file.startsWith(toComplete) - ); - return configFiles.map((file) => ({ action: file })); - }); - } +// if (o.rawName.includes("--config ")) { +// // Completion for --config +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// const configFiles = ["vite.config.ts", "vite.config.js"].filter( +// (file) => file.startsWith(toComplete) +// ); +// return configFiles.map((file) => ({ action: file })); +// }); +// } - // Add more option completions as needed - } +// // Add more option completions as needed +// } - // Handle positional arguments - if (c.args && c.args.length > 0) { - const positionals = c.args.map((arg) => ({ - required: arg.required, - variadic: arg.variadic, - value: arg.value, - completion: async (previousArgs, toComplete) => { - if (arg.value === "root") { - return [ - { action: "src/", description: "💣️.sh loves vite!" }, - { action: "./", description: "This one is better." }, - ]; - } - return []; - }, - })); +// // Handle positional arguments +// if (c.args && c.args.length > 0) { +// const positionals = c.args.map((arg) => ({ +// required: arg.required, +// variadic: arg.variadic, +// value: arg.value, +// completion: async (previousArgs, toComplete) => { +// if (arg.value === "root") { +// return [ +// { action: "src/", description: "💣️.sh loves vite!" }, +// { action: "./", description: "This one is better." }, +// ]; +// } +// return []; +// }, +// })); - positionalMap.set(c.name, positionals); - } -} +// positionalMap.set(c.name, positionals); +// } +// } -tab(cli); +// tab(cli); -cli.parse(); +// cli.parse(); diff --git a/demo.citty.ts b/demo.citty.ts index bd56cb9..d780c24 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -1,4 +1,4 @@ -import { defineCommand, createMain, CommandDef, CommandMeta, ArgDef, PositionalArgDef } from "citty"; +import { defineCommand, createMain, CommandDef } from "citty"; import tab from "./src/citty"; const main = defineCommand({ @@ -11,7 +11,7 @@ const main = defineCommand({ mode: { type: "string", description: "Set env mode", alias: "m" }, logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, }, - run(ctx) { + run(_ctx) { } }); @@ -24,7 +24,7 @@ const devCommand = defineCommand({ host: { type: "string", description: "Specify hostname" }, port: { type: "string", description: "Specify port" }, }, - run(ctx) { + run(ctx) { console.log('dev', ctx) }, }); @@ -47,7 +47,7 @@ const lintCommand = defineCommand({ args: { files: { type: "positional", description: "Files to lint" }, }, - run(ctx) { + run(ctx) { console.log('lint', ctx.cmd.args) }, }); @@ -119,86 +119,6 @@ for (const command of completion.commands.values()) { } } -// for (const command of [main as CommandDef, ...Object.values(main.subCommands as Record>)]) { -// const meta = command.meta as CommandMeta; -// const commandName = meta.name; - -// for (const [argName, argConfig] of Object.entries(command.args || {}) as [string, ArgDef][]) { -// const optionKey = `--${argName}`; - -// if (argName === "port") { -// flagMap.set(optionKey, async (_, toComplete) => { -// const options = [ -// { action: "3000", description: "Development server port" }, -// { action: "8080", description: "Alternative port" }, -// ]; -// return toComplete -// ? options.filter(comp => comp.action.startsWith(toComplete)) -// : options; -// }); -// } else if (argName === "host") { -// flagMap.set(optionKey, async (_, toComplete) => { -// const options = [ -// { action: "localhost", description: "Localhost" }, -// { action: "0.0.0.0", description: "All interfaces" }, -// ]; -// return toComplete -// ? options.filter(comp => comp.action.startsWith(toComplete)) -// : options; -// }); -// } else if (argName === "config") { -// flagMap.set(optionKey, async (_, toComplete) => { -// const configFiles = ["vite.config.ts", "vite.config.js"].filter( -// (file) => file.startsWith(toComplete) -// ); -// return configFiles.map((file) => ({ action: file })); -// }); -// } else if (argName === "mode") { -// flagMap.set(optionKey, async (_, toComplete) => { -// const options = [ -// { action: "development", description: "Development mode" }, -// { action: "production", description: "Production mode" }, -// ]; -// return toComplete -// ? options.filter(comp => comp.action.startsWith(toComplete)) -// : options; -// }); -// } else { -// flagMap.set(optionKey, async (_, toComplete) => { -// const flag = optionKey.startsWith("--") ? optionKey.slice(2) : optionKey; -// if (!toComplete || optionKey.startsWith(toComplete)) { -// return [{ action: optionKey, description: argConfig.description }]; -// } -// return []; -// }); -// } -// } - -// if (command.args) { -// const positionals = Object.entries(command.args) -// .filter(([, config]) => (config as any).type === "positional") -// .map(([argName, argConfig]) => { -// const conf = argConfig as PositionalArgDef; -// return { -// value: argName, -// variadic: false, -// required: !!conf.required, -// completion: async (_, toComplete) => { -// const options = [ -// { action: "src/", description: "Source directory" }, -// { action: "./", description: "Current directory" }, -// ]; -// return toComplete -// ? options.filter(comp => comp.action.startsWith(toComplete)) -// : options; -// }, -// }; -// }); -// positionalMap.set(commandName!, positionals); -// } -// } - - const cli = createMain(main); cli(); diff --git a/package.json b/package.json index 8cdf2e8..3b84c26 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest" + "test": "vitest", + "type-check": "tsc --noEmit" }, "keywords": [], "author": "", diff --git a/shadcn.cac.ts b/shadcn.cac.ts index d9093ed..c890e85 100644 --- a/shadcn.cac.ts +++ b/shadcn.cac.ts @@ -1,104 +1,104 @@ -import fs from "fs/promises"; -import cac from "cac"; -import { - Callback, - Completion, - flagMap, - Positional, - positionalMap, -} from "./shared"; -import path from "path"; -import tab from "./cac"; +// import fs from "fs/promises"; +// import cac from "cac"; +// import { +// Callback, +// Completion, +// flagMap, +// Positional, +// positionalMap, +// } from "./shared"; +// import path from "path"; +// import tab from "./cac"; -const cli = cac("shadcn"); // Using 'shadcn' as the CLI tool name +// const cli = cac("shadcn"); // Using 'shadcn' as the CLI tool name -// Global options -cli - .option("-c, --cwd [cwd]", `[string] the working directory. defaults to the current directory.`) - .option("-h, --help", `display help for command`); +// // Global options +// cli +// .option("-c, --cwd [cwd]", `[string] the working directory. defaults to the current directory.`) +// .option("-h, --help", `display help for command`); -// Init command -cli - .command("init", "initialize your project and install dependencies") - .option("-d, --defaults", `[boolean] use default values i.e new-york, zinc, and css variables`, { default: false }) - .option("-f, --force", `[boolean] force overwrite of existing components.json`, { default: false }) - .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false }) - .action((options) => { - console.log(`Initializing project with options:`, options); - }); +// // Init command +// cli +// .command("init", "initialize your project and install dependencies") +// .option("-d, --defaults", `[boolean] use default values i.e new-york, zinc, and css variables`, { default: false }) +// .option("-f, --force", `[boolean] force overwrite of existing components.json`, { default: false }) +// .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false }) +// .action((options) => { +// console.log(`Initializing project with options:`, options); +// }); -// Add command -cli - .command("add [...components]", "add a component to your project") - .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false }) - .option("-o, --overwrite", `[boolean] overwrite existing files`, { default: false }) - .option("-a, --all", `[boolean] add all available components`, { default: false }) - .option("-p, --path [path]", `[string] the path to add the component to`) - .action((components, options) => { - console.log(`Adding components:`, components, `with options:`, options); - }); +// // Add command +// cli +// .command("add [...components]", "add a component to your project") +// .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false }) +// .option("-o, --overwrite", `[boolean] overwrite existing files`, { default: false }) +// .option("-a, --all", `[boolean] add all available components`, { default: false }) +// .option("-p, --path [path]", `[string] the path to add the component to`) +// .action((components, options) => { +// console.log(`Adding components:`, components, `with options:`, options); +// }); -// Build positional completions for each command using command.args -for (const c of [cli.globalCommand, ...cli.commands]) { - // Handle options - for (const o of [...cli.globalCommand.options, ...c.options]) { - const optionKey = `${c.name} ${o.name}`; +// // Build positional completions for each command using command.args +// for (const c of [cli.globalCommand, ...cli.commands]) { +// // Handle options +// for (const o of [...cli.globalCommand.options, ...c.options]) { +// const optionKey = `${c.name} ${o.name}`; - if (o.rawName.includes("--cwd ")) { - // Completion for --cwd (common working directories) - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "./apps/www", description: "Default app directory" }, - { action: "./apps/admin", description: "Admin app directory" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } +// if (o.rawName.includes("--cwd ")) { +// // Completion for --cwd (common working directories) +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "./apps/www", description: "Default app directory" }, +// { action: "./apps/admin", description: "Admin app directory" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } - if (o.rawName.includes("--defaults")) { - // Completion for --defaults (show info for default setup) - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [{ action: "true", description: "Use default values for setup" }]; - }); - } +// if (o.rawName.includes("--defaults")) { +// // Completion for --defaults (show info for default setup) +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [{ action: "true", description: "Use default values for setup" }]; +// }); +// } - if (o.rawName.includes("--path ")) { - // Completion for --path (common component paths) - flagMap.set(optionKey, async (previousArgs, toComplete) => { - return [ - { action: "src/components", description: "Main components directory" }, - { action: "src/ui", description: "UI components directory" }, - ].filter((comp) => comp.action.startsWith(toComplete)); - }); - } - } +// if (o.rawName.includes("--path ")) { +// // Completion for --path (common component paths) +// flagMap.set(optionKey, async (previousArgs, toComplete) => { +// return [ +// { action: "src/components", description: "Main components directory" }, +// { action: "src/ui", description: "UI components directory" }, +// ].filter((comp) => comp.action.startsWith(toComplete)); +// }); +// } +// } - // Handle positional arguments - if (c.name === "add" && c.args && c.args.length > 0) { - const componentChoices = [ - "accordion", "alert", "alert-dialog", "aspect-ratio", "avatar", - "badge", "button", "calendar", "card", "checkbox" - ]; - const positionals = c.args.map((arg) => ({ - required: arg.required, - variadic: arg.variadic, - value: arg.value, - completion: async (previousArgs, toComplete) => { - // if (arg.value === "root") { - return componentChoices - // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true - // .filter((comp) => comp.startsWith(toComplete)) - .map((comp) => ({ action: comp, description: `Add ${comp} component` })); - // } - // return []; - }, - })); +// // Handle positional arguments +// if (c.name === "add" && c.args && c.args.length > 0) { +// const componentChoices = [ +// "accordion", "alert", "alert-dialog", "aspect-ratio", "avatar", +// "badge", "button", "calendar", "card", "checkbox" +// ]; +// const positionals = c.args.map((arg) => ({ +// required: arg.required, +// variadic: arg.variadic, +// value: arg.value, +// completion: async (previousArgs, toComplete) => { +// // if (arg.value === "root") { +// return componentChoices +// // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true +// // .filter((comp) => comp.startsWith(toComplete)) +// .map((comp) => ({ action: comp, description: `Add ${comp} component` })); +// // } +// // return []; +// }, +// })); - positionalMap.set(c.name, positionals); - } -} +// positionalMap.set(c.name, positionals); +// } +// } -// Initialize tab completion -tab(cli); +// // Initialize tab completion +// tab(cli); -cli.parse(); +// cli.parse(); diff --git a/src/cac.ts b/src/cac.ts new file mode 100644 index 0000000..98f55f3 --- /dev/null +++ b/src/cac.ts @@ -0,0 +1,304 @@ +// // @bombsh/tab/cac +// import { CAC } from "cac"; +// import * as zsh from "./zsh"; +// import * as bash from "./bash"; +// import * as fish from "./fish"; +// import * as powershell from "./powershell"; +// import { +// flagMap, +// Positional, +// positionalMap, +// ShellCompDirective, +// } from "./shared"; + +// function quoteIfNeeded(path: string): string { +// return path.includes(" ") ? `'${path}'` : path; +// } + +// const execPath = process.execPath; +// const processArgs = process.argv.slice(1); + +// // Apply the quoting function to each part of x +// // This ensures that paths like "Program Files" are quoted for PowerShell execution. +// const quotedExecPath = quoteIfNeeded(execPath); +// const quotedProcessArgs = processArgs.map(quoteIfNeeded); +// const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); + +// const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${ +// quotedProcessArgs[0] +// }`; + +// export default function tab(instance: CAC): void { +// instance.command("complete [shell]").action(async (shell, extra) => { +// switch (shell) { +// case "zsh": { +// const script = zsh.generate(instance.name, x); +// console.log(script); +// break; +// } +// case "bash": { +// const script = bash.generate(instance.name, x); +// console.log(script); +// break; +// } +// case "fish": { +// const script = fish.generate(instance.name, x); +// console.log(script); +// break; +// } +// case "powershell": { +// const script = powershell.generate(instance.name, x); +// console.log(script); +// break; +// } +// default: { +// const args: string[] = extra["--"]; + +// instance.showHelpOnExit = false; +// let directive = ShellCompDirective.ShellCompDirectiveDefault; + +// const endsWithSpace = args[args.length - 1] === ""; +// if (endsWithSpace) { +// args.pop(); +// } + +// let toComplete = args[args.length - 1] || ""; +// const previousArgs = args.slice(0, -1); + +// const completions: string[] = []; + +// instance.unsetMatchedCommand(); +// instance.parse([execPath, processArgs[0], ...previousArgs], { +// run: false, +// }); + +// const command = instance.matchedCommand ?? instance.globalCommand; + +// const options = [ +// ...new Set([ +// ...(command?.options ?? []), +// ...instance.globalCommand.options, +// ]), +// ]; + +// let isCompletingFlagValue = false; +// let flagName = ""; +// let option: (typeof options)[number] | null = null; +// const lastArg = previousArgs[previousArgs.length - 1]; + +// function processOption() { +// const matchedOption = options.find((o) => +// o.names.some((name) => name === flagName) +// ); + +// if (matchedOption && !matchedOption.isBoolean) { +// isCompletingFlagValue = true; +// option = matchedOption; +// if (endsWithSpace) { +// toComplete = ""; +// } +// } else { +// isCompletingFlagValue = false; +// option = null; +// } +// } + +// if (toComplete.startsWith("--")) { +// // Long option +// flagName = toComplete.slice(2); +// const equalsIndex = flagName.indexOf("="); +// if (equalsIndex !== -1 && !endsWithSpace) { +// // Option with '=', get the name before '=' +// flagName = flagName.slice(0, equalsIndex); +// toComplete = toComplete.slice(toComplete.indexOf("=") + 1); +// processOption(); +// } else if (!endsWithSpace) { +// // If not ending with space, still typing option name +// flagName = ""; +// } else { +// // User pressed space after typing the option name +// processOption(); +// toComplete = ""; +// } +// } else if (toComplete.startsWith("-") && toComplete.length > 1) { +// // Short option +// flagName = toComplete.slice(1); +// if (!endsWithSpace) { +// // Still typing option name +// flagName = ""; +// } else { +// processOption(); +// toComplete = ""; +// } +// } else if (lastArg?.startsWith("--") && !endsWithSpace) { +// flagName = lastArg.slice(2); +// processOption(); +// } else if ( +// lastArg?.startsWith("-") && +// lastArg.length > 1 && +// !endsWithSpace +// ) { +// flagName = lastArg.slice(2); +// processOption(); +// } + +// if (isCompletingFlagValue) { +// const flagCompletionFn = flagMap.get( +// `${command.name} ${option?.name}` +// ); + +// if (flagCompletionFn) { +// // Call custom completion function for the flag +// const comps = await flagCompletionFn(previousArgs, toComplete); +// completions.push( +// ...comps.map( +// (comp) => `${comp.action}\t${comp.description ?? ""}` +// ) +// ); +// directive = ShellCompDirective.ShellCompDirectiveNoFileComp; +// } else { +// // Default completion (e.g., file completion) +// directive = ShellCompDirective.ShellCompDirectiveDefault; +// } +// } else if (toComplete.startsWith("-") && !endsWithSpace) { +// const flag = toComplete.replace(/^-+/, ""); // Remove leading '-' + +// // Determine options to suggest +// let optionsToSuggest = options.filter((o) => { +// const equalToDefault = +// "default" in o.config && +// instance.options[o.name] === o.config.default; +// return ( +// o.names.some((name) => name.startsWith(flag)) && +// !(instance.options[o.name] && !equalToDefault) +// ); +// }); + +// const requiredOptions = optionsToSuggest.filter((o) => o.required); + +// if (requiredOptions.length) { +// // Required options not yet specified +// optionsToSuggest = requiredOptions; +// } + +// if (optionsToSuggest.length > 0) { +// completions.push( +// ...optionsToSuggest.map( +// (o) => `--${o.name}\t${o.description ?? ""}` +// ) +// ); +// } + +// directive = ShellCompDirective.ShellCompDirectiveNoFileComp; +// } else { +// instance.parse( +// [execPath, processArgs[0], ...previousArgs, toComplete], +// { +// run: false, +// } +// ); +// const fullCommandName = args +// .filter((arg) => !arg.startsWith("-")) +// .join(" "); + +// for (const c of instance.commands) { +// if (c.name === "complete") { +// // avoid showing completions for the completion server +// continue; +// } +// const fullCommandParts = fullCommandName.split(" "); +// const commandParts: { type: "command"; value: string }[] = c.name +// .split(" ") +// .map((part) => ({ type: "command", value: part })); +// const args: { +// type: "positional"; +// position: number; +// value: Positional; +// }[] = +// positionalMap.get(c.name)?.map((arg, i) => ({ +// type: "positional", +// position: i, +// value: arg, +// })) ?? []; +// const parts = [...commandParts, ...args]; + +// for (let i = 0; i < parts.length; i++) { +// const fullCommandPart = fullCommandParts[i]; +// const part = parts[i]; + +// if (part.type === "command") { +// // Command part matching +// if (part.value === fullCommandPart) { +// // Command part matches user input, continue to next part +// continue; +// } else if ( +// !fullCommandPart || +// part.value.startsWith(fullCommandPart) +// ) { +// // User is typing this command part, provide completion +// completions.push(`${part.value}\t${c.description ?? ""}`); +// } +// // Command part does not match, break +// break; +// } else if (part.type === "positional") { +// const positional = part.value; +// // Positional argument handling +// if (part.value.variadic) { +// const comps = await positional.completion( +// previousArgs, +// toComplete +// ); +// completions.push( +// ...comps.map( +// (comp) => `${comp.action}\t${comp.description ?? ""}` +// ) +// ); +// break; +// } +// if (typeof fullCommandPart !== "undefined") { +// // User has provided input for this positional argument +// if (i === fullCommandParts.length - 1 && !endsWithSpace) { +// // User is still typing this positional argument, provide completions +// const comps = await positional.completion( +// previousArgs, +// toComplete +// ); +// completions.push( +// ...comps.map( +// (comp) => `${comp.action}\t${comp.description ?? ""}` +// ) +// ); +// break; +// } else { +// // Positional argument is already provided, move to next +// previousArgs.push(fullCommandPart); +// continue; +// } +// } else { +// // User has not provided input for this positional argument +// const comps = await positional.completion( +// previousArgs, +// toComplete +// ); +// completions.push( +// ...comps.map( +// (comp) => `${comp.action}\t${comp.description ?? ""}` +// ) +// ); +// break; +// } +// } +// } +// } +// } + +// // Output completions +// for (const comp of completions) { +// console.log(comp.split("\n")[0].trim()); +// } +// console.log(`:${directive}`); +// console.error(`Completion ended with directive: ${directive}`); +// } +// } +// }); +// } diff --git a/src/citty.ts b/src/citty.ts index 2ef4c4f..37ec199 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -6,7 +6,7 @@ import * as powershell from "./powershell"; import { Completion } from "."; import type { ArgsDef, CommandDef, PositionalArgDef, SubCommandsDef } from "citty"; -function quoteIfNeeded(path) { +function quoteIfNeeded(path: string) { return path.includes(" ") ? `'${path}'` : path; } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71c41ee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From e6f3863f7d38275619d3507941a2ac231c448b60 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 16 Jan 2025 14:01:24 +0330 Subject: [PATCH 14/22] update --- .github/workflows/ci.yml | 4 ---- .github/workflows/pkg.pr.new.yml | 2 -- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 052e0fe..7b850b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4.0.0 - with: - version: 8 - name: Set node version to 20 uses: actions/setup-node@v4 @@ -48,8 +46,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4.0.0 - with: - version: 8 - name: Set node version to 20 uses: actions/setup-node@v4 diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 38a1b76..d22de0d 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -11,8 +11,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4.0.0 - with: - version: 8 - name: Set node version to 20 uses: actions/setup-node@v4 From e7c2805b281df0b3a467f1ebed5e1306a1185029 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Thu, 16 Jan 2025 14:03:54 +0330 Subject: [PATCH 15/22] update --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b850b7..671bf35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CLI Completion Tests +name: CI on: push: From fee88ffcc6ac83d32fa48a733f1532488a56c095 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 18 Jan 2025 12:54:06 +0330 Subject: [PATCH 16/22] config --- .eslintrc.json | 12 + .github/workflows/ci.yml | 122 +- .github/workflows/format.yml | 22 + .github/workflows/pkg.pr.new.yml | 50 +- .prettierignore | 3 + .prettierrc | 7 + README.md | 139 +- demo.citty.ts | 254 ++-- package.json | 9 +- pnpm-lock.yaml | 2090 +++++++++++++++++++++++++++--- shadcn.cac.ts | 3 +- src/bash.ts | 3 +- src/citty.ts | 349 ++--- src/fish.ts | 3 +- src/index.ts | 577 +++++---- src/powershell.ts | 550 ++++---- src/shared.ts | 1 - src/zsh.ts | 3 +- tests/cli.test.ts | 266 ++-- tsconfig.json | 222 ++-- 20 files changed, 3257 insertions(+), 1428 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .github/workflows/format.yml create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8249554 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "rules": { + "prettier/prettier": "error" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671bf35..0ceac30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,61 +1,61 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - name: Tests - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4.0.0 - - - name: Set node version to 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: https://registry.npmjs.org/ - cache: "pnpm" - - - name: Install deps - run: pnpm install - - - name: Run tests - run: pnpm test - - typecheck: - name: Type Check - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4.0.0 - - - name: Set node version to 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: https://registry.npmjs.org/ - cache: pnpm - - - name: Install deps - run: pnpm install - - - name: Type-check - run: pnpm type-check +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: Tests + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Set node version to 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + + - name: Install deps + run: pnpm install + + - name: Run tests + run: pnpm test + + typecheck: + name: Type Check + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Set node version to 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: pnpm + + - name: Install deps + run: pnpm install + + - name: Type-check + run: pnpm type-check diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..6931fec --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: Prettier Check + +on: [push, pull_request] + +jobs: + prettier: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install dependencies + run: npm install + + - name: Run Prettier Check + run: npm run format:check diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index d22de0d..32bce5d 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -1,25 +1,25 @@ -name: pkg.pr.new -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4.0.0 - - - name: Set node version to 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: https://registry.npmjs.org/ - cache: "pnpm" - - - name: Install deps - run: pnpm install - - - run: pnpx pkg-pr-new publish +name: pkg.pr.new +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Set node version to 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: 'pnpm' + + - name: Install deps + run: pnpm install + + - run: pnpx pkg-pr-new publish diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..01877fb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +dist +build diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..22e285c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} diff --git a/README.md b/README.md index f25aa21..8b1029c 100644 --- a/README.md +++ b/README.md @@ -2,49 +2,57 @@ # tab -Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool. +Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool. -Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces. +Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces. ```ts -import { Completion, script } from "@bombsh/tab"; - -const name = "my-cli" -const completion = new Completion() - -completion.addCommand("start", "Start the application", async (previousArgs, toComplete, endsWithSpace) => { - // suggestions - return [ - { value: "dev", description: "Start in development mode" }, - { value: "prod", description: "Start in production mode" } - ] -}) - -completion.addOption("start", "--port", "Specify the port number", async (previousArgs, toComplete, endsWithSpace) => { - return [ - { value: "3000", description: "Development port" }, - { value: "8080", description: "Production port" } - ] -}) - +import { Completion, script } from '@bombsh/tab'; + +const name = 'my-cli'; +const completion = new Completion(); + +completion.addCommand( + 'start', + 'Start the application', + async (previousArgs, toComplete, endsWithSpace) => { + // suggestions + return [ + { value: 'dev', description: 'Start in development mode' }, + { value: 'prod', description: 'Start in production mode' }, + ]; + } +); + +completion.addOption( + 'start', + '--port', + 'Specify the port number', + async (previousArgs, toComplete, endsWithSpace) => { + return [ + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; + } +); // a way of getting the executable path to pass to the shell autocompletion script function quoteIfNeeded(path: string) { - return path.includes(" ") ? `'${path}'` : path; + return path.includes(' ') ? `'${path}'` : path; } const execPath = process.execPath; const processArgs = process.argv.slice(1); const quotedExecPath = quoteIfNeeded(execPath); const quotedProcessArgs = processArgs.map(quoteIfNeeded); const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); -const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`; -if (process.argv[2] === "--") { +if (process.argv[2] === '--') { // autocompletion logic - await completion.parse(process.argv.slice(2), "start") // TODO: remove "start" + await completion.parse(process.argv.slice(2), 'start'); // TODO: remove "start" } else { // process.argv[2] can be "zsh", "bash", "fish", "powershell" - script(process.argv[2], name, x) + script(process.argv[2], name, x); } ``` @@ -57,31 +65,34 @@ Since we are heavy users of tools like `cac` and `citty`, we have created adapte ### `@bombsh/tab/cac` ```ts -import cac from "cac"; -import tab from "@bombsh/tab/cac"; +import cac from 'cac'; +import tab from '@bombsh/tab/cac'; -const cli = cac("my-cli"); +const cli = cac('my-cli'); -cli - .command("dev", "Start dev server") - .option("--port ", "Specify port"); +cli.command('dev', 'Start dev server').option('--port ', 'Specify port'); const completion = tab(cli); // Get the dev command completion handler -const devCommandCompletion = completion.commands.get("dev"); +const devCommandCompletion = completion.commands.get('dev'); // Get and configure the port option completion handler -const portOptionCompletion = devCommandCompletion.options.get("--port"); -portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { +const portOptionCompletion = devCommandCompletion.options.get('--port'); +portOptionCompletion.handler = async ( + previousArgs, + toComplete, + endsWithSpace +) => { return [ - { value: "3000", description: "Development port" }, - { value: "8080", description: "Production port" } - ] -} + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}; cli.parse(); ``` + Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `my-cli dev --po`, they will get suggestions for the `--port` option. Or if they write `my-cli d` they will get suggestions for the `dev` command. Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation. @@ -89,43 +100,47 @@ Suggestions are missing in the adapters since yet cac or citty do not have a way ### `@bombsh/tab/citty` ```ts -import citty, { defineCommand, createMain } from "citty"; -import tab from "@bombsh/tab/citty"; +import citty, { defineCommand, createMain } from 'citty'; +import tab from '@bombsh/tab/citty'; const main = defineCommand({ meta: { - name: "my-cli", - description: "My CLI tool", + name: 'my-cli', + description: 'My CLI tool', }, }); const devCommand = defineCommand({ meta: { - name: "dev", - description: "Start dev server", + name: 'dev', + description: 'Start dev server', }, args: { - port: { type: "string", description: "Specify port" }, - } + port: { type: 'string', description: 'Specify port' }, + }, }); main.subCommands = { - dev: devCommand + dev: devCommand, }; const completion = await tab(main); // TODO: addHandler function to export -const devCommandCompletion = completion.commands.get("dev"); +const devCommandCompletion = completion.commands.get('dev'); -const portOptionCompletion = devCommandCompletion.options.get("--port"); +const portOptionCompletion = devCommandCompletion.options.get('--port'); -portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => { +portOptionCompletion.handler = async ( + previousArgs, + toComplete, + endsWithSpace +) => { return [ - { value: "3000", description: "Development port" }, - { value: "8080", description: "Production port" } - ] -} + { value: '3000', description: 'Development port' }, + { value: '8080', description: 'Production port' }, + ]; +}; const cli = createMain(main); cli(); @@ -148,24 +163,26 @@ By integrating tab into your cli, your cli would have a new command called `comp ```zsh my-cli complete -- --po ---port Specify the port number -:0 +--port Specify the port number +:0 ``` The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`). -In situations like `my-cli dev --po` you'd have autocompletions! But in the case of `pnpm my-cli dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. +In situations like `my-cli dev --po` you'd have autocompletions! But in the case of `pnpm my-cli dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm. Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available. This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box. -Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support. +Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support. + +## Inspiration -## Inspiration - git - [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go), without cobra, tab would have took 10x longer to build ## TODO + - [] fish - [] bash diff --git a/demo.citty.ts b/demo.citty.ts index d780c24..b72c296 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -1,124 +1,130 @@ -import { defineCommand, createMain, CommandDef } from "citty"; -import tab from "./src/citty"; - -const main = defineCommand({ - meta: { - name: "vite", - description: "Vite CLI tool", - }, - args: { - config: { type: "string", description: "Use specified config file", alias: "c" }, - mode: { type: "string", description: "Set env mode", alias: "m" }, - logLevel: { type: "string", description: "info | warn | error | silent", alias: "l" }, - }, - run(_ctx) { - } -}); - -const devCommand = defineCommand({ - meta: { - name: "dev", - description: "Start dev server", - }, - args: { - host: { type: "string", description: "Specify hostname" }, - port: { type: "string", description: "Specify port" }, - }, - run(ctx) { - console.log('dev', ctx) - }, -}); - -devCommand.subCommands = { - build: defineCommand({ - meta: { - name: "build", - description: "Build project", - }, - run({ args }) { }, - }) -} - -const lintCommand = defineCommand({ - meta: { - name: "lint", - description: "Lint project", - }, - args: { - files: { type: "positional", description: "Files to lint" }, - }, - run(ctx) { - console.log('lint', ctx.cmd.args) - }, -}); - -main.subCommands = { - dev: devCommand, - lint: lintCommand - -} as Record>; - -const completion = await tab(main) - -for (const command of completion.commands.values()) { - console.log(command) - - if (command.name === 'lint') { - console.log('lint') - command.handler = () => { - return [ - { value: "main.ts", description: "Main file" }, - { value: "index.ts", description: "Index file" }, - ] - } - } - - for (const [o, config] of command.options.entries()) { - if (o === "--port") { - config.handler = () => { - return [ - { value: "3000", description: "Development server port" }, - { value: "8080", description: "Alternative port" }, - ] - } - } - if (o === "--host") { - config.handler = () => { - return [ - { value: "localhost", description: "Localhost" }, - { value: "0.0.0.0", description: "All interfaces" }, - ] - } - } - if (o === "--config") { - config.handler = () => { - return [ - { value: "vite.config.ts", description: "Vite config file" }, - { value: "vite.config.js", description: "Vite config file" }, - ] - } - } - if (o === "--mode") { - config.handler = () => { - return [ - { value: "development", description: "Development mode" }, - { value: "production", description: "Production mode" }, - ] - } - } - if (o === "--logLevel") { - config.handler = () => { - return [ - { value: "info", description: "Info level" }, - { value: "warn", description: "Warn level" }, - { value: "error", description: "Error level" }, - { value: "silent", description: "Silent level" }, - ] - } - } - } -} - -const cli = createMain(main); - -cli(); +import { defineCommand, createMain, CommandDef } from 'citty'; +import tab from './src/citty'; + +const main = defineCommand({ + meta: { + name: 'vite', + description: 'Vite CLI tool', + }, + args: { + config: { + type: 'string', + description: 'Use specified config file', + alias: 'c', + }, + mode: { type: 'string', description: 'Set env mode', alias: 'm' }, + logLevel: { + type: 'string', + description: 'info | warn | error | silent', + alias: 'l', + }, + }, + run(_ctx) {}, +}); + +const devCommand = defineCommand({ + meta: { + name: 'dev', + description: 'Start dev server', + }, + args: { + host: { type: 'string', description: 'Specify hostname' }, + port: { type: 'string', description: 'Specify port' }, + }, + run(ctx) { + console.log('dev', ctx); + }, +}); + +devCommand.subCommands = { + build: defineCommand({ + meta: { + name: 'build', + description: 'Build project', + }, + run({ args }) {}, + }), +}; + +const lintCommand = defineCommand({ + meta: { + name: 'lint', + description: 'Lint project', + }, + args: { + files: { type: 'positional', description: 'Files to lint' }, + }, + run(ctx) { + console.log('lint', ctx.cmd.args); + }, +}); + +main.subCommands = { + dev: devCommand, + lint: lintCommand, +} as Record>; + +const completion = await tab(main); + +for (const command of completion.commands.values()) { + console.log(command); + + if (command.name === 'lint') { + console.log('lint'); + command.handler = () => { + return [ + { value: 'main.ts', description: 'Main file' }, + { value: 'index.ts', description: 'Index file' }, + ]; + }; + } + + for (const [o, config] of command.options.entries()) { + if (o === '--port') { + config.handler = () => { + return [ + { value: '3000', description: 'Development server port' }, + { value: '8080', description: 'Alternative port' }, + ]; + }; + } + if (o === '--host') { + config.handler = () => { + return [ + { value: 'localhost', description: 'Localhost' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; + }; + } + if (o === '--config') { + config.handler = () => { + return [ + { value: 'vite.config.ts', description: 'Vite config file' }, + { value: 'vite.config.js', description: 'Vite config file' }, + ]; + }; + } + if (o === '--mode') { + config.handler = () => { + return [ + { value: 'development', description: 'Development mode' }, + { value: 'production', description: 'Production mode' }, + ]; + }; + } + if (o === '--logLevel') { + config.handler = () => { + return [ + { value: 'info', description: 'Info level' }, + { value: 'warn', description: 'Warn level' }, + { value: 'error', description: 'Error level' }, + { value: 'silent', description: 'Silent level' }, + ]; + }; + } + } +} + +const cli = createMain(main); + +cli(); diff --git a/package.json b/package.json index 3b84c26..c0732e5 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,22 @@ "type": "module", "scripts": { "test": "vitest", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/node": "^22.7.4", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", "cac": "^6.7.14", "citty": "^0.1.6", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "prettier": "^3.4.2", "tsx": "^4.19.1", "vitest": "^2.1.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a5dd87..ece142f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: dependencies: mri: @@ -15,12 +14,27 @@ importers: '@types/node': specifier: ^22.7.4 version: 22.7.5 + '@typescript-eslint/eslint-plugin': + specifier: ^8.20.0 + version: 8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^8.20.0 + version: 8.20.0(eslint@9.18.0)(typescript@5.7.3) cac: specifier: ^6.7.14 version: 6.7.14 citty: specifier: ^0.1.6 version: 0.1.6 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.0.1(eslint@9.18.0) + eslint-plugin-prettier: + specifier: ^5.2.2 + version: 5.2.2(eslint-config-prettier@10.0.1(eslint@9.18.0))(eslint@9.18.0)(prettier@3.4.2) + prettier: + specifier: ^3.4.2 + version: 3.4.2 tsx: specifier: ^4.19.1 version: 4.19.1 @@ -29,393 +43,800 @@ importers: version: 2.1.3(@types/node@22.7.5) packages: - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==, + } + engines: { node: '>=12' } cpu: [ppc64] os: [aix] '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==, + } + engines: { node: '>=18' } cpu: [ppc64] os: [aix] '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==, + } + engines: { node: '>=12' } cpu: [arm64] os: [android] '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==, + } + engines: { node: '>=18' } cpu: [arm64] os: [android] '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==, + } + engines: { node: '>=12' } cpu: [arm] os: [android] '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==, + } + engines: { node: '>=18' } cpu: [arm] os: [android] '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==, + } + engines: { node: '>=12' } cpu: [x64] os: [android] '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==, + } + engines: { node: '>=18' } cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==, + } + engines: { node: '>=12' } cpu: [arm64] os: [darwin] '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==, + } + engines: { node: '>=18' } cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==, + } + engines: { node: '>=12' } cpu: [x64] os: [darwin] '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==, + } + engines: { node: '>=18' } cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==, + } + engines: { node: '>=12' } cpu: [arm64] os: [freebsd] '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==, + } + engines: { node: '>=18' } cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [freebsd] '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==, + } + engines: { node: '>=18' } cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==, + } + engines: { node: '>=12' } cpu: [arm64] os: [linux] '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==, + } + engines: { node: '>=18' } cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==, + } + engines: { node: '>=12' } cpu: [arm] os: [linux] '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==, + } + engines: { node: '>=18' } cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==, + } + engines: { node: '>=12' } cpu: [ia32] os: [linux] '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==, + } + engines: { node: '>=18' } cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==, + } + engines: { node: '>=12' } cpu: [loong64] os: [linux] '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==, + } + engines: { node: '>=18' } cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==, + } + engines: { node: '>=12' } cpu: [mips64el] os: [linux] '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==, + } + engines: { node: '>=18' } cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==, + } + engines: { node: '>=12' } cpu: [ppc64] os: [linux] '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==, + } + engines: { node: '>=18' } cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==, + } + engines: { node: '>=12' } cpu: [riscv64] os: [linux] '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==, + } + engines: { node: '>=18' } cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==, + } + engines: { node: '>=12' } cpu: [s390x] os: [linux] '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==, + } + engines: { node: '>=18' } cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==, + } + engines: { node: '>=12' } cpu: [x64] os: [linux] '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==, + } + engines: { node: '>=18' } cpu: [x64] os: [linux] '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==, + } + engines: { node: '>=12' } cpu: [x64] os: [netbsd] '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==, + } + engines: { node: '>=18' } cpu: [x64] os: [netbsd] '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==, + } + engines: { node: '>=18' } cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==, + } + engines: { node: '>=12' } cpu: [x64] os: [openbsd] '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==, + } + engines: { node: '>=18' } cpu: [x64] os: [openbsd] '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==, + } + engines: { node: '>=12' } cpu: [x64] os: [sunos] '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==, + } + engines: { node: '>=18' } cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==, + } + engines: { node: '>=12' } cpu: [arm64] os: [win32] '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==, + } + engines: { node: '>=18' } cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==, + } + engines: { node: '>=12' } cpu: [ia32] os: [win32] '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==, + } + engines: { node: '>=18' } cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==, + } + engines: { node: '>=12' } cpu: [x64] os: [win32] '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==, + } + engines: { node: '>=18' } cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.4.1': + resolution: + { + integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: + { + integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + + '@eslint/config-array@0.19.1': + resolution: + { + integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@eslint/core@0.10.0': + resolution: + { + integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@eslint/eslintrc@3.2.0': + resolution: + { + integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@eslint/js@9.18.0': + resolution: + { + integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@eslint/object-schema@2.1.5': + resolution: + { + integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@eslint/plugin-kit@0.2.5': + resolution: + { + integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@humanfs/core@0.19.1': + resolution: + { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, + } + engines: { node: '>=18.18.0' } + + '@humanfs/node@0.16.6': + resolution: + { + integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==, + } + engines: { node: '>=18.18.0' } + + '@humanwhocodes/module-importer@1.0.1': + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: '>=12.22' } + + '@humanwhocodes/retry@0.3.1': + resolution: + { + integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==, + } + engines: { node: '>=18.18' } + + '@humanwhocodes/retry@0.4.1': + resolution: + { + integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==, + } + engines: { node: '>=18.18' } + '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + resolution: + { + integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, + } + + '@nodelib/fs.scandir@2.1.5': + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: '>= 8' } + + '@nodelib/fs.stat@2.0.5': + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: '>= 8' } + + '@nodelib/fs.walk@1.2.8': + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: '>= 8' } + + '@pkgr/core@0.1.1': + resolution: + { + integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==, + } + engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } '@rollup/rollup-android-arm-eabi@4.24.2': - resolution: {integrity: sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==} + resolution: + { + integrity: sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA==, + } cpu: [arm] os: [android] '@rollup/rollup-android-arm64@4.24.2': - resolution: {integrity: sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==} + resolution: + { + integrity: sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA==, + } cpu: [arm64] os: [android] '@rollup/rollup-darwin-arm64@4.24.2': - resolution: {integrity: sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==} + resolution: + { + integrity: sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg==, + } cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-x64@4.24.2': - resolution: {integrity: sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==} + resolution: + { + integrity: sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA==, + } cpu: [x64] os: [darwin] '@rollup/rollup-freebsd-arm64@4.24.2': - resolution: {integrity: sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==} + resolution: + { + integrity: sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw==, + } cpu: [arm64] os: [freebsd] '@rollup/rollup-freebsd-x64@4.24.2': - resolution: {integrity: sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==} + resolution: + { + integrity: sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg==, + } cpu: [x64] os: [freebsd] '@rollup/rollup-linux-arm-gnueabihf@4.24.2': - resolution: {integrity: sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==} + resolution: + { + integrity: sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==, + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm-musleabihf@4.24.2': - resolution: {integrity: sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==} + resolution: + { + integrity: sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==, + } cpu: [arm] os: [linux] '@rollup/rollup-linux-arm64-gnu@4.24.2': - resolution: {integrity: sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==} + resolution: + { + integrity: sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==, + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-arm64-musl@4.24.2': - resolution: {integrity: sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==} + resolution: + { + integrity: sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==, + } cpu: [arm64] os: [linux] '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': - resolution: {integrity: sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==} + resolution: + { + integrity: sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==, + } cpu: [ppc64] os: [linux] '@rollup/rollup-linux-riscv64-gnu@4.24.2': - resolution: {integrity: sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==} + resolution: + { + integrity: sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==, + } cpu: [riscv64] os: [linux] '@rollup/rollup-linux-s390x-gnu@4.24.2': - resolution: {integrity: sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==} + resolution: + { + integrity: sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==, + } cpu: [s390x] os: [linux] '@rollup/rollup-linux-x64-gnu@4.24.2': - resolution: {integrity: sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==} + resolution: + { + integrity: sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==, + } cpu: [x64] os: [linux] '@rollup/rollup-linux-x64-musl@4.24.2': - resolution: {integrity: sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==} + resolution: + { + integrity: sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==, + } cpu: [x64] os: [linux] '@rollup/rollup-win32-arm64-msvc@4.24.2': - resolution: {integrity: sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==} + resolution: + { + integrity: sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==, + } cpu: [arm64] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.24.2': - resolution: {integrity: sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==} + resolution: + { + integrity: sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw==, + } cpu: [ia32] os: [win32] '@rollup/rollup-win32-x64-msvc@4.24.2': - resolution: {integrity: sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==} + resolution: + { + integrity: sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA==, + } cpu: [x64] os: [win32] '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + resolution: + { + integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==, + } + + '@types/json-schema@7.0.15': + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + resolution: + { + integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==, + } + + '@typescript-eslint/eslint-plugin@8.20.0': + resolution: + { + integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/parser@8.20.0': + resolution: + { + integrity: sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/scope-manager@8.20.0': + resolution: + { + integrity: sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@typescript-eslint/type-utils@8.20.0': + resolution: + { + integrity: sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/types@8.20.0': + resolution: + { + integrity: sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + '@typescript-eslint/typescript-estree@8.20.0': + resolution: + { + integrity: sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.20.0': + resolution: + { + integrity: sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.20.0': + resolution: + { + integrity: sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } '@vitest/expect@2.1.3': - resolution: {integrity: sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==} + resolution: + { + integrity: sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==, + } '@vitest/mocker@2.1.3': - resolution: {integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==} + resolution: + { + integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==, + } peerDependencies: '@vitest/spy': 2.1.3 msw: ^2.3.5 @@ -427,46 +848,182 @@ packages: optional: true '@vitest/pretty-format@2.1.3': - resolution: {integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==} + resolution: + { + integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==, + } '@vitest/runner@2.1.3': - resolution: {integrity: sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==} + resolution: + { + integrity: sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==, + } '@vitest/snapshot@2.1.3': - resolution: {integrity: sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==} + resolution: + { + integrity: sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==, + } '@vitest/spy@2.1.3': - resolution: {integrity: sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==} + resolution: + { + integrity: sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==, + } '@vitest/utils@2.1.3': - resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==} + resolution: + { + integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==, + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: + { + integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==, + } + engines: { node: '>=0.4.0' } + hasBin: true + + ajv@6.12.6: + resolution: + { + integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, + } + + ansi-styles@4.3.0: + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: '>=8' } + + argparse@2.0.1: + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: '>=12' } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + brace-expansion@1.1.11: + resolution: + { + integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, + } + + brace-expansion@2.0.1: + resolution: + { + integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==, + } + + braces@3.0.3: + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, + } + engines: { node: '>=8' } cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, + } + engines: { node: '>=8' } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: '>=6' } chai@5.1.2: - resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==, + } + engines: { node: '>=12' } + + chalk@4.1.2: + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: '>=10' } check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} + resolution: + { + integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==, + } + engines: { node: '>= 16' } citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + resolution: + { + integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==, + } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: '>=7.0.0' } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} + resolution: + { + integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==, + } + engines: { node: ^14.18.0 || >=16.10.0 } + + cross-spawn@7.0.6: + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: '>= 8' } debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==, + } + engines: { node: '>=6.0' } peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -474,117 +1031,742 @@ packages: optional: true deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, + } + engines: { node: '>=6' } + + deep-is@0.1.4: + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==, + } + engines: { node: '>=12' } hasBin: true esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==, + } + engines: { node: '>=18' } hasBin: true + escape-string-regexp@4.0.0: + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: '>=10' } + + eslint-config-prettier@10.0.1: + resolution: + { + integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==, + } + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.2: + resolution: + { + integrity: sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==, + } + engines: { node: ^14.18.0 || >=16.0.0 } + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.2.0: + resolution: + { + integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint-visitor-keys@3.4.3: + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + + eslint-visitor-keys@4.2.0: + resolution: + { + integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + eslint@9.18.0: + resolution: + { + integrity: sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: + { + integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + + esquery@1.6.0: + resolution: + { + integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, + } + engines: { node: '>=0.10' } + + esrecurse@4.3.0: + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: '>=4.0' } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: '>=4.0' } + estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } + + esutils@2.0.3: + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: '>=0.10.0' } + + fast-deep-equal@3.1.3: + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } + + fast-diff@1.3.0: + resolution: + { + integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==, + } + + fast-glob@3.3.3: + resolution: + { + integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, + } + engines: { node: '>=8.6.0' } + + fast-json-stable-stringify@2.1.0: + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } + + fast-levenshtein@2.0.6: + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } + + fastq@1.18.0: + resolution: + { + integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==, + } + + file-entry-cache@8.0.0: + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: '>=16.0.0' } + + fill-range@7.1.1: + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, + } + engines: { node: '>=8' } + + find-up@5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: '>=10' } + + flat-cache@4.0.1: + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: '>=16' } + + flatted@3.3.2: + resolution: + { + integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==, + } fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] get-tsconfig@4.8.1: - resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + resolution: + { + integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==, + } + + glob-parent@5.1.2: + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: '>= 6' } + + glob-parent@6.0.2: + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: '>=10.13.0' } + + globals@14.0.0: + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: '>=18' } + + graphemer@1.4.0: + resolution: + { + integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, + } + + has-flag@4.0.0: + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: '>=8' } + + ignore@5.3.2: + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: '>= 4' } + + import-fresh@3.3.0: + resolution: + { + integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==, + } + engines: { node: '>=6' } + + imurmurhash@0.1.4: + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: '>=0.8.19' } + + is-extglob@2.1.1: + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: '>=0.10.0' } + + is-glob@4.0.3: + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: '>=0.10.0' } + + is-number@7.0.0: + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: '>=0.12.0' } + + isexe@2.0.0: + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } + + js-yaml@4.1.0: + resolution: + { + integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, + } + hasBin: true + + json-buffer@3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + + json-schema-traverse@0.4.1: + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } + + json-stable-stringify-without-jsonify@1.0.1: + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } + + keyv@4.5.4: + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } + + levn@0.4.1: + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: '>= 0.8.0' } + + locate-path@6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: '>=10' } + + lodash.merge@4.6.2: + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + resolution: + { + integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==, + } magic-string@0.30.12: - resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + resolution: + { + integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==, + } + + merge2@1.4.1: + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: '>= 8' } + + micromatch@4.0.8: + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, + } + engines: { node: '>=8.6' } + + minimatch@3.1.2: + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } + + minimatch@9.0.5: + resolution: + { + integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, + } + engines: { node: '>=16 || 14 >=14.17' } mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, + } + engines: { node: '>=4' } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + natural-compare@1.4.0: + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } + + optionator@0.9.4: + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: '>= 0.8.0' } + + p-limit@3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: '>=10' } + + p-locate@5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: '>=10' } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: '>=6' } + + path-exists@4.0.0: + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: '>=8' } + + path-key@3.1.1: + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: '>=8' } + pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + resolution: + { + integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, + } pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} + resolution: + { + integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==, + } + engines: { node: '>= 14.16' } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + picomatch@2.3.1: + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, + } + engines: { node: '>=8.6' } postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==, + } + engines: { node: ^10 || ^12 || >=14 } + + prelude-ls@1.2.1: + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: '>= 0.8.0' } + + prettier-linter-helpers@1.0.0: + resolution: + { + integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==, + } + engines: { node: '>=6.0.0' } + + prettier@3.4.2: + resolution: + { + integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==, + } + engines: { node: '>=14' } + hasBin: true + + punycode@2.3.1: + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: '>=6' } + + queue-microtask@1.2.3: + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: '>=4' } resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolution: + { + integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, + } + + reusify@1.0.4: + resolution: + { + integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==, + } + engines: { iojs: '>=1.0.0', node: '>=0.10.0' } rollup@4.24.2: - resolution: {integrity: sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww==, + } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true + run-parallel@1.2.0: + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } + + semver@7.6.3: + resolution: + { + integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==, + } + engines: { node: '>=10' } + hasBin: true + + shebang-command@2.0.0: + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: '>=8' } + + shebang-regex@3.0.0: + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: '>=8' } + siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, + } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: '>=0.10.0' } stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, + } std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + resolution: + { + integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==, + } + + strip-json-comments@3.1.1: + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: '>=8' } + + supports-color@7.2.0: + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: '>=8' } + + synckit@0.9.2: + resolution: + { + integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==, + } + engines: { node: ^14.18.0 || >=16.0.0 } tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, + } tinyexec@0.3.1: - resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + resolution: + { + integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==, + } tinypool@1.0.1: - resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==, + } + engines: { node: '>=14.0.0' } tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==, + } + engines: { node: '>=14.0.0' } + + to-regex-range@5.0.1: + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: '>=8.0' } + + ts-api-utils@2.0.0: + resolution: + { + integrity: sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==, + } + engines: { node: '>=18.12' } + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } tsx@4.19.1: - resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} - engines: {node: '>=18.0.0'} + resolution: + { + integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==, + } + engines: { node: '>=18.0.0' } + hasBin: true + + type-check@0.4.0: + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: '>= 0.8.0' } + + typescript@5.7.3: + resolution: + { + integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==, + } + engines: { node: '>=14.17' } hasBin: true undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + resolution: + { + integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, + } + + uri-js@4.4.1: + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } vite-node@2.1.3: - resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true vite@5.4.10: - resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@types/node': ^18.0.0 || >=20.0.0 @@ -614,8 +1796,11 @@ packages: optional: true vitest@2.1.3: - resolution: {integrity: sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -638,13 +1823,37 @@ packages: jsdom: optional: true + which@2.0.2: + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: '>= 8' } + hasBin: true + why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, + } + engines: { node: '>=8' } hasBin: true -snapshots: + word-wrap@1.2.5: + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: '>=0.10.0' } + + yocto-queue@0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: '>=10' } +snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true @@ -786,8 +1995,77 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@9.18.0)': + dependencies: + eslint: 9.18.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.10.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.18.0': {} + + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.5': + dependencies: + '@eslint/core': 0.10.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 + + '@pkgr/core@0.1.1': {} + '@rollup/rollup-android-arm-eabi@4.24.2': optional: true @@ -844,10 +2122,89 @@ snapshots: '@types/estree@1.0.6': {} + '@types/json-schema@7.0.15': {} + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 + '@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0)(typescript@5.7.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.20.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.20.0 + '@typescript-eslint/type-utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.20.0 + eslint: 9.18.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.20.0 + '@typescript-eslint/types': 8.20.0 + '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.20.0 + debug: 4.3.7 + eslint: 9.18.0 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.20.0': + dependencies: + '@typescript-eslint/types': 8.20.0 + '@typescript-eslint/visitor-keys': 8.20.0 + + '@typescript-eslint/type-utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.20.0(eslint@9.18.0)(typescript@5.7.3) + debug: 4.3.7 + eslint: 9.18.0 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.20.0': {} + + '@typescript-eslint/typescript-estree@8.20.0(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 8.20.0 + '@typescript-eslint/visitor-keys': 8.20.0 + debug: 4.3.7 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 2.0.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.20.0(eslint@9.18.0)(typescript@5.7.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) + '@typescript-eslint/scope-manager': 8.20.0 + '@typescript-eslint/types': 8.20.0 + '@typescript-eslint/typescript-estree': 8.20.0(typescript@5.7.3) + eslint: 9.18.0 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.20.0': + dependencies: + '@typescript-eslint/types': 8.20.0 + eslint-visitor-keys: 4.2.0 + '@vitest/expect@2.1.3': dependencies: '@vitest/spy': 2.1.3 @@ -855,11 +2212,12 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10)': + '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@22.7.5))': dependencies: '@vitest/spy': 2.1.3 estree-walker: 3.0.3 magic-string: 0.30.12 + optionalDependencies: vite: 5.4.10(@types/node@22.7.5) '@vitest/pretty-format@2.1.3': @@ -887,10 +2245,46 @@ snapshots: loupe: 3.1.2 tinyrainbow: 1.2.0 + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + cac@6.7.14: {} + callsites@3.1.0: {} + chai@5.1.2: dependencies: assertion-error: 2.0.1 @@ -899,20 +2293,41 @@ snapshots: loupe: 3.1.2 pathval: 2.0.0 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + check-error@2.1.1: {} citty@0.1.6: dependencies: consola: 3.2.3 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + consola@3.2.3: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.3.7: dependencies: ms: 2.1.3 deep-eql@5.0.2: {} + deep-is@0.1.4: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -966,10 +2381,131 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.0.1(eslint@9.18.0): + dependencies: + eslint: 9.18.0 + + eslint-plugin-prettier@5.2.2(eslint-config-prettier@10.0.1(eslint@9.18.0))(eslint@9.18.0)(prettier@3.4.2): + dependencies: + eslint: 9.18.0 + prettier: 3.4.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + eslint-config-prettier: 10.0.1(eslint@9.18.0) + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.18.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.18.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.10.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.18.0 + '@eslint/plugin-kit': 0.2.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.18.0: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + fsevents@2.3.3: optional: true @@ -977,32 +2513,150 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + loupe@3.1.2: {} magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + mri@1.2.0: {} ms@2.1.3: {} nanoid@3.3.7: {} + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + pathe@1.1.2: {} pathval@2.0.0: {} picocolors@1.1.1: {} + picomatch@2.3.1: {} + postcss@8.4.47: dependencies: nanoid: 3.3.7 picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.4.2: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + reusify@1.0.4: {} + rollup@4.24.2: dependencies: '@types/estree': 1.0.6 @@ -1027,6 +2681,18 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.2 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -1035,6 +2701,17 @@ snapshots: std-env@3.7.0: {} + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + tinybench@2.9.0: {} tinyexec@0.3.1: {} @@ -1045,6 +2722,16 @@ snapshots: tinyspy@3.0.2: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.0.0(typescript@5.7.3): + dependencies: + typescript: 5.7.3 + + tslib@2.8.1: {} + tsx@4.19.1: dependencies: esbuild: 0.23.1 @@ -1052,8 +2739,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.7.3: {} + undici-types@6.19.8: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + vite-node@2.1.3(@types/node@22.7.5): dependencies: cac: 6.7.14 @@ -1073,18 +2770,17 @@ snapshots: vite@5.4.10(@types/node@22.7.5): dependencies: - '@types/node': 22.7.5 esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.2 optionalDependencies: + '@types/node': 22.7.5 fsevents: 2.3.3 vitest@2.1.3(@types/node@22.7.5): dependencies: - '@types/node': 22.7.5 '@vitest/expect': 2.1.3 - '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10) + '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.10(@types/node@22.7.5)) '@vitest/pretty-format': 2.1.3 '@vitest/runner': 2.1.3 '@vitest/snapshot': 2.1.3 @@ -1102,6 +2798,8 @@ snapshots: vite: 5.4.10(@types/node@22.7.5) vite-node: 2.1.3(@types/node@22.7.5) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.7.5 transitivePeerDependencies: - less - lightningcss @@ -1113,7 +2811,15 @@ snapshots: - supports-color - terser + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/shadcn.cac.ts b/shadcn.cac.ts index c890e85..9074d70 100644 --- a/shadcn.cac.ts +++ b/shadcn.cac.ts @@ -85,7 +85,7 @@ // completion: async (previousArgs, toComplete) => { // // if (arg.value === "root") { // return componentChoices -// // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true +// // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true // // .filter((comp) => comp.startsWith(toComplete)) // .map((comp) => ({ action: comp, description: `Add ${comp} component` })); // // } @@ -101,4 +101,3 @@ // tab(cli); // cli.parse(); - diff --git a/src/bash.ts b/src/bash.ts index 3f6a4f1..7514d89 100644 --- a/src/bash.ts +++ b/src/bash.ts @@ -1,2 +1 @@ -export function generate(name: string, exec: string) { -} +export function generate(name: string, exec: string) {} diff --git a/src/citty.ts b/src/citty.ts index 37ec199..e7d049a 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -1,164 +1,185 @@ -import { ArgDef, defineCommand, parseArgs } from "citty"; -import * as zsh from "./zsh"; -import * as bash from "./bash"; -import * as fish from "./fish"; -import * as powershell from "./powershell"; -import { Completion } from "."; -import type { ArgsDef, CommandDef, PositionalArgDef, SubCommandsDef } from "citty"; - -function quoteIfNeeded(path: string) { - return path.includes(" ") ? `'${path}'` : path; -} - -const execPath = process.execPath; -const processArgs = process.argv.slice(1); -const quotedExecPath = quoteIfNeeded(execPath); -const quotedProcessArgs = processArgs.map(quoteIfNeeded); -const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); -const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; - -function isConfigPositional(config: CommandDef) { - return config.args && Object.values(config.args).some(arg => arg.type === 'positional') -} - -async function handleSubCommands( - completion: Completion, - subCommands: SubCommandsDef, - parentCmd?: string -) { - for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { - const config = await resolve(resolvableConfig); - const meta = await resolve(config.meta); - const subCommands = await resolve(config.subCommands) - - if (!meta || typeof meta?.description !== "string") { - throw new Error("Invalid meta or missing description."); - } - const isPositional = isConfigPositional(config) - const name = completion.addCommand(cmd, meta.description, isPositional ? [false] : [], async (previousArgs, toComplete, endsWithSpace) => { - return [] - }, parentCmd); - - // Handle nested subcommands recursively - if (subCommands) { - await handleSubCommands(completion, subCommands, name); - } - - // Handle arguments - if (config.args) { - for (const [argName, argConfig] of Object.entries(config.args)) { - const conf = argConfig as ArgDef; - if (conf.type === 'positional') { - continue - } - completion.addOption( - name, - `--${argName}`, - conf.description ?? "", - async (previousArgs, toComplete, endsWithSpace) => { - return [] - } - ); - } - } - } -} - -export default async function tab(instance: CommandDef) { - const completion = new Completion(); - - const meta = await resolve(instance.meta); - - if (!meta) { - throw new Error("Invalid meta."); - } - const name = meta.name; - if (!name) { - throw new Error("Invalid meta or missing name."); - } - - const subCommands = await resolve(instance.subCommands); - if (!subCommands) { - throw new Error("Invalid or missing subCommands."); - } - - const root = '' - const isPositional = isConfigPositional(instance) - completion.addCommand(root, meta?.description ?? "", isPositional ? [false] : [], async (previousArgs, toComplete, endsWithSpace) => { - return [] - }); - - await handleSubCommands(completion, subCommands); - - // console.log("LOOK HERE", subCommands) - - if (instance.args) { - for (const [argName, argConfig] of Object.entries(instance.args)) { - const conf = argConfig as PositionalArgDef; - completion.addOption( - root, - `--${argName}`, - conf.description ?? "", - async (previousArgs, toComplete, endsWithSpace) => { - return [] - } - ); - } - } - - const completeCommand = defineCommand({ - meta: { - name: "complete", - description: "Generate shell completion scripts", - }, - async run(ctx) { - let shell: string | undefined = ctx.rawArgs[0]; - const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf("--") + 1); - - if (shell === '--') { - shell = undefined; - } - - switch (shell) { - case "zsh": { - const script = zsh.generate(name, x); - console.log(script); - break; - } - case "bash": { - const script = bash.generate(name, x); - console.log(script); - break; - } - case "fish": { - const script = fish.generate(name, x); - console.log(script); - break; - } - case "powershell": { - const script = powershell.generate(name, x); - console.log(script); - break; - } - default: { - const args = (await resolve(instance.args))!; - const parsed = parseArgs(extra, args); - // TODO: this is not ideal at all - const matchedCommand = parsed._.join(' ') - console.log(completion) - return completion.parse(extra, matchedCommand); - } - } - }, - }); - - subCommands.complete = completeCommand - - return completion -} - -type Resolvable = T | Promise | (() => T) | (() => Promise); - -async function resolve(resolvable: Resolvable): Promise { - return resolvable instanceof Function ? await resolvable() : await resolvable; -} \ No newline at end of file +import { ArgDef, defineCommand, parseArgs } from 'citty'; +import * as zsh from './zsh'; +import * as bash from './bash'; +import * as fish from './fish'; +import * as powershell from './powershell'; +import { Completion } from '.'; +import type { + ArgsDef, + CommandDef, + PositionalArgDef, + SubCommandsDef, +} from 'citty'; + +function quoteIfNeeded(path: string) { + return path.includes(' ') ? `'${path}'` : path; +} + +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`; + +function isConfigPositional(config: CommandDef) { + return ( + config.args && + Object.values(config.args).some((arg) => arg.type === 'positional') + ); +} + +async function handleSubCommands( + completion: Completion, + subCommands: SubCommandsDef, + parentCmd?: string +) { + for (const [cmd, resolvableConfig] of Object.entries(subCommands)) { + const config = await resolve(resolvableConfig); + const meta = await resolve(config.meta); + const subCommands = await resolve(config.subCommands); + + if (!meta || typeof meta?.description !== 'string') { + throw new Error('Invalid meta or missing description.'); + } + const isPositional = isConfigPositional(config); + const name = completion.addCommand( + cmd, + meta.description, + isPositional ? [false] : [], + async (previousArgs, toComplete, endsWithSpace) => { + return []; + }, + parentCmd + ); + + // Handle nested subcommands recursively + if (subCommands) { + await handleSubCommands(completion, subCommands, name); + } + + // Handle arguments + if (config.args) { + for (const [argName, argConfig] of Object.entries(config.args)) { + const conf = argConfig as ArgDef; + if (conf.type === 'positional') { + continue; + } + completion.addOption( + name, + `--${argName}`, + conf.description ?? '', + async (previousArgs, toComplete, endsWithSpace) => { + return []; + } + ); + } + } + } +} + +export default async function tab( + instance: CommandDef +) { + const completion = new Completion(); + + const meta = await resolve(instance.meta); + + if (!meta) { + throw new Error('Invalid meta.'); + } + const name = meta.name; + if (!name) { + throw new Error('Invalid meta or missing name.'); + } + + const subCommands = await resolve(instance.subCommands); + if (!subCommands) { + throw new Error('Invalid or missing subCommands.'); + } + + const root = ''; + const isPositional = isConfigPositional(instance); + completion.addCommand( + root, + meta?.description ?? '', + isPositional ? [false] : [], + async (previousArgs, toComplete, endsWithSpace) => { + return []; + } + ); + + await handleSubCommands(completion, subCommands); + + // console.log("LOOK HERE", subCommands) + + if (instance.args) { + for (const [argName, argConfig] of Object.entries(instance.args)) { + const conf = argConfig as PositionalArgDef; + completion.addOption( + root, + `--${argName}`, + conf.description ?? '', + async (previousArgs, toComplete, endsWithSpace) => { + return []; + } + ); + } + } + + const completeCommand = defineCommand({ + meta: { + name: 'complete', + description: 'Generate shell completion scripts', + }, + async run(ctx) { + let shell: string | undefined = ctx.rawArgs[0]; + const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf('--') + 1); + + if (shell === '--') { + shell = undefined; + } + + switch (shell) { + case 'zsh': { + const script = zsh.generate(name, x); + console.log(script); + break; + } + case 'bash': { + const script = bash.generate(name, x); + console.log(script); + break; + } + case 'fish': { + const script = fish.generate(name, x); + console.log(script); + break; + } + case 'powershell': { + const script = powershell.generate(name, x); + console.log(script); + break; + } + default: { + const args = (await resolve(instance.args))!; + const parsed = parseArgs(extra, args); + // TODO: this is not ideal at all + const matchedCommand = parsed._.join(' '); + console.log(completion); + return completion.parse(extra, matchedCommand); + } + } + }, + }); + + subCommands.complete = completeCommand; + + return completion; +} + +type Resolvable = T | Promise | (() => T) | (() => Promise); + +async function resolve(resolvable: Resolvable): Promise { + return resolvable instanceof Function ? await resolvable() : await resolvable; +} diff --git a/src/fish.ts b/src/fish.ts index 3f6a4f1..7514d89 100644 --- a/src/fish.ts +++ b/src/fish.ts @@ -1,2 +1 @@ -export function generate(name: string, exec: string) { -} +export function generate(name: string, exec: string) {} diff --git a/src/index.ts b/src/index.ts index 3f1526a..bf2892b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,271 +1,306 @@ -import * as zsh from "./zsh"; -import * as bash from "./bash"; -import * as fish from "./fish"; -import * as powershell from "./powershell"; - -// ShellCompRequestCmd is the name of the hidden command that is used to request -// completion results from the program. It is used by the shell completion scripts. -export const ShellCompRequestCmd: string = "__complete"; - -// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request -// completion results without their description. It is used by the shell completion scripts. -export const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; - -// ShellCompDirective is a bit map representing the different behaviors the shell -// can be instructed to have once completions have been provided. -export const ShellCompDirective = { - // ShellCompDirectiveError indicates an error occurred and completions should be ignored. - ShellCompDirectiveError: 1 << 0, - - // ShellCompDirectiveNoSpace indicates that the shell should not add a space - // after the completion even if there is a single completion provided. - ShellCompDirectiveNoSpace: 1 << 1, - - // ShellCompDirectiveNoFileComp indicates that the shell should not provide - // file completion even when no completion is provided. - ShellCompDirectiveNoFileComp: 1 << 2, - - // ShellCompDirectiveFilterFileExt indicates that the provided completions - // should be used as file extension filters. - // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() - // is a shortcut to using this directive explicitly. The BashCompFilenameExt - // annotation can also be used to obtain the same behavior for flags. - ShellCompDirectiveFilterFileExt: 1 << 3, - - // ShellCompDirectiveFilterDirs indicates that only directory names should - // be provided in file completion. To request directory names within another - // directory, the returned completions should specify the directory within - // which to search. The BashCompSubdirsInDir annotation can be used to - // obtain the same behavior but only for flags. - ShellCompDirectiveFilterDirs: 1 << 4, - - // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order - // in which the completions are provided. - ShellCompDirectiveKeepOrder: 1 << 5, - - // =========================================================================== - - // All directives using iota (or equivalent in Go) should be above this one. - // For internal use. - shellCompDirectiveMaxValue: 1 << 6, - - // ShellCompDirectiveDefault indicates to let the shell perform its default - // behavior after completions have been provided. - // This one must be last to avoid messing up the iota count. - ShellCompDirectiveDefault: 0, -}; - - -export type Positional = { - required: boolean; - variadic: boolean; - completion: Handler; -}; - -type Item = { - description: string; - value: string; -} - -type Handler = (previousArgs: string[], toComplete: string, endsWithSpace: boolean) => (Item[] | Promise); - -type Option = { - description: string; - handler: Handler; -} - -type Command = { - name: string; - description: string; - handler: Handler; - options: Map; - parent?: Command; -} - -export class Completion { - commands = new Map(); - - // vite [...files] - // args: [false, false, true], only the last argument can be variadic - addCommand(name: string, description: string, args: boolean[], handler: Handler, parent?: string) { - const key = parent ? `${parent} ${name}` : name; - this.commands.set(key, { - name: key, - description, - handler, - options: new Map(), - parent: parent ? this.commands.get(parent)! : undefined, - }); - return key; - } - - // --port - addOption(command: string, option: string, description: string, handler: Handler) { - const cmd = this.commands.get(command); - if (!cmd) { - throw new Error(`Command ${command} not found.`); - } - cmd.options.set(option, { description, handler }); - return option; - } - - async parse(args: string[], potentialCommand: string) { - const matchedCommand = this.commands.get(potentialCommand) ?? this.commands.get('')!; - let directive = ShellCompDirective.ShellCompDirectiveDefault; - const completions: Item[] = []; - - const endsWithSpace = args[args.length - 1] === ""; - if (endsWithSpace) { - args.pop(); - } - - let toComplete = args[args.length - 1] || ""; - const previousArgs = args.slice(0, -1); - - if (previousArgs.length > 0) { - const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg.startsWith("--") && endsWithSpace) { - const { handler } = matchedCommand.options.get(lastPrevArg)!; - if (handler) { - const flagSuggestions = await handler(previousArgs, toComplete, endsWithSpace); - completions.push( - ...flagSuggestions - .filter(comp => comp.value.startsWith(toComplete)) - ); - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - completions.forEach(comp => console.log(comp)); - console.log(`:${directive}`); - return; - } - } - } - - if (toComplete.startsWith("--")) { - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - const equalsIndex = toComplete.indexOf("="); - - if (equalsIndex !== -1) { - const flagName = toComplete.slice(2, equalsIndex); - const valueToComplete = toComplete.slice(equalsIndex + 1); - const { handler } = matchedCommand.options.get(`--${flagName}`)!; - - if (handler) { - const suggestions = await handler(previousArgs, valueToComplete, endsWithSpace); - completions.push(...suggestions) - } - } else if (!endsWithSpace) { - const options = new Map(matchedCommand.options); - - let currentCommand = matchedCommand; - while (currentCommand.parent) { - for (const [key, value] of currentCommand.parent.options) { - if (!options.has(key)) { - options.set(key, value); - } - } - currentCommand = currentCommand.parent; - } - - const specifiedFlags = previousArgs - .filter(arg => arg.startsWith("-")) - .filter(arg => arg.startsWith("--")); - const availableFlags = [...options.keys()] - .filter(flag => !specifiedFlags.includes(flag)) - .filter(flag => flag.startsWith(toComplete)); - - completions.push( - ...availableFlags.map( - flag => ({ value: flag, description: options.get(flag)!.description ?? "" }) - ) - ); - } else { - const { handler } = matchedCommand.options.get(toComplete)!; - - if (handler) { - const suggestions = await handler(previousArgs, toComplete, endsWithSpace); - completions.push(...suggestions) - } - } - } else { - const potentialCommandParts = potentialCommand.split(' ') - for (const [k, v] of this.commands) { - // if the command is root, skip it - if (k === '') { - continue - } - - const parts = k.split(' ') - for (let i = 0; i < parts.length; i++) { - const part = parts[i] - const potentialPart = potentialCommandParts[i] || '' - - // Skip if we've already added this suggestion - const alreadyExists = completions.findIndex(item => item.value === part) !== -1 - if (alreadyExists) { - break - } - - // If we're at the current word being completed - if (i === potentialCommandParts.length - 1) { - // Only add if it matches the current partial input - if (part.startsWith(potentialPart)) { - completions.push({ value: part, description: v.description }) - } - break - } - - // For previous parts, they must match exactly - if (part !== potentialPart) { - break - } - } - } - - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } - // vite [...items] - // vite dev - // vite lint [item] - // vite dev build - - // TODO: prettier (plus check in ci) - // TODO: ci type check - - // TODO: positionals (tomorrow night), this is nearly there! - - // TODO: cac (tomorrow night) - // TODO: check behaviour of the tests (tomorrow night) - - - completions.forEach(comp => console.log(`${comp.value}\t${comp.description ?? ""}`)); - console.log(`:${directive}`); - } -} - -export function script(shell: "zsh" | "bash" | "fish" | "powershell", name: string, x: string) { - switch (shell) { - case "zsh": { - const script = zsh.generate(name, x); - console.log(script); - break; - } - case "bash": { - const script = bash.generate(name, x); - console.log(script); - break; - } - case "fish": { - const script = fish.generate(name, x); - console.log(script); - break; - } - case "powershell": { - const script = powershell.generate(name, x); - console.log(script); - break; - } - default: { - throw new Error(`Unsupported shell: ${shell}`); - } - } -} \ No newline at end of file +import * as zsh from './zsh'; +import * as bash from './bash'; +import * as fish from './fish'; +import * as powershell from './powershell'; + +// ShellCompRequestCmd is the name of the hidden command that is used to request +// completion results from the program. It is used by the shell completion scripts. +export const ShellCompRequestCmd: string = '__complete'; + +// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request +// completion results without their description. It is used by the shell completion scripts. +export const ShellCompNoDescRequestCmd: string = '__completeNoDesc'; + +// ShellCompDirective is a bit map representing the different behaviors the shell +// can be instructed to have once completions have been provided. +export const ShellCompDirective = { + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. + ShellCompDirectiveError: 1 << 0, + + // ShellCompDirectiveNoSpace indicates that the shell should not add a space + // after the completion even if there is a single completion provided. + ShellCompDirectiveNoSpace: 1 << 1, + + // ShellCompDirectiveNoFileComp indicates that the shell should not provide + // file completion even when no completion is provided. + ShellCompDirectiveNoFileComp: 1 << 2, + + // ShellCompDirectiveFilterFileExt indicates that the provided completions + // should be used as file extension filters. + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() + // is a shortcut to using this directive explicitly. The BashCompFilenameExt + // annotation can also be used to obtain the same behavior for flags. + ShellCompDirectiveFilterFileExt: 1 << 3, + + // ShellCompDirectiveFilterDirs indicates that only directory names should + // be provided in file completion. To request directory names within another + // directory, the returned completions should specify the directory within + // which to search. The BashCompSubdirsInDir annotation can be used to + // obtain the same behavior but only for flags. + ShellCompDirectiveFilterDirs: 1 << 4, + + // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order + // in which the completions are provided. + ShellCompDirectiveKeepOrder: 1 << 5, + + // =========================================================================== + + // All directives using iota (or equivalent in Go) should be above this one. + // For internal use. + shellCompDirectiveMaxValue: 1 << 6, + + // ShellCompDirectiveDefault indicates to let the shell perform its default + // behavior after completions have been provided. + // This one must be last to avoid messing up the iota count. + ShellCompDirectiveDefault: 0, +}; + +export type Positional = { + required: boolean; + variadic: boolean; + completion: Handler; +}; + +type Item = { + description: string; + value: string; +}; + +type Handler = ( + previousArgs: string[], + toComplete: string, + endsWithSpace: boolean +) => Item[] | Promise; + +type Option = { + description: string; + handler: Handler; +}; + +type Command = { + name: string; + description: string; + handler: Handler; + options: Map; + parent?: Command; +}; + +export class Completion { + commands = new Map(); + + // vite [...files] + // args: [false, false, true], only the last argument can be variadic + addCommand( + name: string, + description: string, + args: boolean[], + handler: Handler, + parent?: string + ) { + const key = parent ? `${parent} ${name}` : name; + this.commands.set(key, { + name: key, + description, + handler, + options: new Map(), + parent: parent ? this.commands.get(parent)! : undefined, + }); + return key; + } + + // --port + addOption( + command: string, + option: string, + description: string, + handler: Handler + ) { + const cmd = this.commands.get(command); + if (!cmd) { + throw new Error(`Command ${command} not found.`); + } + cmd.options.set(option, { description, handler }); + return option; + } + + async parse(args: string[], potentialCommand: string) { + const matchedCommand = + this.commands.get(potentialCommand) ?? this.commands.get('')!; + let directive = ShellCompDirective.ShellCompDirectiveDefault; + const completions: Item[] = []; + + const endsWithSpace = args[args.length - 1] === ''; + if (endsWithSpace) { + args.pop(); + } + + let toComplete = args[args.length - 1] || ''; + const previousArgs = args.slice(0, -1); + + if (previousArgs.length > 0) { + const lastPrevArg = previousArgs[previousArgs.length - 1]; + if (lastPrevArg.startsWith('--') && endsWithSpace) { + const { handler } = matchedCommand.options.get(lastPrevArg)!; + if (handler) { + const flagSuggestions = await handler( + previousArgs, + toComplete, + endsWithSpace + ); + completions.push( + ...flagSuggestions.filter((comp) => + comp.value.startsWith(toComplete) + ) + ); + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + completions.forEach((comp) => console.log(comp)); + console.log(`:${directive}`); + return; + } + } + } + + if (toComplete.startsWith('--')) { + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + const equalsIndex = toComplete.indexOf('='); + + if (equalsIndex !== -1) { + const flagName = toComplete.slice(2, equalsIndex); + const valueToComplete = toComplete.slice(equalsIndex + 1); + const { handler } = matchedCommand.options.get(`--${flagName}`)!; + + if (handler) { + const suggestions = await handler( + previousArgs, + valueToComplete, + endsWithSpace + ); + completions.push(...suggestions); + } + } else if (!endsWithSpace) { + const options = new Map(matchedCommand.options); + + let currentCommand = matchedCommand; + while (currentCommand.parent) { + for (const [key, value] of currentCommand.parent.options) { + if (!options.has(key)) { + options.set(key, value); + } + } + currentCommand = currentCommand.parent; + } + + const specifiedFlags = previousArgs + .filter((arg) => arg.startsWith('-')) + .filter((arg) => arg.startsWith('--')); + const availableFlags = [...options.keys()] + .filter((flag) => !specifiedFlags.includes(flag)) + .filter((flag) => flag.startsWith(toComplete)); + + completions.push( + ...availableFlags.map((flag) => ({ + value: flag, + description: options.get(flag)!.description ?? '', + })) + ); + } else { + const { handler } = matchedCommand.options.get(toComplete)!; + + if (handler) { + const suggestions = await handler( + previousArgs, + toComplete, + endsWithSpace + ); + completions.push(...suggestions); + } + } + } else { + const potentialCommandParts = potentialCommand.split(' '); + for (const [k, v] of this.commands) { + // if the command is root, skip it + if (k === '') { + continue; + } + + const parts = k.split(' '); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const potentialPart = potentialCommandParts[i] || ''; + + // Skip if we've already added this suggestion + const alreadyExists = + completions.findIndex((item) => item.value === part) !== -1; + if (alreadyExists) { + break; + } + + // If we're at the current word being completed + if (i === potentialCommandParts.length - 1) { + // Only add if it matches the current partial input + if (part.startsWith(potentialPart)) { + completions.push({ value: part, description: v.description }); + } + break; + } + + // For previous parts, they must match exactly + if (part !== potentialPart) { + break; + } + } + } + + directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + } + // vite [...items] + // vite dev + // vite lint [item] + // vite dev build + + // TODO: prettier (plus check in ci) + // TODO: ci type check + + // TODO: positionals (tomorrow night), this is nearly there! + + // TODO: cac (tomorrow night) + // TODO: check behaviour of the tests (tomorrow night) + + completions.forEach((comp) => + console.log(`${comp.value}\t${comp.description ?? ''}`) + ); + console.log(`:${directive}`); + } +} + +export function script( + shell: 'zsh' | 'bash' | 'fish' | 'powershell', + name: string, + x: string +) { + switch (shell) { + case 'zsh': { + const script = zsh.generate(name, x); + console.log(script); + break; + } + case 'bash': { + const script = bash.generate(name, x); + console.log(script); + break; + } + case 'fish': { + const script = fish.generate(name, x); + console.log(script); + break; + } + case 'powershell': { + const script = powershell.generate(name, x); + console.log(script); + break; + } + default: { + throw new Error(`Unsupported shell: ${shell}`); + } + } +} diff --git a/src/powershell.ts b/src/powershell.ts index 22ca425..ffc7f4b 100644 --- a/src/powershell.ts +++ b/src/powershell.ts @@ -1,275 +1,275 @@ -import { ShellCompDirective } from "./"; - -// TODO: issue with -- -- completions - -export function generate( - name: string, - exec: string, - includeDesc = false -): string { - // Replace '-' and ':' with '_' for variable names - const nameForVar = name.replace(/[-:]/g, "_"); - - // Determine the completion command - // const compCmd = includeDesc ? "complete" : "complete"; - - // Shell completion directives - const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError; - const ShellCompDirectiveNoSpace = - ShellCompDirective.ShellCompDirectiveNoSpace; - const ShellCompDirectiveNoFileComp = - ShellCompDirective.ShellCompDirectiveNoFileComp; - const ShellCompDirectiveFilterFileExt = - ShellCompDirective.ShellCompDirectiveFilterFileExt; - const ShellCompDirectiveFilterDirs = - ShellCompDirective.ShellCompDirectiveFilterDirs; - const ShellCompDirectiveKeepOrder = - ShellCompDirective.ShellCompDirectiveKeepOrder; - - return `# powershell completion for ${name} -*- shell-script -*- - - [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - function __${name}_debug { - if ($env:BASH_COMP_DEBUG_FILE) { - "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" - } - } - - filter __${name}_escapeStringWithSpecialChars { - $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|"|\\||<|>|&','\`$&' - } - -[scriptblock]$__${nameForVar}CompleterBlock = { - param( - $WordToComplete, - $CommandAst, - $CursorPosition - ) - - # Get the current command line and convert into a string - $Command = $CommandAst.CommandElements - $Command = "$Command" - - __${name}_debug "" - __${name}_debug "========= starting completion logic ==========" - __${name}_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" - - # The user could have moved the cursor backwards on the command-line. - # We need to trigger completion from the $CursorPosition location, so we need - # to truncate the command-line ($Command) up to the $CursorPosition location. - # Make sure the $Command is longer then the $CursorPosition before we truncate. - # This happens because the $Command does not include the last space. - if ($Command.Length -gt $CursorPosition) { - $Command = $Command.Substring(0, $CursorPosition) - } - __${name}_debug "Truncated command: $Command" - - $ShellCompDirectiveError=${ShellCompDirectiveError} - $ShellCompDirectiveNoSpace=${ShellCompDirectiveNoSpace} - $ShellCompDirectiveNoFileComp=${ShellCompDirectiveNoFileComp} - $ShellCompDirectiveFilterFileExt=${ShellCompDirectiveFilterFileExt} - $ShellCompDirectiveFilterDirs=${ShellCompDirectiveFilterDirs} - $ShellCompDirectiveKeepOrder=${ShellCompDirectiveKeepOrder} - - # Prepare the command to request completions for the program. - # Split the command at the first space to separate the program and arguments. - $Program, $Arguments = $Command.Split(" ", 2) - - $RequestComp = "& ${exec} complete -- $Arguments" - __${name}_debug "RequestComp: $RequestComp" - - # we cannot use $WordToComplete because it - # has the wrong values if the cursor was moved - # so use the last argument - if ($WordToComplete -ne "" ) { - $WordToComplete = $Arguments.Split(" ")[-1] - } - __${name}_debug "New WordToComplete: $WordToComplete" - - - # Check for flag with equal sign - $IsEqualFlag = ($WordToComplete -Like "--*=*" ) - if ( $IsEqualFlag ) { - __${name}_debug "Completing equal sign flag" - # Remove the flag part - $Flag, $WordToComplete = $WordToComplete.Split("=", 2) - } - - if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { - # If the last parameter is complete (there is a space following it) - # We add an extra empty parameter so we can indicate this to the go method. - __${name}_debug "Adding extra empty parameter" - # PowerShell 7.2+ changed the way how the arguments are passed to executables, - # so for pre-7.2 or when Legacy argument passing is enabled we need to use - if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or - ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or - (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and - $PSNativeCommandArgumentPassing -eq 'Legacy')) { - $RequestComp="$RequestComp" + ' \`"\`"' - } else { - $RequestComp = "$RequestComp" + ' ""' - } - } - - __${name}_debug "Calling $RequestComp" - # First disable ActiveHelp which is not supported for Powershell - $env:ActiveHelp = 0 - - # call the command store the output in $out and redirect stderr and stdout to null - # $Out is an array contains each line per element - Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null - - # get directive from last line - [int]$Directive = $Out[-1].TrimStart(':') - if ($Directive -eq "") { - # There is no directive specified - $Directive = 0 - } - __${name}_debug "The completion directive is: $Directive" - - # remove directive (last element) from out - $Out = $Out | Where-Object { $_ -ne $Out[-1] } - __${name}_debug "The completions are: $Out" - - if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { - # Error code. No completion. - __${name}_debug "Received error from custom completion go code" - return - } - - $Longest = 0 - [Array]$Values = $Out | ForEach-Object { - # Split the output in name and description - $Name, $Description = $_.Split("\`t", 2) - __${name}_debug "Name: $Name Description: $Description" - - # Look for the longest completion so that we can format things nicely - if ($Longest -lt $Name.Length) { - $Longest = $Name.Length - } - - # Set the description to a one space string if there is none set. - # This is needed because the CompletionResult does not accept an empty string as argument - if (-Not $Description) { - $Description = " " - } - @{ Name = "$Name"; Description = "$Description" } - } - - - $Space = " " - if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { - # remove the space here - __${name}_debug "ShellCompDirectiveNoSpace is called" - $Space = "" - } - - if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or - (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { - __${name}_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" - - # return here to prevent the completion of the extensions - return - } - - $Values = $Values | Where-Object { - # filter the result - $_.Name -like "$WordToComplete*" - - # Join the flag back if we have an equal sign flag - if ( $IsEqualFlag ) { - __${name}_debug "Join the equal sign flag back to the completion value" - $_.Name = $Flag + "=" + $_.Name - } - } - - # we sort the values in ascending order by name if keep order isn't passed - if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { - $Values = $Values | Sort-Object -Property Name - } - - if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { - __${name}_debug "ShellCompDirectiveNoFileComp is called" - - if ($Values.Length -eq 0) { - # Just print an empty string here so the - # shell does not start to complete paths. - # We cannot use CompletionResult here because - # it does not accept an empty string as argument. - "" - return - } - } - - # Get the current mode - $Mode = (Get-PSReadLineKeyHandler | Where-Object { $_.Key -eq "Tab" }).Function - __${name}_debug "Mode: $Mode" - - $Values | ForEach-Object { - - # store temporary because switch will overwrite $_ - $comp = $_ - - # PowerShell supports three different completion modes - # - TabCompleteNext (default windows style - on each key press the next option is displayed) - # - Complete (works like bash) - # - MenuComplete (works like zsh) - # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function - - # CompletionResult Arguments: - # 1) CompletionText text to be used as the auto completion result - # 2) ListItemText text to be displayed in the suggestion list - # 3) ResultType type of completion result - # 4) ToolTip text for the tooltip with details about the object - - switch ($Mode) { - - # bash like - "Complete" { - - if ($Values.Length -eq 1) { - __${name}_debug "Only one completion left" - - # insert space after value - [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") - - } else { - # Add the proper number of spaces to align the descriptions - while($comp.Name.Length -lt $Longest) { - $comp.Name = $comp.Name + " " - } - - # Check for empty description and only add parentheses if needed - if ($($comp.Description) -eq " " ) { - $Description = "" - } else { - $Description = " ($($comp.Description))" - } - - [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") - } - } - - # zsh like - "MenuComplete" { - # insert space after value - # MenuComplete will automatically show the ToolTip of - # the highlighted value at the bottom of the suggestions. - [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") - } - - # TabCompleteNext and in case we get something unknown - Default { - # Like MenuComplete but we don't want to add a space here because - # the user need to press space anyway to get the completion. - # Description will not be shown because that's not possible with TabCompleteNext - [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") - } - } - - } -} - -Register-ArgumentCompleter -CommandName '${name}' -ScriptBlock $__${nameForVar}CompleterBlock -`; -} +import { ShellCompDirective } from './'; + +// TODO: issue with -- -- completions + +export function generate( + name: string, + exec: string, + includeDesc = false +): string { + // Replace '-' and ':' with '_' for variable names + const nameForVar = name.replace(/[-:]/g, '_'); + + // Determine the completion command + // const compCmd = includeDesc ? "complete" : "complete"; + + // Shell completion directives + const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError; + const ShellCompDirectiveNoSpace = + ShellCompDirective.ShellCompDirectiveNoSpace; + const ShellCompDirectiveNoFileComp = + ShellCompDirective.ShellCompDirectiveNoFileComp; + const ShellCompDirectiveFilterFileExt = + ShellCompDirective.ShellCompDirectiveFilterFileExt; + const ShellCompDirectiveFilterDirs = + ShellCompDirective.ShellCompDirectiveFilterDirs; + const ShellCompDirectiveKeepOrder = + ShellCompDirective.ShellCompDirectiveKeepOrder; + + return `# powershell completion for ${name} -*- shell-script -*- + + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + function __${name}_debug { + if ($env:BASH_COMP_DEBUG_FILE) { + "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" + } + } + + filter __${name}_escapeStringWithSpecialChars { + $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|"|\\||<|>|&','\`$&' + } + +[scriptblock]$__${nameForVar}CompleterBlock = { + param( + $WordToComplete, + $CommandAst, + $CursorPosition + ) + + # Get the current command line and convert into a string + $Command = $CommandAst.CommandElements + $Command = "$Command" + + __${name}_debug "" + __${name}_debug "========= starting completion logic ==========" + __${name}_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CursorPosition location, so we need + # to truncate the command-line ($Command) up to the $CursorPosition location. + # Make sure the $Command is longer then the $CursorPosition before we truncate. + # This happens because the $Command does not include the last space. + if ($Command.Length -gt $CursorPosition) { + $Command = $Command.Substring(0, $CursorPosition) + } + __${name}_debug "Truncated command: $Command" + + $ShellCompDirectiveError=${ShellCompDirectiveError} + $ShellCompDirectiveNoSpace=${ShellCompDirectiveNoSpace} + $ShellCompDirectiveNoFileComp=${ShellCompDirectiveNoFileComp} + $ShellCompDirectiveFilterFileExt=${ShellCompDirectiveFilterFileExt} + $ShellCompDirectiveFilterDirs=${ShellCompDirectiveFilterDirs} + $ShellCompDirectiveKeepOrder=${ShellCompDirectiveKeepOrder} + + # Prepare the command to request completions for the program. + # Split the command at the first space to separate the program and arguments. + $Program, $Arguments = $Command.Split(" ", 2) + + $RequestComp = "& ${exec} complete -- $Arguments" + __${name}_debug "RequestComp: $RequestComp" + + # we cannot use $WordToComplete because it + # has the wrong values if the cursor was moved + # so use the last argument + if ($WordToComplete -ne "" ) { + $WordToComplete = $Arguments.Split(" ")[-1] + } + __${name}_debug "New WordToComplete: $WordToComplete" + + + # Check for flag with equal sign + $IsEqualFlag = ($WordToComplete -Like "--*=*" ) + if ( $IsEqualFlag ) { + __${name}_debug "Completing equal sign flag" + # Remove the flag part + $Flag, $WordToComplete = $WordToComplete.Split("=", 2) + } + + if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __${name}_debug "Adding extra empty parameter" + # PowerShell 7.2+ changed the way how the arguments are passed to executables, + # so for pre-7.2 or when Legacy argument passing is enabled we need to use + if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or + ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or + (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and + $PSNativeCommandArgumentPassing -eq 'Legacy')) { + $RequestComp="$RequestComp" + ' \`"\`"' + } else { + $RequestComp = "$RequestComp" + ' ""' + } + } + + __${name}_debug "Calling $RequestComp" + # First disable ActiveHelp which is not supported for Powershell + $env:ActiveHelp = 0 + + # call the command store the output in $out and redirect stderr and stdout to null + # $Out is an array contains each line per element + Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null + + # get directive from last line + [int]$Directive = $Out[-1].TrimStart(':') + if ($Directive -eq "") { + # There is no directive specified + $Directive = 0 + } + __${name}_debug "The completion directive is: $Directive" + + # remove directive (last element) from out + $Out = $Out | Where-Object { $_ -ne $Out[-1] } + __${name}_debug "The completions are: $Out" + + if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { + # Error code. No completion. + __${name}_debug "Received error from custom completion go code" + return + } + + $Longest = 0 + [Array]$Values = $Out | ForEach-Object { + # Split the output in name and description + $Name, $Description = $_.Split("\`t", 2) + __${name}_debug "Name: $Name Description: $Description" + + # Look for the longest completion so that we can format things nicely + if ($Longest -lt $Name.Length) { + $Longest = $Name.Length + } + + # Set the description to a one space string if there is none set. + # This is needed because the CompletionResult does not accept an empty string as argument + if (-Not $Description) { + $Description = " " + } + @{ Name = "$Name"; Description = "$Description" } + } + + + $Space = " " + if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { + # remove the space here + __${name}_debug "ShellCompDirectiveNoSpace is called" + $Space = "" + } + + if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or + (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { + __${name}_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" + + # return here to prevent the completion of the extensions + return + } + + $Values = $Values | Where-Object { + # filter the result + $_.Name -like "$WordToComplete*" + + # Join the flag back if we have an equal sign flag + if ( $IsEqualFlag ) { + __${name}_debug "Join the equal sign flag back to the completion value" + $_.Name = $Flag + "=" + $_.Name + } + } + + # we sort the values in ascending order by name if keep order isn't passed + if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { + $Values = $Values | Sort-Object -Property Name + } + + if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { + __${name}_debug "ShellCompDirectiveNoFileComp is called" + + if ($Values.Length -eq 0) { + # Just print an empty string here so the + # shell does not start to complete paths. + # We cannot use CompletionResult here because + # it does not accept an empty string as argument. + "" + return + } + } + + # Get the current mode + $Mode = (Get-PSReadLineKeyHandler | Where-Object { $_.Key -eq "Tab" }).Function + __${name}_debug "Mode: $Mode" + + $Values | ForEach-Object { + + # store temporary because switch will overwrite $_ + $comp = $_ + + # PowerShell supports three different completion modes + # - TabCompleteNext (default windows style - on each key press the next option is displayed) + # - Complete (works like bash) + # - MenuComplete (works like zsh) + # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function + + # CompletionResult Arguments: + # 1) CompletionText text to be used as the auto completion result + # 2) ListItemText text to be displayed in the suggestion list + # 3) ResultType type of completion result + # 4) ToolTip text for the tooltip with details about the object + + switch ($Mode) { + + # bash like + "Complete" { + + if ($Values.Length -eq 1) { + __${name}_debug "Only one completion left" + + # insert space after value + [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + + } else { + # Add the proper number of spaces to align the descriptions + while($comp.Name.Length -lt $Longest) { + $comp.Name = $comp.Name + " " + } + + # Check for empty description and only add parentheses if needed + if ($($comp.Description) -eq " " ) { + $Description = "" + } else { + $Description = " ($($comp.Description))" + } + + [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") + } + } + + # zsh like + "MenuComplete" { + # insert space after value + # MenuComplete will automatically show the ToolTip of + # the highlighted value at the bottom of the suggestions. + [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } + + # TabCompleteNext and in case we get something unknown + Default { + # Like MenuComplete but we don't want to add a space here because + # the user need to press space anyway to get the completion. + # Description will not be shown because that's not possible with TabCompleteNext + [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") + } + } + + } +} + +Register-ArgumentCompleter -CommandName '${name}' -ScriptBlock $__${nameForVar}CompleterBlock +`; +} diff --git a/src/shared.ts b/src/shared.ts index 8b13789..e69de29 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1 +0,0 @@ - diff --git a/src/zsh.ts b/src/zsh.ts index ae06f1a..3dbdfb4 100644 --- a/src/zsh.ts +++ b/src/zsh.ts @@ -1,4 +1,4 @@ -import { ShellCompDirective } from "./"; +import { ShellCompDirective } from './'; export function generate(name: string, exec: string) { return `#compdef ${name} @@ -212,4 +212,3 @@ if [ "\${funcstack[1]}" = "_${name}" ]; then fi `; } - diff --git a/tests/cli.test.ts b/tests/cli.test.ts index f0fd64c..660f8fc 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,134 +1,132 @@ -import { exec } from "child_process"; -import { describe, it, expect, test } from "vitest"; - -function runCommand(command: string): Promise { - return new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - reject(stderr); - } else { - resolve(stdout); - } - }); - }); -} - -const cliTools = ["citty"]; - -describe.each(cliTools)("cli completion tests for %s", (cliTool) => { - const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; - - it("should complete cli options", async () => { - const output = await runCommand(`${commandPrefix}`); - expect(output).toMatchSnapshot(); - }); - - describe("cli option completion tests", () => { - const optionTests = [ - { partial: "--p", expected: "--port" }, - ]; - - test.each(optionTests)( - "should complete option for partial input '%s'", - async ({ partial }) => { - const command = `${commandPrefix} dev ${partial}`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - } - ); - }); - - describe("cli option exclusion tests", () => { - const alreadySpecifiedTests = [ - { specified: "--config", shouldNotContain: "--config" }, - ]; - - test.each(alreadySpecifiedTests)( - "should not suggest already specified option '%s'", - async ({ specified }) => { - const command = `${commandPrefix} ${specified} --`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - } - ); - }); - - describe("cli option value handling", () => { - it("should resolve port value correctly", async () => { - const command = `${commandPrefix} dev --port=3`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should handle conflicting options appropriately", async () => { - const command = `${commandPrefix} --config vite.config.js --`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should resolve config option values correctly", async () => { - const command = `${commandPrefix} --config vite.config`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should handle unknown options with no completions", async () => { - const command = `${commandPrefix} --unknownoption`; - const output = await runCommand(command); - expect(output.trim()).toMatchSnapshot(); - }); - }); - - describe("edge case completions for end with space", () => { - //TOOD: remove this - it("should suggest port values if user ends with space after `--port`", async () => { - const command = `${commandPrefix} dev --port ""`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { - const command = `${commandPrefix} dev --po`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { - const command = `${commandPrefix} dev --port=`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - }); - - // single positional command: `lint [file]` - // vite "" - // -> src/ - // -> ./ - - // vite src/ "" - // -> nothing - // should not suggest anything - - // multiple postiionals command `lint [...files]` - // vite "" - // -> src/ - // -> ./ - - // vite src/ "" - // -> src/ - // -> ./ - - describe("positional argument completions", () => { - it("should complete single positional argument when ending with space (vite src/)", async () => { - const command = `${commandPrefix} vite src/ ""`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - - it("should complete multiple positional arguments when ending with space (vite src/ ./)", async () => { - const command = `${commandPrefix} vite ""`; - const output = await runCommand(command); - expect(output).toMatchSnapshot(); - }); - }); -}); +import { exec } from 'child_process'; +import { describe, it, expect, test } from 'vitest'; + +function runCommand(command: string): Promise { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); +} + +const cliTools = ['citty']; + +describe.each(cliTools)('cli completion tests for %s', (cliTool) => { + const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; + + it('should complete cli options', async () => { + const output = await runCommand(`${commandPrefix}`); + expect(output).toMatchSnapshot(); + }); + + describe('cli option completion tests', () => { + const optionTests = [{ partial: '--p', expected: '--port' }]; + + test.each(optionTests)( + "should complete option for partial input '%s'", + async ({ partial }) => { + const command = `${commandPrefix} dev ${partial}`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + } + ); + }); + + describe('cli option exclusion tests', () => { + const alreadySpecifiedTests = [ + { specified: '--config', shouldNotContain: '--config' }, + ]; + + test.each(alreadySpecifiedTests)( + "should not suggest already specified option '%s'", + async ({ specified }) => { + const command = `${commandPrefix} ${specified} --`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + } + ); + }); + + describe('cli option value handling', () => { + it('should resolve port value correctly', async () => { + const command = `${commandPrefix} dev --port=3`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it('should handle conflicting options appropriately', async () => { + const command = `${commandPrefix} --config vite.config.js --`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it('should resolve config option values correctly', async () => { + const command = `${commandPrefix} --config vite.config`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it('should handle unknown options with no completions', async () => { + const command = `${commandPrefix} --unknownoption`; + const output = await runCommand(command); + expect(output.trim()).toMatchSnapshot(); + }); + }); + + describe('edge case completions for end with space', () => { + //TOOD: remove this + it('should suggest port values if user ends with space after `--port`', async () => { + const command = `${commandPrefix} dev --port ""`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { + const command = `${commandPrefix} dev --po`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { + const command = `${commandPrefix} dev --port=`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + }); + + // single positional command: `lint [file]` + // vite "" + // -> src/ + // -> ./ + + // vite src/ "" + // -> nothing + // should not suggest anything + + // multiple postiionals command `lint [...files]` + // vite "" + // -> src/ + // -> ./ + + // vite src/ "" + // -> src/ + // -> ./ + + describe('positional argument completions', () => { + it('should complete single positional argument when ending with space (vite src/)', async () => { + const command = `${commandPrefix} vite src/ ""`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it('should complete multiple positional arguments when ending with space (vite src/ ./)', async () => { + const command = `${commandPrefix} vite ""`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 71c41ee..6b328fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,111 +1,111 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "es2022", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node10" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 39b8954c2aec6b2da37dd73584d0793ca769bf0a Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 18 Jan 2025 13:02:33 +0330 Subject: [PATCH 17/22] update --- .github/workflows/format.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 6931fec..606b1e3 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -7,16 +7,21 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v3 + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + + - name: Set node version to 20 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 + registry-url: https://registry.npmjs.org/ + cache: pnpm - - name: Install dependencies - run: npm install + - name: Install deps + run: pnpm install - name: Run Prettier Check run: npm run format:check From 0decdda4efae7c28c444698448a5b3c8fbc0fb25 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 18 Jan 2025 13:05:09 +0330 Subject: [PATCH 18/22] update --- .prettierrc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.prettierrc b/.prettierrc index 22e285c..6bdf86a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2 -} +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} From 9464253e9211f4ae3674554fab668265e86f0ef9 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Sat, 18 Jan 2025 14:09:06 +0330 Subject: [PATCH 19/22] update --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 01877fb..16b0a03 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ node_modules dist build +pnpm-lock.yaml \ No newline at end of file From 82e0ec71dcaae25ef23172024ec0bfdeee6b67c3 Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Mon, 20 Jan 2025 00:13:20 +0330 Subject: [PATCH 20/22] citty tests done --- demo.citty.ts | 4 ---- src/citty.ts | 3 --- src/index.ts | 12 ++++------ tests/__snapshots__/cli.test.ts.snap | 34 ++++++++++++---------------- tests/cli.test.ts | 7 +++++- 5 files changed, 26 insertions(+), 34 deletions(-) diff --git a/demo.citty.ts b/demo.citty.ts index b72c296..451a344 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -32,7 +32,6 @@ const devCommand = defineCommand({ port: { type: 'string', description: 'Specify port' }, }, run(ctx) { - console.log('dev', ctx); }, }); @@ -55,7 +54,6 @@ const lintCommand = defineCommand({ files: { type: 'positional', description: 'Files to lint' }, }, run(ctx) { - console.log('lint', ctx.cmd.args); }, }); @@ -67,10 +65,8 @@ main.subCommands = { const completion = await tab(main); for (const command of completion.commands.values()) { - console.log(command); if (command.name === 'lint') { - console.log('lint'); command.handler = () => { return [ { value: 'main.ts', description: 'Main file' }, diff --git a/src/citty.ts b/src/citty.ts index e7d049a..48940fe 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -111,8 +111,6 @@ export default async function tab( await handleSubCommands(completion, subCommands); - // console.log("LOOK HERE", subCommands) - if (instance.args) { for (const [argName, argConfig] of Object.entries(instance.args)) { const conf = argConfig as PositionalArgDef; @@ -166,7 +164,6 @@ export default async function tab( const parsed = parseArgs(extra, args); // TODO: this is not ideal at all const matchedCommand = parsed._.join(' '); - console.log(completion); return completion.parse(extra, matchedCommand); } } diff --git a/src/index.ts b/src/index.ts index bf2892b..9c1e123 100644 --- a/src/index.ts +++ b/src/index.ts @@ -139,7 +139,7 @@ export class Completion { if (previousArgs.length > 0) { const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg.startsWith('--') && endsWithSpace) { + if (lastPrevArg.startsWith('--') && !endsWithSpace) { const { handler } = matchedCommand.options.get(lastPrevArg)!; if (handler) { const flagSuggestions = await handler( @@ -153,14 +153,12 @@ export class Completion { ) ); directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - completions.forEach((comp) => console.log(comp)); - console.log(`:${directive}`); - return; + // completions.forEach((comp) => ); + // console.log(`:${directive}`); + // return; } } - } - - if (toComplete.startsWith('--')) { + } else if (toComplete.startsWith('--')) { directive = ShellCompDirective.ShellCompDirectiveNoFileComp; const equalsIndex = toComplete.indexOf('='); diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index cf64dc6..516bfa7 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -2,6 +2,7 @@ exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` "--port Specify port +:4 " `; @@ -12,48 +13,41 @@ exports[`cli completion tests for citty > cli option exclusion tests > should no " `; -exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--port', shouldNotContain: '--port' }' 1`] = ` -"--mode Set env mode ---logLevel info | warn | error | silent -:4 -" -`; +exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; -exports[`cli completion tests for citty > cli option value handling > should handle conflicting options appropriately 1`] = ` +exports[`cli completion tests for citty > cli option value handling > should not show duplicate options 1`] = ` "--mode Set env mode --logLevel info | warn | error | silent :4 " `; -exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `""`; - exports[`cli completion tests for citty > cli option value handling > should resolve config option values correctly 1`] = ` -"vite.config.js -vite.config.ts +"vite.config.ts Vite config file +vite.config.js Vite config file +:4 " `; exports[`cli completion tests for citty > cli option value handling > should resolve port value correctly 1`] = ` "3000 Development server port +8080 Alternative port +:4 " `; exports[`cli completion tests for citty > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` -"--port Specify port +":0 " `; exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = ` -"3000 Development server port -8080 Alternative port +":0 " `; exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` -"3000 Development server port -8080 Alternative port -:4 +":0 " `; @@ -69,7 +63,9 @@ exports[`cli completion tests for citty > positional argument completions > shou `; exports[`cli completion tests for citty > should complete cli options 1`] = ` -"dev -devbuild +"here +dev Start dev server +lint Lint project +:4 " `; diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 660f8fc..301345c 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,5 +1,8 @@ import { exec } from 'child_process'; import { describe, it, expect, test } from 'vitest'; +import '../src/index.js' +import '../src/citty.js' +import '../src/cac.js' function runCommand(command: string): Promise { return new Promise((resolve, reject) => { @@ -13,6 +16,7 @@ function runCommand(command: string): Promise { }); } +// TODO: add cac const cliTools = ['citty']; describe.each(cliTools)('cli completion tests for %s', (cliTool) => { @@ -46,6 +50,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => { async ({ specified }) => { const command = `${commandPrefix} ${specified} --`; const output = await runCommand(command); + console.log(output) expect(output).toMatchSnapshot(); } ); @@ -58,7 +63,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => { expect(output).toMatchSnapshot(); }); - it('should handle conflicting options appropriately', async () => { + it('should not show duplicate options', async () => { const command = `${commandPrefix} --config vite.config.js --`; const output = await runCommand(command); expect(output).toMatchSnapshot(); From 6c5e8aaa520e8beaf359684feafc0d2fd299e253 Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Sun, 26 Jan 2025 00:04:26 +0330 Subject: [PATCH 21/22] positionals nearly there --- demo.citty.ts | 1 + src/citty.ts | 6 +++- src/index.ts | 47 ++++++++++++++++++++-------- tests/__snapshots__/cli.test.ts.snap | 7 ++--- tests/cli.test.ts | 4 +-- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/demo.citty.ts b/demo.citty.ts index 451a344..4823334 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -68,6 +68,7 @@ for (const command of completion.commands.values()) { if (command.name === 'lint') { command.handler = () => { + console.log('lint handler') return [ { value: 'main.ts', description: 'Main file' }, { value: 'index.ts', description: 'Index file' }, diff --git a/src/citty.ts b/src/citty.ts index 48940fe..e0e8b66 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -163,7 +163,11 @@ export default async function tab( const args = (await resolve(instance.args))!; const parsed = parseArgs(extra, args); // TODO: this is not ideal at all - const matchedCommand = parsed._.join(' '); + const matchedCommand = parsed._.join(' ').trim(); + // TODO: `command lint i` does not work because `lint` and `i` are potential commands + // instead the potential command should only be `lint` + // and `i` is the to be completed part + console.log('extra', parsed, ctx) return completion.parse(extra, matchedCommand); } } diff --git a/src/index.ts b/src/index.ts index 9c1e123..4879903 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,6 +80,7 @@ type Option = { type Command = { name: string; description: string; + args: boolean[] handler: Handler; options: Map; parent?: Command; @@ -101,6 +102,7 @@ export class Completion { this.commands.set(key, { name: key, description, + args, handler, options: new Map(), parent: parent ? this.commands.get(parent)! : undefined, @@ -137,11 +139,10 @@ export class Completion { let toComplete = args[args.length - 1] || ''; const previousArgs = args.slice(0, -1); - if (previousArgs.length > 0) { - const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg.startsWith('--') && !endsWithSpace) { - const { handler } = matchedCommand.options.get(lastPrevArg)!; - if (handler) { + const lastPrevArg = previousArgs[previousArgs.length - 1]; + if (lastPrevArg?.startsWith('--') && !endsWithSpace) { + const { handler } = matchedCommand.options.get(lastPrevArg)!; + if (handler) { const flagSuggestions = await handler( previousArgs, toComplete, @@ -153,11 +154,7 @@ export class Completion { ) ); directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - // completions.forEach((comp) => ); - // console.log(`:${directive}`); - // return; } - } } else if (toComplete.startsWith('--')) { directive = ShellCompDirective.ShellCompDirectiveNoFileComp; const equalsIndex = toComplete.indexOf('='); @@ -215,13 +212,15 @@ export class Completion { } } else { const potentialCommandParts = potentialCommand.split(' '); + console.log(potentialCommandParts) for (const [k, v] of this.commands) { // if the command is root, skip it if (k === '') { continue; } - const parts = k.split(' '); + const parts = [...k.split(' '), ...v.args]; + console.log(parts) for (let i = 0; i < parts.length; i++) { const part = parts[i]; const potentialPart = potentialCommandParts[i] || ''; @@ -233,13 +232,35 @@ export class Completion { break; } + async function callHandler() { + console.log(matchedCommand) + console.log('callHandler', previousArgs, toComplete, endsWithSpace) + completions.push(...await matchedCommand.handler?.( + previousArgs, + toComplete, + endsWithSpace + )) + } + // If we're at the current word being completed if (i === potentialCommandParts.length - 1) { - // Only add if it matches the current partial input - if (part.startsWith(potentialPart)) { - completions.push({ value: part, description: v.description }); + if (endsWithSpace) { + const nextPart = parts[i + 1] + if (typeof nextPart === 'boolean') { + await callHandler() + } + } else { + // Only add if it matches the current partial input + console.log('part', part, potentialPart) + if (typeof part === 'boolean') { + await callHandler() + } else if (part.startsWith(potentialPart)) { + completions.push({ value: part, description: v.description }); + } } break; + } else if (i === parts.length - 1 && part === true) { // variadic + await callHandler() } // For previous parts, they must match exactly diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index 516bfa7..f35db9f 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -7,9 +7,7 @@ exports[`cli completion tests for citty > cli option completion tests > should c `; exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` -"--mode Set env mode ---logLevel info | warn | error | silent -:4 +":4 " `; @@ -63,8 +61,7 @@ exports[`cli completion tests for citty > positional argument completions > shou `; exports[`cli completion tests for citty > should complete cli options 1`] = ` -"here -dev Start dev server +"dev Start dev server lint Lint project :4 " diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 301345c..66e551c 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -123,13 +123,13 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => { describe('positional argument completions', () => { it('should complete single positional argument when ending with space (vite src/)', async () => { - const command = `${commandPrefix} vite src/ ""`; + const command = `${commandPrefix} lint src/ ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); it('should complete multiple positional arguments when ending with space (vite src/ ./)', async () => { - const command = `${commandPrefix} vite ""`; + const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); From 291b8a7a16315af0ef7d321268b300b8a1db9eee Mon Sep 17 00:00:00 2001 From: Mohammad Bagher Abiyat Date: Sat, 1 Feb 2025 10:10:22 +0330 Subject: [PATCH 22/22] project done ig --- demo.cac.ts | 208 ++++++----------- demo.citty.ts | 1 - src/cac.ts | 114 +++++++--- src/citty.ts | 1 - src/index.ts | 324 +++++++++++++++------------ tests/__snapshots__/cli.test.ts.snap | 109 +++++++-- tests/cli.test.ts | 21 +- 7 files changed, 446 insertions(+), 332 deletions(-) diff --git a/demo.cac.ts b/demo.cac.ts index a7fb966..e139423 100644 --- a/demo.cac.ts +++ b/demo.cac.ts @@ -1,143 +1,83 @@ -// import fs from "fs/promises"; -// import cac from "cac"; -// import { -// Callback, -// Completion, -// flagMap, -// Positional, -// positionalMap, -// } from "./shared"; -// import path from "path"; -// import tab from "./cac"; +import cac from "cac"; +import tab from "./src/cac"; -// const cli = cac("vite"); // Using 'vite' as the CLI tool name +const cli = cac("vite"); -// // Custom converters (placeholders) -// function convertBase(value) { -// return value; -// } +cli + .option("-c, --config ", `Use specified config file`) + .option("-m, --mode ", `Set env mode`) + .option("-l, --logLevel ", `info | warn | error | silent`); -// function convertHost(value) { -// return value; -// } +cli + .command("dev", "Start dev server") + .option("--host [host]", `Specify hostname`) + .option("--port ", `Specify port`) + .action((options) => {}); -// // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/cli.ts -// // Global options -// cli -// .option("-c, --config ", `[string] use specified config file`) -// .option("--base ", `[string] public base path (default: /)`, { -// type: [convertBase], -// }) -// .option("-l, --logLevel ", `[string] info | warn | error | silent`) -// .option("--clearScreen", `[boolean] allow/disable clear screen when logging`) -// .option("-d, --debug [feat]", `[string | boolean] show debug logs`) -// .option("-f, --filter ", `[string] filter debug logs`) -// .option("-m, --mode ", `[string] set env mode`); +cli + .command("dev build", "Build project") + .action((options) => {}); -// // Dev command -// cli -// .command("[root]", "start dev server") // default command -// .alias("serve") // the command is called 'serve' in Vite's API -// .alias("dev") // alias to align with the script name -// .option("--host [host]", `[string] specify hostname`, { type: [convertHost] }) -// .option("--port ", `[number] specify port`) -// .option("--open [path]", `[boolean | string] open browser on startup`) -// .option("--cors", `[boolean] enable CORS`) -// .option("--strictPort", `[boolean] exit if specified port is already in use`) -// .option( -// "--force", -// `[boolean] force the optimizer to ignore the cache and re-bundle` -// ) -// .action((root, options) => { -// console.log(`Starting dev server at ${root || "."} with options:`, options); -// }); -// // Build positional completions for each command using command.args -// for (const c of [cli.globalCommand, ...cli.commands]) { -// // Handle options -// for (const o of [...cli.globalCommand.options, ...c.options]) { -// const optionKey = `${c.name} ${o.name}`; +cli + .command("lint [...files]", "Lint project") + .action((files, options) => {}); -// if (o.rawName.includes("--logLevel ")) { -// // Completion for --logLevel -// flagMap.set(optionKey, async (previousArgs, toComplete) => { -// return [ -// { action: "info", description: "Info level logging" }, -// { action: "warn", description: "Warning level logging" }, -// { action: "error", description: "Error level logging" }, -// { action: "silent", description: "No logging" }, -// ].filter((comp) => comp.action.startsWith(toComplete)); -// }); -// } +const completion = await tab(cli); -// if (o.rawName.includes("--mode ")) { -// // Completion for --mode -// flagMap.set(optionKey, async (previousArgs, toComplete) => { -// return [ -// { action: "production", description: "Production mode" }, -// { action: "development", description: "Development mode" }, -// { action: "staging", description: "Staging mode" }, -// ].filter((comp) => comp.action.startsWith(toComplete)); -// }); -// } +for (const command of completion.commands.values()) { + if (command.name === 'lint') { + command.handler = () => { + return [ + { value: 'main.ts', description: 'Main file' }, + { value: 'index.ts', description: 'Index file' }, + ]; + }; + } -// if (o.rawName.includes("--port ")) { -// // Completion for --port -// flagMap.set(optionKey, async (previousArgs, toComplete) => { -// return [ -// { action: "3000", description: "Development server port" }, -// { action: "8080", description: "Alternative port" }, -// { action: "80", description: "HTTP port" }, -// { action: "443", description: "HTTPS port" }, -// { action: "5000", description: "Common backend port" }, -// ].filter((comp) => comp.action.startsWith(toComplete)); -// }); -// } + for (const [o, config] of command.options.entries()) { + if (o === '--port') { + config.handler = () => { + return [ + { value: '3000', description: 'Development server port' }, + { value: '8080', description: 'Alternative port' }, + ]; + }; + } + if (o === '--host') { + config.handler = () => { + return [ + { value: 'localhost', description: 'Localhost' }, + { value: '0.0.0.0', description: 'All interfaces' }, + ]; + }; + } + if (o === '--config') { + config.handler = () => { + return [ + { value: 'vite.config.ts', description: 'Vite config file' }, + { value: 'vite.config.js', description: 'Vite config file' }, + ]; + }; + } + if (o === '--mode') { + config.handler = () => { + return [ + { value: 'development', description: 'Development mode' }, + { value: 'production', description: 'Production mode' }, + ]; + }; + } + if (o === '--logLevel') { + config.handler = () => { + return [ + { value: 'info', description: 'Info level' }, + { value: 'warn', description: 'Warn level' }, + { value: 'error', description: 'Error level' }, + { value: 'silent', description: 'Silent level' }, + ]; + }; + } + } +} -// if (o.rawName.includes("--host [host]")) { -// // Completion for --host -// flagMap.set(optionKey, async (previousArgs, toComplete) => { -// return [ -// { action: "localhost", description: "Localhost" }, -// { action: "0.0.0.0", description: "All interfaces" }, -// { action: "127.0.0.1", description: "Loopback interface" }, -// ].filter((comp) => comp.action.startsWith(toComplete)); -// }); -// } - -// if (o.rawName.includes("--config ")) { -// // Completion for --config -// flagMap.set(optionKey, async (previousArgs, toComplete) => { -// const configFiles = ["vite.config.ts", "vite.config.js"].filter( -// (file) => file.startsWith(toComplete) -// ); -// return configFiles.map((file) => ({ action: file })); -// }); -// } - -// // Add more option completions as needed -// } - -// // Handle positional arguments -// if (c.args && c.args.length > 0) { -// const positionals = c.args.map((arg) => ({ -// required: arg.required, -// variadic: arg.variadic, -// value: arg.value, -// completion: async (previousArgs, toComplete) => { -// if (arg.value === "root") { -// return [ -// { action: "src/", description: "💣️.sh loves vite!" }, -// { action: "./", description: "This one is better." }, -// ]; -// } -// return []; -// }, -// })); - -// positionalMap.set(c.name, positionals); -// } -// } - -// tab(cli); - -// cli.parse(); +cli.parse(); diff --git a/demo.citty.ts b/demo.citty.ts index 4823334..451a344 100644 --- a/demo.citty.ts +++ b/demo.citty.ts @@ -68,7 +68,6 @@ for (const command of completion.commands.values()) { if (command.name === 'lint') { command.handler = () => { - console.log('lint handler') return [ { value: 'main.ts', description: 'Main file' }, { value: 'index.ts', description: 'Index file' }, diff --git a/src/cac.ts b/src/cac.ts index 98f55f3..76add15 100644 --- a/src/cac.ts +++ b/src/cac.ts @@ -1,32 +1,21 @@ -// // @bombsh/tab/cac -// import { CAC } from "cac"; -// import * as zsh from "./zsh"; -// import * as bash from "./bash"; -// import * as fish from "./fish"; -// import * as powershell from "./powershell"; -// import { -// flagMap, -// Positional, -// positionalMap, -// ShellCompDirective, -// } from "./shared"; - -// function quoteIfNeeded(path: string): string { -// return path.includes(" ") ? `'${path}'` : path; -// } +import * as zsh from "./zsh"; +import * as bash from "./bash"; +import * as fish from "./fish"; +import * as powershell from "./powershell"; +import type { CAC } from "cac"; +import { Completion } from "./"; -// const execPath = process.execPath; -// const processArgs = process.argv.slice(1); +const execPath = process.execPath; +const processArgs = process.argv.slice(1); +const quotedExecPath = quoteIfNeeded(execPath); +const quotedProcessArgs = processArgs.map(quoteIfNeeded); +const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); -// // Apply the quoting function to each part of x -// // This ensures that paths like "Program Files" are quoted for PowerShell execution. -// const quotedExecPath = quoteIfNeeded(execPath); -// const quotedProcessArgs = processArgs.map(quoteIfNeeded); -// const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded); +const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`; -// const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${ -// quotedProcessArgs[0] -// }`; +function quoteIfNeeded(path: string): string { + return path.includes(" ") ? `'${path}'` : path; +} // export default function tab(instance: CAC): void { // instance.command("complete [shell]").action(async (shell, extra) => { @@ -302,3 +291,76 @@ // } // }); // } + +export default function tab(instance: CAC): Completion { + const completion = new Completion(); + + // Add all commands and their options + for (const cmd of [instance.globalCommand, ...instance.commands]) { + if (cmd.name === 'complete') continue; // Skip completion command + + // Get positional args info from command usage + const args = (cmd.rawName.match(/\[.*?\]|\<.*?\>/g) || []) + .map(arg => arg.startsWith('[')); // true if optional (wrapped in []) + + // Add command to completion + const commandName = completion.addCommand( + cmd.name === '@@global@@' ? '' : cmd.name, + cmd.description || '', + args, + async () => [] + ); + + // Add command options + for (const option of [...instance.globalCommand.options, ...cmd.options]) { + completion.addOption( + commandName, + `--${option.name}`, + option.description || '', + async () => [] + ); + } + } + + instance.command("complete [shell]").action(async (shell, extra) => { + switch (shell) { + case "zsh": { + const script = zsh.generate(instance.name, x); + console.log(script); + break; + } + case "bash": { + const script = bash.generate(instance.name, x); + console.log(script); + break; + } + case "fish": { + const script = fish.generate(instance.name, x); + console.log(script); + break; + } + case "powershell": { + const script = powershell.generate(instance.name, x); + console.log(script); + break; + } + default: { + const args: string[] = extra["--"]; + instance.showHelpOnExit = false; + + // Parse current command context + instance.unsetMatchedCommand(); + instance.parse([execPath, processArgs[0], ...args], { + run: false, + }); + + // const matchedCommand = instance.matchedCommand?.name || ''; + // const potentialCommand = args.join(' ') + // console.log(potentialCommand) + return completion.parse(args); + } + } + }); + + return completion; +} \ No newline at end of file diff --git a/src/citty.ts b/src/citty.ts index e0e8b66..07e1541 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -167,7 +167,6 @@ export default async function tab( // TODO: `command lint i` does not work because `lint` and `i` are potential commands // instead the potential command should only be `lint` // and `i` is the to be completed part - console.log('extra', parsed, ctx) return completion.parse(extra, matchedCommand); } } diff --git a/src/index.ts b/src/index.ts index 4879903..cc789a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,6 +88,8 @@ type Command = { export class Completion { commands = new Map(); + completions: Item[] = [] + directive = ShellCompDirective.ShellCompDirectiveDefault; // vite [...files] // args: [false, false, true], only the last argument can be variadic @@ -125,13 +127,47 @@ export class Completion { return option; } - async parse(args: string[], potentialCommand: string) { - const matchedCommand = - this.commands.get(potentialCommand) ?? this.commands.get('')!; - let directive = ShellCompDirective.ShellCompDirectiveDefault; - const completions: Item[] = []; + // TODO: this should be aware of boolean args and stuff + private stripOptions(args: string[]): string[] { + const parts: string[] = [] + let option = false; + for (const k of args) { + if (k.startsWith('-')) { + option = true; + continue + } + if (option) { + option = false; + continue + } + parts.push(k) + } + return parts + } + + private matchCommand(args: string[]): [Command, string[]] { + args = this.stripOptions(args); + let parts: string[] = []; + let remaining: string[] = []; + let matched: Command = this.commands.get('')! + for (let i = 0; i < args.length; i++) { + const k = args[i]; + parts.push(k); + const potential = this.commands.get(parts.join(' ')) + + if (potential) { + matched = potential; + } else { + remaining = args.slice(i, args.length); + break + } + } + return [matched, remaining]; + } + async parse(args: string[]) { const endsWithSpace = args[args.length - 1] === ''; + if (endsWithSpace) { args.pop(); } @@ -139,156 +175,156 @@ export class Completion { let toComplete = args[args.length - 1] || ''; const previousArgs = args.slice(0, -1); + if (endsWithSpace) { + previousArgs.push(toComplete); + toComplete = ''; + } + + const [matchedCommand, remaining] = this.matchCommand(previousArgs); + const lastPrevArg = previousArgs[previousArgs.length - 1]; - if (lastPrevArg?.startsWith('--') && !endsWithSpace) { - const { handler } = matchedCommand.options.get(lastPrevArg)!; - if (handler) { - const flagSuggestions = await handler( - previousArgs, - toComplete, - endsWithSpace - ); - completions.push( - ...flagSuggestions.filter((comp) => - comp.value.startsWith(toComplete) - ) - ); - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - } - } else if (toComplete.startsWith('--')) { - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; - const equalsIndex = toComplete.indexOf('='); - - if (equalsIndex !== -1) { - const flagName = toComplete.slice(2, equalsIndex); - const valueToComplete = toComplete.slice(equalsIndex + 1); - const { handler } = matchedCommand.options.get(`--${flagName}`)!; - - if (handler) { - const suggestions = await handler( - previousArgs, - valueToComplete, - endsWithSpace - ); - completions.push(...suggestions); - } - } else if (!endsWithSpace) { - const options = new Map(matchedCommand.options); - - let currentCommand = matchedCommand; - while (currentCommand.parent) { - for (const [key, value] of currentCommand.parent.options) { - if (!options.has(key)) { - options.set(key, value); - } - } - currentCommand = currentCommand.parent; - } - const specifiedFlags = previousArgs - .filter((arg) => arg.startsWith('-')) - .filter((arg) => arg.startsWith('--')); - const availableFlags = [...options.keys()] - .filter((flag) => !specifiedFlags.includes(flag)) - .filter((flag) => flag.startsWith(toComplete)); - - completions.push( - ...availableFlags.map((flag) => ({ - value: flag, - description: options.get(flag)!.description ?? '', - })) + // 1. Handle flag/option completion + if (this.shouldCompleteFlags(lastPrevArg, toComplete, endsWithSpace)) { + await this.handleFlagCompletion( + matchedCommand, + previousArgs, + toComplete, + endsWithSpace, + lastPrevArg, + ); + } + else { + // 2. Handle command/subcommand completion + if (this.shouldCompleteCommands(toComplete, endsWithSpace)) { + await this.handleCommandCompletion( + previousArgs, + toComplete, ); - } else { - const { handler } = matchedCommand.options.get(toComplete)!; - - if (handler) { - const suggestions = await handler( - previousArgs, - toComplete, - endsWithSpace - ); - completions.push(...suggestions); - } } - } else { - const potentialCommandParts = potentialCommand.split(' '); - console.log(potentialCommandParts) - for (const [k, v] of this.commands) { - // if the command is root, skip it - if (k === '') { - continue; - } - - const parts = [...k.split(' '), ...v.args]; - console.log(parts) - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const potentialPart = potentialCommandParts[i] || ''; - - // Skip if we've already added this suggestion - const alreadyExists = - completions.findIndex((item) => item.value === part) !== -1; - if (alreadyExists) { - break; - } - - async function callHandler() { - console.log(matchedCommand) - console.log('callHandler', previousArgs, toComplete, endsWithSpace) - completions.push(...await matchedCommand.handler?.( - previousArgs, - toComplete, - endsWithSpace - )) - } - - // If we're at the current word being completed - if (i === potentialCommandParts.length - 1) { - if (endsWithSpace) { - const nextPart = parts[i + 1] - if (typeof nextPart === 'boolean') { - await callHandler() - } - } else { - // Only add if it matches the current partial input - console.log('part', part, potentialPart) - if (typeof part === 'boolean') { - await callHandler() - } else if (part.startsWith(potentialPart)) { - completions.push({ value: part, description: v.description }); - } - } - break; - } else if (i === parts.length - 1 && part === true) { // variadic - await callHandler() - } - - // For previous parts, they must match exactly - if (part !== potentialPart) { - break; - } - } + // 3. Handle positional arguments + if (matchedCommand && matchedCommand.args.length > 0) { + await this.handlePositionalCompletion( + matchedCommand, + previousArgs, + toComplete, + endsWithSpace, + ); } + } + this.complete(toComplete) + } + + private complete(toComplete: string) { + this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + + const seen = new Set(); + this.completions + .filter((comp) => { + if (seen.has(comp.value)) return false; + seen.add(comp.value); + return true; + }) + .filter((comp) => comp.value.startsWith(toComplete)) + .forEach((comp) => console.log(`${comp.value}\t${comp.description ?? ''}`)); + console.log(`:${this.directive}`); + } + + private shouldCompleteFlags(lastPrevArg: string | undefined, toComplete: string, endsWithSpace: boolean): boolean { + return (lastPrevArg?.startsWith('--')) || toComplete.startsWith('--'); + } - directive = ShellCompDirective.ShellCompDirectiveNoFileComp; + private shouldCompleteCommands(toComplete: string, endsWithSpace: boolean): boolean { + return !toComplete.startsWith('-'); + } + + private async handleFlagCompletion( + command: Command, + previousArgs: string[], + toComplete: string, + endsWithSpace: boolean, + lastPrevArg: string | undefined, + ) { + // Handle flag value completion + let flagName: string | undefined; + let valueToComplete = toComplete; + + if (toComplete.includes('=')) { + // Handle --flag=value case + const parts = toComplete.split('='); + flagName = parts[0]; + valueToComplete = parts[1] || ''; + } else if (lastPrevArg?.startsWith('--')) { + // Handle --flag value case + flagName = lastPrevArg; } - // vite [...items] - // vite dev - // vite lint [item] - // vite dev build - // TODO: prettier (plus check in ci) - // TODO: ci type check + if (flagName) { + const option = command.options.get(flagName); + if (option) { + const suggestions = await option.handler(previousArgs, valueToComplete, endsWithSpace); + if (toComplete.includes('=')) { + // Reconstruct the full flag=value format + this.completions = suggestions.map(suggestion => ({ + value: `${flagName}=${suggestion.value}`, + description: suggestion.description + })); + } else { + this.completions.push(...suggestions); + } + } + return; + } - // TODO: positionals (tomorrow night), this is nearly there! + // Handle flag name completion + if (toComplete.startsWith('--')) { + for (const [name, option] of command.options) { + if (name.startsWith(toComplete)) { + this.completions.push({ + value: name, + description: option.description + }); + } + } + } + } - // TODO: cac (tomorrow night) - // TODO: check behaviour of the tests (tomorrow night) + private async handleCommandCompletion( + previousArgs: string[], + toComplete: string, + ) { + const commandParts = [...previousArgs]; + + for (const k of this.commands.keys()) { + if (k === '') continue; + const parts = k.split(' '); + let match = true; + + let i = 0; + while (i < commandParts.length) { + if (parts[i] !== commandParts[i]) { + match = false; + break + } + i++; + } + if (match && parts[i]?.startsWith(toComplete)) { + this.completions.push({ + value: parts[i], + description: this.commands.get(k)!.description + }); + } + } + } - completions.forEach((comp) => - console.log(`${comp.value}\t${comp.description ?? ''}`) - ); - console.log(`:${directive}`); + private async handlePositionalCompletion( + command: Command, + previousArgs: string[], + toComplete: string, + endsWithSpace: boolean, + ) { + const suggestions = await command.handler(previousArgs, toComplete, endsWithSpace); + this.completions.push(...suggestions); } } diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index f35db9f..b5085c8 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -1,5 +1,86 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`cli completion tests for cac > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` +"--port Specify port +:4 +" +`; + +exports[`cli completion tests for cac > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = ` +":4 +" +`; + +exports[`cli completion tests for cac > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; + +exports[`cli completion tests for cac > cli option value handling > should not show duplicate options 1`] = ` +"--config Use specified config file +--mode Set env mode +--logLevel info | warn | error | silent +:4 +" +`; + +exports[`cli completion tests for cac > cli option value handling > should resolve config option values correctly 1`] = ` +"vite.config.ts Vite config file +vite.config.js Vite config file +:4 +" +`; + +exports[`cli completion tests for cac > cli option value handling > should resolve port value correctly 1`] = ` +"--port=3000 Development server port +:4 +" +`; + +exports[`cli completion tests for cac > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` +"--port Specify port +:4 +" +`; + +exports[`cli completion tests for cac > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = ` +"3000 Development server port +8080 Alternative port +:4 +" +`; + +exports[`cli completion tests for cac > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` +"--port=3000 Development server port +--port=8080 Alternative port +:4 +" +`; + +exports[`cli completion tests for cac > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = ` +"index.ts Index file +:4 +" +`; + +exports[`cli completion tests for cac > positional argument completions > should complete multiple positional arguments when ending with space 1`] = ` +"main.ts Main file +index.ts Index file +:4 +" +`; + +exports[`cli completion tests for cac > positional argument completions > should complete single positional argument when ending with space 1`] = ` +"main.ts Main file +index.ts Index file +:4 +" +`; + +exports[`cli completion tests for cac > should complete cli options 1`] = ` +"dev Start dev server +lint Lint project +:4 +" +`; + exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = ` "--port Specify port :4 @@ -14,7 +95,8 @@ exports[`cli completion tests for citty > cli option exclusion tests > should no exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; exports[`cli completion tests for citty > cli option value handling > should not show duplicate options 1`] = ` -"--mode Set env mode +"--config Use specified config file +--mode Set env mode --logLevel info | warn | error | silent :4 " @@ -28,35 +110,28 @@ vite.config.js Vite config file `; exports[`cli completion tests for citty > cli option value handling > should resolve port value correctly 1`] = ` -"3000 Development server port -8080 Alternative port +"--port=3000 Development server port :4 " `; exports[`cli completion tests for citty > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = ` -":0 +"--port Specify port +:4 " `; exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = ` -":0 +"3000 Development server port +8080 Alternative port +:4 " `; exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = ` -":0 -" -`; - -exports[`cli completion tests for citty > positional argument completions > should complete multiple positional arguments when ending with space (vite src/ ./) 1`] = ` -"./ -src/ -" -`; - -exports[`cli completion tests for citty > positional argument completions > should complete single positional argument when ending with space (vite src/) 1`] = ` -"./ +"--port=3000 Development server port +--port=8080 Alternative port +:4 " `; diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 66e551c..1ba239e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,8 +1,5 @@ import { exec } from 'child_process'; import { describe, it, expect, test } from 'vitest'; -import '../src/index.js' -import '../src/citty.js' -import '../src/cac.js' function runCommand(command: string): Promise { return new Promise((resolve, reject) => { @@ -16,8 +13,8 @@ function runCommand(command: string): Promise { }); } -// TODO: add cac -const cliTools = ['citty']; +const cliTools = ['citty', 'cac']; +// const cliTools = ['citty', 'cac']; describe.each(cliTools)('cli completion tests for %s', (cliTool) => { const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; @@ -122,14 +119,20 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => { // -> ./ describe('positional argument completions', () => { - it('should complete single positional argument when ending with space (vite src/)', async () => { - const command = `${commandPrefix} lint src/ ""`; + it.runIf(cliTool !== 'citty')('should complete multiple positional arguments when ending with space', async () => { + const command = `${commandPrefix} lint ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); }); - it('should complete multiple positional arguments when ending with space (vite src/ ./)', async () => { - const command = `${commandPrefix} lint ""`; + it.runIf(cliTool !== 'citty')('should complete multiple positional arguments when ending with part of the value', async () => { + const command = `${commandPrefix} lint ind`; + const output = await runCommand(command); + expect(output).toMatchSnapshot(); + }); + + it.runIf(cliTool !== 'citty')('should complete single positional argument when ending with space', async () => { + const command = `${commandPrefix} lint main.ts ""`; const output = await runCommand(command); expect(output).toMatchSnapshot(); });