From 8be7b5f7161998b636edc122d571af7fb65b4dd0 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 28 Apr 2026 16:29:06 +0530 Subject: [PATCH 1/2] chore: update Command Line SDK to 19.2.0 --- CHANGELOG.md | 7 + README.md | 25 +- cli.ts | 19 +- install.ps1 | 4 +- install.sh | 32 ++- lib/commands/update.ts | 82 +++++- lib/completions.ts | 567 +++++++++++++++++++++++++++++++++++++ lib/constants.ts | 4 +- lib/utils.ts | 68 ++++- package-lock.json | 4 +- package.json | 2 +- scoop/appwrite.config.json | 6 +- 12 files changed, 780 insertions(+), 40 deletions(-) create mode 100644 lib/completions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ff683..c039e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 19.2.0 + +* Added `completion install` command with completion scripts for zsh, bash, and fish +* Added automatic shell-completion install step to the `install.sh` standalone installer +* Updated Homebrew install and update flow to detect the installed formula and recommend the official `appwrite/appwrite/appwrite` tap when a different one is in use +* Fixed standalone update check to point at the `appwrite/sdk-for-cli` releases feed instead of the legacy `appwrite/appwrite-cli` repo + ## 19.1.0 * Added `--where`, `--sort-asc`, `--sort-desc`, `--limit`, `--offset`, `--cursor-after`, and `--cursor-before` flags on list commands across services diff --git a/README.md b/README.md index c4a8da5..b0dae28 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using ```sh $ appwrite -v -19.1.0 +19.2.0 ``` ### Install using prebuilt binaries @@ -49,6 +49,27 @@ $ brew install appwrite/appwrite/appwrite Homebrew pulls the formula from the [`appwrite/homebrew-appwrite`](https://github.com/appwrite/homebrew-appwrite) tap and downloads the native binary for your platform. +### Shell completion + +Install completion for your current shell: + +```bash +appwrite completion install +``` + +You can also generate or install completion scripts manually: + +```bash +# zsh +appwrite completion install zsh +# bash +appwrite completion install bash +# fish +appwrite completion install fish +``` + +For zsh, ensure `~/.zfunc` is in your `fpath` and `compinit` is loaded from your shell config. + ### Windows Via Powershell ```powershell @@ -62,7 +83,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc Once the installation completes, you can verify your install using ``` $ appwrite -v -19.1.0 +19.2.0 ``` ## Getting Started diff --git a/cli.ts b/cli.ts index 298f94e..293f533 100644 --- a/cli.ts +++ b/cli.ts @@ -18,6 +18,11 @@ import { syncVersionCheckCache, } from './lib/utils.js'; import inquirerSearchList from 'inquirer-search-list'; +import { + createCompletionCommand, + isCompletionCommand, + isCompletionInvocation, +} from './lib/completions.js'; import { client } from './lib/commands/generic.js'; import { login, logout, whoami, migrate, register } from './lib/commands/generic.js'; @@ -55,7 +60,6 @@ import { webhooks } from './lib/commands/services/webhooks.js'; const { version } = packageJson; inquirer.registerPrompt('search-list', inquirerSearchList); const VERSION_CHECK_TIMEOUT_MS = 5000; - function writeUpdateAvailableNotice(currentVersion: string, latestVersion: string, toStderr: boolean = false): void { const stream = toStderr ? process.stderr : process.stdout; @@ -126,7 +130,9 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { })(); } else { void (async () => { - await maybeShowUpdateNotice(); + if (!isCompletionInvocation()) { + await maybeShowUpdateNotice(); + } program .name('appwrite') @@ -141,7 +147,13 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { .option('-j, --json', 'Output filtered JSON without empty values') .option('-R, --raw', 'Output full JSON response (secrets still redacted unless --show-secrets is set)') .option('--show-secrets', 'Display sensitive values like secrets and tokens in output') - .hook('preAction', migrate) + .hook('preAction', async (_thisCommand, actionCommand) => { + if (isCompletionCommand(actionCommand)) { + return; + } + + await migrate(); + }) .option('-f,--force', 'Flag to confirm all warnings') .option('-a,--all', 'Flag to push all resources') .option('--id [id...]', 'Flag to pass a list of ids for a given action') @@ -214,6 +226,7 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { .addCommand(users) .addCommand(vcs) .addCommand(webhooks) + .addCommand(createCompletionCommand(program)) .addCommand(client) .parse(process.argv); diff --git a/install.ps1 b/install.ps1 index 655a629..b95d779 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,8 +13,8 @@ # You can use "View source" of this page to see the full script. # REPO -$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/19.1.0/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/19.1.0/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/19.2.0/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/19.2.0/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index bdd80e9..d5e9218 100644 --- a/install.sh +++ b/install.sh @@ -37,6 +37,7 @@ ARCH="" # Add some color to life RED='\033[0;31m' GREEN='\033[0;32m' +YELLOW='\033[0;33m' NC='\033[0m' # No Color greeting() { @@ -55,7 +56,7 @@ EOF } getSystemInfo() { - echo "[1/4] Getting System Info ..." + echo "[1/5] Getting System Info ..." ARCH=$(uname -m) case $ARCH in @@ -93,6 +94,10 @@ printSuccess() { printf "${GREEN}โœ… Done ... ${NC}\n\n" } +printSkipped() { + printf "${YELLOW}โ„น๏ธ $1 ${NC}\n" +} + verifyMacOSCodeSignature() { if [ "$OS" != "darwin" ]; then return @@ -113,9 +118,9 @@ verifyMacOSCodeSignature() { } downloadBinary() { - echo "[2/4] Downloading executable for $OS ($ARCH) ..." + echo "[2/5] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="19.1.0" + GITHUB_LATEST_VERSION="19.2.0" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" @@ -130,7 +135,7 @@ downloadBinary() { } install() { - echo "[3/4] Installing ..." + echo "[3/5] Installing ..." printf "${GREEN}๐Ÿšง Setting Permissions ${NC}\n" chmod +x $APPWRITE_TEMP_NAME @@ -151,6 +156,22 @@ install() { printSuccess } +installCompletions() { + echo "[4/5] Installing shell completions ..." + + if $APPWRITE_EXECUTABLE_FILEPATH completion install; then + printSuccess + return + fi + + printSkipped "Skipped shell completion installation. To install manually, run:" + echo " $APPWRITE_EXECUTABLE_NAME completion install" + echo " $APPWRITE_EXECUTABLE_NAME completion install zsh" + echo " $APPWRITE_EXECUTABLE_NAME completion install bash" + echo " $APPWRITE_EXECUTABLE_NAME completion install fish" + echo "" +} + cleanup() { printf "${GREEN}๐Ÿงน Cleaning up mess ... ${NC}\n" rm $APPWRITE_TEMP_NAME @@ -163,7 +184,7 @@ cleanup() { } installCompleted() { - echo "[4/4] Wrapping up installation ... " + echo "[5/5] Wrapping up installation ... " cleanup echo "๐Ÿš€ To get started with Appwrite CLI, please visit https://appwrite.io/docs/command-line" echo "As first step, you can login to your Appwrite account using 'appwrite login'" @@ -174,4 +195,5 @@ greeting getSystemInfo downloadBinary install +installCompletions installCompleted diff --git a/lib/commands/update.ts b/lib/commands/update.ts index a015560..34c8413 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -11,6 +11,7 @@ import { getLatestVersionForInstallation, compareVersions, getErrorMessage, + getInstalledHomebrewFormula, } from "../utils.js"; import { EXECUTABLE_NAME, @@ -136,6 +137,31 @@ const execCommand = ( }); }; +const getHomebrewFormulaForUpdate = (): string => { + return getInstalledHomebrewFormula() ?? HOMEBREW_FORMULA; +}; + +const getHomebrewFormulaForManualInstructions = (): string => { + if (process.platform === "win32") { + return HOMEBREW_FORMULA; + } + + return getHomebrewFormulaForUpdate(); +}; + +const showHomebrewTapRecommendation = (formulaName: string): void => { + if (formulaName === HOMEBREW_FORMULA) { + return; + } + + warn( + `Detected ${chalk.bold(formulaName)} from Homebrew. For faster native binaries, we recommend using the official Appwrite tap.`, + ); + hint( + `To use the official Appwrite tap, run: brew install ${HOMEBREW_FORMULA}`, + ); +}; + /** * Update via npm */ @@ -163,9 +189,16 @@ const updateViaNpm = async (): Promise => { /** * Update via Homebrew */ -const updateViaHomebrew = async (): Promise => { +const updateViaHomebrew = async ( + formulaName: string = getHomebrewFormulaForUpdate(), + options: { showRecommendation?: boolean } = {}, +): Promise => { try { - await execCommand("brew", ["upgrade", HOMEBREW_FORMULA]); + if (options.showRecommendation ?? true) { + showHomebrewTapRecommendation(formulaName); + } + + await execCommand("brew", ["upgrade", formulaName]); console.log(""); success("Updated to latest version via Homebrew!"); hint(`Run '${EXECUTABLE_NAME} --version' to verify the new version.`); @@ -184,7 +217,7 @@ const updateViaHomebrew = async (): Promise => { } else { console.log(""); error(`Failed to update via Homebrew: ${message}`); - hint(`Try running: brew upgrade ${HOMEBREW_FORMULA}`); + hint(`Try running: brew upgrade ${formulaName}`); } } }; @@ -196,7 +229,7 @@ const updateViaStandaloneBinary = async ( latestVersion: string, ): Promise => { if (process.platform === "win32") { - showManualInstructions(latestVersion); + showManualInstructions(latestVersion, HOMEBREW_FORMULA); return; } @@ -252,7 +285,10 @@ const updateViaStandaloneBinary = async ( /** * Show manual update instructions */ -const showManualInstructions = (latestVersion: string): void => { +const showManualInstructions = ( + latestVersion: string, + homebrewFormula: string = getHomebrewFormulaForManualInstructions(), +): void => { log("Manual update options:"); console.log(""); @@ -261,7 +297,13 @@ const showManualInstructions = (latestVersion: string): void => { console.log(""); log(`${chalk.bold("Option 2: Homebrew")}`); - console.log(` brew upgrade ${HOMEBREW_FORMULA}`); + console.log(` brew upgrade ${homebrewFormula}`); + if (homebrewFormula !== HOMEBREW_FORMULA) { + console.log(""); + hint( + `For faster native binaries from the official Appwrite tap, run: brew install ${HOMEBREW_FORMULA}`, + ); + } console.log(""); if (process.platform !== "win32") { @@ -347,8 +389,14 @@ interface UpdateOptions { const updateCli = async ({ manual }: UpdateOptions = {}): Promise => { try { const installationMethod = detectInstallationMethod(); - const latestVersion = - await getLatestVersionForInstallation(installationMethod); + const homebrewFormula = + installationMethod === "homebrew" ? getHomebrewFormulaForUpdate() : null; + const latestVersion = await getLatestVersionForInstallation( + installationMethod, + { + homebrewFormula: homebrewFormula ?? undefined, + }, + ); const comparison = compareVersions(version, latestVersion); @@ -365,22 +413,28 @@ const updateCli = async ({ manual }: UpdateOptions = {}): Promise => { return; } + if (manual) { + showManualInstructions(latestVersion, homebrewFormula ?? undefined); + return; + } + + if (homebrewFormula) { + showHomebrewTapRecommendation(homebrewFormula); + } + log( `Updating from ${chalk.blue(version)} to ${chalk.green(latestVersion)}...`, ); console.log(""); - if (manual) { - showManualInstructions(latestVersion); - return; - } - switch (installationMethod) { case "npm": await updateViaNpm(); break; case "homebrew": - await updateViaHomebrew(); + await updateViaHomebrew(homebrewFormula ?? undefined, { + showRecommendation: false, + }); break; case "standalone": await updateViaStandaloneBinary(latestVersion); diff --git a/lib/completions.ts b/lib/completions.ts new file mode 100644 index 0000000..95210f0 --- /dev/null +++ b/lib/completions.ts @@ -0,0 +1,567 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { Command } from "commander"; +import type { Option } from "commander"; +import { EXECUTABLE_NAME } from "./constants.js"; + +const SUPPORTED_COMPLETION_SHELLS = ["zsh", "bash", "fish"] as const; + +type CompletionShell = (typeof SUPPORTED_COMPLETION_SHELLS)[number]; + +type CompletionOption = { + flags: string[]; + required: boolean; + optional: boolean; +}; + +type CompletionCommand = { + name: string; + aliases: string[]; + options: CompletionOption[]; + commands: CompletionCommand[]; + path: string[]; +}; + +type CompletionContext = { + path: string[]; + commands: string[]; + options: string[]; +}; + +type CompletionTransition = { + from: string; + token: string; + to: string; +}; + +function uniq(values: string[]): string[] { + return [...new Set(values)]; +} + +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function commandPath(path: string[]): string { + return path.join(" "); +} + +function optionToCompletion(option: Option): CompletionOption { + return { + flags: uniq( + [option.short, option.long].filter((flag): flag is string => + Boolean(flag), + ), + ), + required: option.required, + optional: option.optional, + }; +} + +function commandToCompletion( + command: Command, + path: string[] = [], +): CompletionCommand { + const helper = command.createHelp(); + const commands = helper + .visibleCommands(command) + .filter((childCommand) => childCommand.name() !== "help") + .map((childCommand) => + commandToCompletion(childCommand, [...path, childCommand.name()]), + ); + + return { + name: command.name(), + aliases: command.aliases(), + options: helper.visibleOptions(command).map(optionToCompletion), + commands, + path, + }; +} + +function commandCompletionNames(command: CompletionCommand): string[] { + return uniq([command.name, ...command.aliases]); +} + +function collectCompletionContexts( + command: CompletionCommand, + rootOptions: CompletionOption[], + contexts: CompletionContext[] = [], +): CompletionContext[] { + const options = + command.path.length === 0 + ? command.options + : [...rootOptions, ...command.options]; + + contexts.push({ + path: command.path, + commands: command.commands.flatMap(commandCompletionNames), + options: uniq(options.flatMap((option) => option.flags)), + }); + + for (const childCommand of command.commands) { + collectCompletionContexts(childCommand, rootOptions, contexts); + } + + return contexts; +} + +function collectCompletionTransitions( + command: CompletionCommand, + transitions: CompletionTransition[] = [], +): CompletionTransition[] { + const from = commandPath(command.path); + + for (const childCommand of command.commands) { + const to = commandPath(childCommand.path); + + for (const name of commandCompletionNames(childCommand)) { + transitions.push({ from, token: name, to }); + } + + collectCompletionTransitions(childCommand, transitions); + } + + return transitions; +} + +function completionSpec(command: Command): { + contexts: CompletionContext[]; + transitions: CompletionTransition[]; + root: CompletionCommand; +} { + const root = commandToCompletion(command); + + return { + contexts: collectCompletionContexts(root, root.options), + transitions: collectCompletionTransitions(root), + root, + }; +} + +function renderZshCompletion(command: Command): string { + const spec = completionSpec(command); + const commandName = EXECUTABLE_NAME; + const transitionCases = spec.transitions + .map( + (transition) => + ` ${shellQuote(`${transition.from}:${transition.token}`)}) context=${shellQuote(transition.to)} ;;`, + ) + .join("\n"); + const contextCases = spec.contexts + .map((context) => { + const completions = uniq([...context.commands, ...context.options]) + .map(shellQuote) + .join(" "); + + return ` ${shellQuote(commandPath(context.path))}) + completions=( ${completions} ) + ;;`; + }) + .join("\n"); + + return `#compdef ${commandName} + +_${commandName}() { + local context word + local -a completions + + context='' + for (( i = 2; i < CURRENT; i++ )); do + word="\${words[i]}" + [[ "\${word}" == -* ]] && continue + + case "\${context}:\${word}" in +${transitionCases} + esac + done + + case "\${context}" in +${contextCases} + esac + + compadd -- "\${completions[@]}" +} + +if (( $+functions[compdef] )); then + compdef _${commandName} ${commandName} +fi +`; +} + +function renderBashCompletion(command: Command): string { + const spec = completionSpec(command); + const commandName = EXECUTABLE_NAME; + const transitionCases = spec.transitions + .map( + (transition) => + ` ${shellQuote(`${transition.from}:${transition.token}`)}) context=${shellQuote(transition.to)} ;;`, + ) + .join("\n"); + const contextCases = spec.contexts + .map((context) => { + const completions = uniq([...context.commands, ...context.options]).join( + " ", + ); + + return ` ${shellQuote(commandPath(context.path))}) + completions=${shellQuote(completions)} + ;;`; + }) + .join("\n"); + + return `_${commandName}_completion() { + local cur context completions word i + + cur="\${COMP_WORDS[COMP_CWORD]}" + context='' + completions='' + + for (( i = 1; i < COMP_CWORD; i++ )); do + word="\${COMP_WORDS[i]}" + [[ "\${word}" == -* ]] && continue + + case "\${context}:\${word}" in +${transitionCases} + esac + done + + case "\${context}" in +${contextCases} + esac + + COMPREPLY=( $(compgen -W "\${completions}" -- "\${cur}") ) +} + +if type complete >/dev/null 2>&1; then + complete -F _${commandName}_completion ${commandName} +fi +`; +} + +function fishCondition(commandName: string, path: string[]): string { + const pathText = commandPath(path); + + return pathText + ? `__${commandName}_using_command ${pathText}` + : `__${commandName}_using_command`; +} + +function fishOptionLine( + commandName: string, + path: string[], + option: CompletionOption, +): string | null { + const shortFlag = option.flags.find((flag) => /^-[^-]$/.test(flag)); + const longFlag = option.flags.find((flag) => /^--/.test(flag)); + + if (!shortFlag && !longFlag) { + return null; + } + + const parts = [ + "complete", + "-c", + shellQuote(commandName), + "-f", + "-n", + shellQuote(fishCondition(commandName, path)), + ]; + + if (shortFlag) { + parts.push("-s", shellQuote(shortFlag.slice(1))); + } + + if (longFlag) { + parts.push("-l", shellQuote(longFlag.slice(2))); + } + + if (option.required) { + parts.push("-r"); + } else if (option.optional) { + parts.push("-x"); + } + + return parts.join(" "); +} + +function renderFishCompletion(command: Command): string { + const commandName = EXECUTABLE_NAME; + const spec = completionSpec(command); + const transitionCases = spec.transitions + .map( + (transition) => + ` case ${shellQuote(`${transition.from}:${transition.token}`)} + set context ${shellQuote(transition.to)}`, + ) + .join("\n"); + const contextLines = spec.contexts + .flatMap((context) => { + const lines: string[] = []; + const commandNames = uniq(context.commands); + + if (commandNames.length > 0) { + lines.push( + `complete -c ${shellQuote(commandName)} -f -n ${shellQuote(fishCondition(commandName, context.path))} -a ${shellQuote(commandNames.join(" "))}`, + ); + } + + const commandNode = + context.path.length === 0 + ? spec.root + : findCompletionCommand(spec.root, context.path); + const rootOptions = spec.root.options; + const optionSource = + context.path.length === 0 || !commandNode + ? rootOptions + : [...rootOptions, ...commandNode.options]; + + for (const option of dedupeCompletionOptions(optionSource)) { + const line = fishOptionLine(commandName, context.path, option); + + if (line) { + lines.push(line); + } + } + + return lines; + }) + .join("\n"); + + return `function __${commandName}_using_command + set -l tokens (commandline -opc) + set -l context '' + set -l expected (string join ' ' $argv) + + if test (count $tokens) -gt 0 + set -e tokens[1] + end + + for token in $tokens + if string match -qr '^-' -- $token + continue + end + + switch "$context:$token" +${transitionCases} + end + end + + test "$context" = "$expected" +end + +${contextLines} +`; +} + +function findCompletionCommand( + command: CompletionCommand, + path: string[], +): CompletionCommand | null { + if (commandPath(command.path) === commandPath(path)) { + return command; + } + + for (const childCommand of command.commands) { + const found = findCompletionCommand(childCommand, path); + + if (found) { + return found; + } + } + + return null; +} + +function dedupeCompletionOptions( + options: CompletionOption[], +): CompletionOption[] { + const seen = new Set(); + const deduped: CompletionOption[] = []; + + for (const option of options) { + const key = option.flags.join(","); + + if (seen.has(key)) { + continue; + } + + seen.add(key); + deduped.push(option); + } + + return deduped; +} + +function renderCompletion(command: Command, shell: CompletionShell): string { + if (shell === "zsh") { + return renderZshCompletion(command); + } + + if (shell === "bash") { + return renderBashCompletion(command); + } + + return renderFishCompletion(command); +} + +function normalizeShell(value: string | undefined): CompletionShell | null { + if (!value) { + return null; + } + + const shellName = path.basename(value).toLowerCase(); + + if (shellName === "zsh" || shellName === "bash" || shellName === "fish") { + return shellName; + } + + return null; +} + +function detectShell(): CompletionShell | null { + return normalizeShell(process.env.SHELL); +} + +function xdgPath(envName: string, fallback: string): string { + const configuredPath = process.env[envName]; + + if (configuredPath) { + return configuredPath; + } + + return fallback; +} + +function completionInstallPath( + commandName: string, + shell: CompletionShell, +): string { + const homeDir = os.homedir(); + + if (shell === "zsh") { + return path.join( + xdgPath("ZSH_COMPLETION_DIR", path.join(homeDir, ".zfunc")), + `_${commandName}`, + ); + } + + if (shell === "bash") { + return path.join( + xdgPath( + "BASH_COMPLETION_DIR", + path.join(homeDir, ".local", "share", "bash-completion", "completions"), + ), + commandName, + ); + } + + return path.join( + xdgPath( + "FISH_COMPLETION_DIR", + path.join(homeDir, ".config", "fish", "completions"), + ), + `${commandName}.fish`, + ); +} + +function installCompletion( + rootCommand: Command, + shell: CompletionShell, +): string { + const installPath = completionInstallPath(EXECUTABLE_NAME, shell); + + fs.mkdirSync(path.dirname(installPath), { recursive: true }); + fs.writeFileSync(installPath, renderCompletion(rootCommand, shell)); + + return installPath; +} + +export function isCompletionInvocation(): boolean { + return findTopLevelCommandArg(process.argv.slice(2)) === "completion"; +} + +function findTopLevelCommandArg(args: string[]): string | undefined { + let consumesIdValues = false; + + for (const arg of args) { + if (arg === "--") { + return undefined; + } + + if (consumesIdValues) { + if (!arg.startsWith("-")) { + continue; + } + + consumesIdValues = false; + } + + if (arg === "--id") { + consumesIdValues = true; + continue; + } + + if (arg.startsWith("--id=")) { + continue; + } + + if (arg.startsWith("-")) { + continue; + } + + return arg; + } + + return undefined; +} + +export function isCompletionCommand(command: Command): boolean { + let currentCommand: Command | undefined = command; + + while (currentCommand) { + if (currentCommand.name() === "completion") { + return true; + } + + currentCommand = currentCommand.parent; + } + + return false; +} + +export function createCompletionCommand(rootCommand: Command): Command { + const completionCommand = new Command("completion").description( + "Generate shell completion scripts", + ); + + completionCommand + .command("install [shell]") + .description("Install shell completion script") + .action((requestedShell?: string) => { + const shell = normalizeShell(requestedShell) ?? detectShell(); + + if (!shell) { + process.stderr.write( + `Unable to detect shell. Run ${EXECUTABLE_NAME} completion install zsh, bash, or fish.\n`, + ); + process.exitCode = 1; + return; + } + + const installPath = installCompletion(rootCommand, shell); + process.stdout.write(`Installed ${shell} completion to ${installPath}\n`); + }); + + for (const shell of SUPPORTED_COMPLETION_SHELLS) { + completionCommand + .command(shell) + .description(`Generate ${shell} completion script`) + .action(() => { + process.stdout.write(renderCompletion(rootCommand, shell)); + }); + } + + return completionCommand; +} diff --git a/lib/constants.ts b/lib/constants.ts index 2eef4fb..06ffb35 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,7 +1,7 @@ // SDK export const SDK_TITLE = 'Appwrite'; export const SDK_TITLE_LOWER = 'appwrite'; -export const SDK_VERSION = '19.1.0'; +export const SDK_VERSION = '19.2.0'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; @@ -21,7 +21,7 @@ export const NPM_PACKAGE_NAME = 'appwrite-cli'; export const NPM_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`; // GitHub -export const GITHUB_REPO = 'appwrite/appwrite-cli'; +export const GITHUB_REPO = 'appwrite/sdk-for-cli'; export const GITHUB_RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases`; // API diff --git a/lib/utils.ts b/lib/utils.ts index ffaedd2..eb98280 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -94,8 +94,13 @@ export const getErrorMessage = (error: unknown): string => { export type InstallationMethod = "npm" | "homebrew" | "standalone"; type LatestVersionSource = "npm" | "homebrew"; +type LatestVersionOptions = { + timeoutMs?: number; + homebrewFormula?: string; +}; type HomebrewInfoResponse = { formulae?: Array<{ + full_name?: string; versions?: { stable?: string; }; @@ -221,17 +226,55 @@ const normalizeHomebrewVersion = (version: string): string => { return version.split("_")[0].trim(); }; -const getHomebrewLatestVersion = async ( +const DEFAULT_HOMEBREW_COMMAND_TIMEOUT_MS = 15000; + +export const getInstalledHomebrewFormula = ( options: { timeoutMs?: number } = {}, +): string | null => { + try { + const output = childProcess.execFileSync( + "brew", + ["list", "--formula", "--full-name"], + { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: options.timeoutMs ?? DEFAULT_HOMEBREW_COMMAND_TIMEOUT_MS, + env: { + ...process.env, + HOMEBREW_NO_AUTO_UPDATE: "1", + }, + }, + ); + const formulaName = EXECUTABLE_NAME.toLowerCase(); + const supportedFormulas = new Set([HOMEBREW_FORMULA, formulaName]); + + return ( + output + .split(/\r?\n/) + .map((formula) => formula.trim()) + .find((formula) => supportedFormulas.has(formula)) ?? null + ); + } catch (_error) { + return null; + } +}; + +const getHomebrewLatestVersion = async ( + options: LatestVersionOptions = {}, ): Promise => { + const formulaName = + options.homebrewFormula ?? + getInstalledHomebrewFormula(options) ?? + HOMEBREW_FORMULA; + try { const output = childProcess.execFileSync( "brew", - ["info", "--json=v2", HOMEBREW_FORMULA], + ["info", "--json=v2", formulaName], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], - timeout: options.timeoutMs, + timeout: options.timeoutMs ?? DEFAULT_HOMEBREW_COMMAND_TIMEOUT_MS, env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1", @@ -240,10 +283,16 @@ const getHomebrewLatestVersion = async ( ); const parsed = JSON.parse(output) as HomebrewInfoResponse; - const stableVersion = parsed.formulae?.[0]?.versions?.stable; + const formula = + parsed.formulae?.find((formula) => { + return formula.full_name === formulaName; + }) ?? parsed.formulae?.[0]; + const stableVersion = formula?.versions?.stable; if (typeof stableVersion !== "string" || stableVersion.trim() === "") { - throw new Error("Homebrew did not return a stable formula version."); + throw new Error( + `Homebrew did not return a stable version for ${formulaName}.`, + ); } return normalizeHomebrewVersion(stableVersion); @@ -492,7 +541,7 @@ export async function getLatestVersion( export async function getLatestVersionForInstallation( method: InstallationMethod | null, - options: { timeoutMs?: number } = {}, + options: LatestVersionOptions = {}, ): Promise { switch (getLatestVersionSource(method)) { case "homebrew": @@ -557,10 +606,17 @@ export async function getCachedUpdateNotification( } try { + const homebrewFormula = + installationMethod === "homebrew" + ? (getInstalledHomebrewFormula({ + timeoutMs: DEFAULT_UPDATE_CHECK_TIMEOUT_MS, + }) ?? HOMEBREW_FORMULA) + : undefined; const latestVersion = await getLatestVersionForInstallation( installationMethod, { timeoutMs: DEFAULT_UPDATE_CHECK_TIMEOUT_MS, + homebrewFormula, }, ); const updateAvailable = compareVersions(currentVersion, latestVersion) > 0; diff --git a/package-lock.json b/package-lock.json index 71a7cd3..f6ecc69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite-cli", - "version": "19.1.0", + "version": "19.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "19.1.0", + "version": "19.2.0", "license": "BSD-3-Clause", "dependencies": { "@appwrite.io/console": "11.0.0", diff --git a/package.json b/package.json index 2e17bb7..6281b49 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", - "version": "19.1.0", + "version": "19.2.0", "license": "BSD-3-Clause", "main": "dist/index.cjs", "module": "dist/index.js", diff --git a/scoop/appwrite.config.json b/scoop/appwrite.config.json index 4dfe285..9afc1c6 100644 --- a/scoop/appwrite.config.json +++ b/scoop/appwrite.config.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json", - "version": "19.1.0", + "version": "19.2.0", "description": "The Appwrite CLI is a command-line application that allows you to interact with Appwrite and perform server-side tasks using your terminal.", "homepage": "https://github.com/appwrite/sdk-for-cli", "license": "BSD-3-Clause", "architecture": { "64bit": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/19.1.0/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/19.2.0/appwrite-cli-win-x64.exe", "bin": [ [ "appwrite-cli-win-x64.exe", @@ -15,7 +15,7 @@ ] }, "arm64": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/19.1.0/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/19.2.0/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe", From 6848b7614cb4abf0f930fd2a2c7600f672496219 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 28 Apr 2026 16:45:49 +0530 Subject: [PATCH 2/2] chore: update Command Line SDK to 19.2.0 --- lib/completions.ts | 14 ++++++++++++-- lib/utils.ts | 25 ++++++++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/completions.ts b/lib/completions.ts index 95210f0..229c4f6 100644 --- a/lib/completions.ts +++ b/lib/completions.ts @@ -550,8 +550,18 @@ export function createCompletionCommand(rootCommand: Command): Command { return; } - const installPath = installCompletion(rootCommand, shell); - process.stdout.write(`Installed ${shell} completion to ${installPath}\n`); + try { + const installPath = installCompletion(rootCommand, shell); + process.stdout.write( + `Installed ${shell} completion to ${installPath}\n`, + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write( + `Failed to install ${shell} completion: ${message}\n`, + ); + process.exitCode = 1; + } }); for (const shell of SUPPORTED_COMPLETION_SHELLS) { diff --git a/lib/utils.ts b/lib/utils.ts index eb98280..3196b66 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -246,14 +246,25 @@ export const getInstalledHomebrewFormula = ( }, ); const formulaName = EXECUTABLE_NAME.toLowerCase(); - const supportedFormulas = new Set([HOMEBREW_FORMULA, formulaName]); - - return ( - output - .split(/\r?\n/) - .map((formula) => formula.trim()) - .find((formula) => supportedFormulas.has(formula)) ?? null + const preferredFormulas = new Set([HOMEBREW_FORMULA, formulaName]); + const installedFormulas = output + .split(/\r?\n/) + .map((formula) => formula.trim()) + .filter((formula) => formula.length > 0); + + const preferred = installedFormulas.find((formula) => + preferredFormulas.has(formula), ); + if (preferred) { + return preferred; + } + + const tapMatch = installedFormulas.find((formula) => { + const segments = formula.split("/"); + return segments[segments.length - 1] === formulaName; + }); + + return tapMatch ?? null; } catch (_error) { return null; }