From 4959927e16366910c99866b169a3661fb8935b47 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 28 Jul 2026 22:31:45 +0530 Subject: [PATCH 1/2] chore: update Command Line SDK to 24.0.0 --- .github/workflows/publish.yml | 26 ++++---------------------- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- bun.lock | 4 ++-- install.ps1 | 4 ++-- install.sh | 2 +- lib/auth/login.ts | 12 ++++++++---- lib/commands/generic.ts | 8 ++++---- lib/commands/services/backups.ts | 6 +++--- lib/commands/services/storage.ts | 5 +++-- lib/commands/services/tablesdb.ts | 7 ++++--- lib/constants.ts | 2 +- lib/flags.ts | 2 -- lib/sdks.ts | 4 +--- package-lock.json | 12 ++++++------ package.json | 4 ++-- scoop/appwrite.config.json | 6 +++--- 17 files changed, 52 insertions(+), 62 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5a58958..09a4deb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -226,15 +226,12 @@ jobs: echo "formula_path=${FORMULA_PATH}" } >> "$GITHUB_OUTPUT" - - name: Open pull request on Homebrew tap + - name: Commit Homebrew formula update working-directory: homebrew-tap env: RELEASE_TAG: ${{ github.event.release.tag_name }} EXECUTABLE_NAME: ${{ steps.tap.outputs.executable }} FORMULA_PATH: ${{ steps.tap.outputs.formula_path }} - SOURCE_REPO: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GH_TOKEN: ${{ steps.bot-token.outputs.token }} run: | set -euo pipefail @@ -243,27 +240,12 @@ jobs: exit 0 fi - BASE_BRANCH="$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" - BRANCH="release/${EXECUTABLE_NAME}-${RELEASE_TAG}" - git config user.name "appwrite-bot[bot]" git config user.email "217594562+appwrite-bot[bot]@users.noreply.github.com" - git checkout -B "$BRANCH" git add "$FORMULA_PATH" git commit -m "${EXECUTABLE_NAME} ${RELEASE_TAG}" - git push -f -u origin "$BRANCH" - - PR_TITLE="${EXECUTABLE_NAME} ${RELEASE_TAG}" - PR_BODY=$(printf 'Automated formula update for the `%s` CLI release [`%s`](%s/%s/releases/tag/%s).\n\nOpened automatically by the `%s` publish workflow after release binaries were uploaded.' "$EXECUTABLE_NAME" "$RELEASE_TAG" "$SERVER_URL" "$SOURCE_REPO" "$RELEASE_TAG" "${SOURCE_REPO#*/}") - EXISTING_PR="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' || true)" - if [ -n "$EXISTING_PR" ]; then - gh pr edit "$EXISTING_PR" --title "$PR_TITLE" --body "$PR_BODY" - else - gh pr create \ - --title "$PR_TITLE" \ - --body "$PR_BODY" \ - --base "$BASE_BRANCH" \ - --head "$BRANCH" - fi + BRANCH="$(git rev-parse --abbrev-ref HEAD)" + git pull --rebase origin "$BRANCH" + git push origin "HEAD:${BRANCH}" diff --git a/CHANGELOG.md b/CHANGELOG.md index c303cce..29236f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 24.0.0 + +* Added: `login` against Cloud now signs in through your browser +* Updated: `--email`, `--password`, `--mfa`, and `--code` now apply to self-hosted instances only +* Updated: Existing Cloud sessions keep working in read-only mode until you run `login` again + ## 23.2.0 * Added: `apps` installation commands: `list-installations`, `get-installation`, `delete-installation`, `create-installation-token` diff --git a/README.md b/README.md index 91c82b1..a92f8bc 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 -23.2.0 +24.0.0 ``` ### Install using prebuilt binaries @@ -83,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 -23.2.0 +24.0.0 ``` ## Getting Started diff --git a/bun.lock b/bun.lock index 7910561..7cba9a4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "appwrite-cli", "dependencies": { - "@appwrite.io/console": "15.6.0", + "@appwrite.io/console": "15.7.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", @@ -54,7 +54,7 @@ "tmp": "^0.2.6", }, "packages": { - "@appwrite.io/console": ["@appwrite.io/console@15.6.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-dSlNP01auhPeppdXEGg3mtg5nRTxqKX7H9aHsbxE/nOlJAzAGEcz9Lt/gwmY/NwzMcXrugU1+uLcXUA4Cu4XTQ=="], + "@appwrite.io/console": ["@appwrite.io/console@15.7.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-43FVjVbqdedkoiMrAKCFpEnAhBb7YruW9fHqUJ/uvzMT2yg8g8cx+s5b0gkmiC276URALa5BzkYUBoTLw/Dnsw=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], diff --git a/install.ps1 b/install.ps1 index 4698278..cc94c18 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/23.2.0/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/23.2.0/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/24.0.0/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/24.0.0/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index aeecfc6..aec0f10 100644 --- a/install.sh +++ b/install.sh @@ -120,7 +120,7 @@ verifyMacOSCodeSignature() { downloadBinary() { echo "[2/5] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="23.2.0" + GITHUB_LATEST_VERSION="24.0.0" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" diff --git a/lib/auth/login.ts b/lib/auth/login.ts index ead58a4..de562ab 100644 --- a/lib/auth/login.ts +++ b/lib/auth/login.ts @@ -13,7 +13,6 @@ import { isRegionalCloudEndpoint, openBrowser, } from "../utils.js"; -import { isFlagEnabled } from "../flags.js"; import ID from "../id.js"; import { questionsListFactors, @@ -476,8 +475,13 @@ export const loginCommand = async ({ const configEndpoint = normalizeCloudConsoleEndpoint( (endpoint ?? globalConfig.getEndpoint()) || DEFAULT_ENDPOINT, ); - const shouldUseCloudLogin = - isFlagEnabled("oauthLogin") && isCloudLoginEndpoint(configEndpoint); + const shouldUseCloudLogin = isCloudLoginEndpoint(configEndpoint); + + if (shouldUseCloudLogin && (email || password || mfa || code)) { + throw new Error( + `Cloud sign-in happens in your browser. Run '${EXECUTABLE_NAME} login' without --email, --password, --mfa or --code — those options are for self-hosted instances.`, + ); + } let oldCurrent = globalConfig.getCurrentSession(); @@ -493,7 +497,7 @@ export const loginCommand = async ({ if (account) { if ( - isFlagEnabled("oauthLogin") && + shouldUseCloudLogin && !globalConfig.getAccessToken() && globalConfig.getCookie() ) { diff --git a/lib/commands/generic.ts b/lib/commands/generic.ts index 2d4c2d1..cb9e601 100644 --- a/lib/commands/generic.ts +++ b/lib/commands/generic.ts @@ -81,13 +81,13 @@ export const register = new Command("register") export const login = new Command("login") .description(commandDescriptions["login"]) - .option(`--email [email]`, `Email`) - .option(`--password [password]`, `Password`) + .option(`--email [email]`, `Email, for self hosted instances`) + .option(`--password [password]`, `Password, for self hosted instances`) .option( `--mfa [factor]`, - `Factor used for MFA. Must be one of: email, phone, totp, recoveryCode`, + `Factor used for MFA on self hosted instances. Must be one of: email, phone, totp, recoveryCode`, ) - .option(`--code [code]`, `Code used for MFA`) + .option(`--code [code]`, `Code used for MFA on self hosted instances`) .option( `--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`, diff --git a/lib/commands/services/backups.ts b/lib/commands/services/backups.ts index f3b5280..32707d2 100644 --- a/lib/commands/services/backups.ts +++ b/lib/commands/services/backups.ts @@ -181,17 +181,17 @@ const backupsCreateRestorationCommand = backups .command(`create-restoration`) .description(`Create and trigger a new restoration for a backup on a project. -For a backup of one database, the restoration resolves its destination before it is queued. Pass \`newResourceId\` to restore into that database ID, including the archived database ID to overwrite it. When \`newResourceId\` is omitted, a new database ID is generated and returned in \`options\`. +For a backup of one database, the restoration resolves its destination before it is queued. When \`newResourceId\` is omitted, the archived database is restored in place and its own ID is returned in \`options\`. Pass a different \`newResourceId\` to restore alongside it as a new database instead. The restoration migration records the archived database in \`resourceId\` and \`resourceType\`, and the resolved database in \`destinationResourceId\` and \`destinationResourceType\`. Database types are stored canonically as \`database\`, \`documentsdb\`, or \`vectorsdb\`. Project-wide restorations leave these fields empty because they do not have a single source or destination database. To list every migration related to one database, use its canonical type in a nested \`OR(AND(...), AND(...), AND(...))\` across the root, parent, and destination relation pairs: \`(resourceType, resourceId)\`, \`(parentResourceType, parentResourceId)\`, and \`(destinationResourceType, destinationResourceId)\`. Legacy and TablesDB databases use \`database\`; the operational \`resourceType\` of a table migration is not rewritten to \`tablesdb\`. -When restoring a DocumentsDB or VectorsDB database to a new resource from a dedicated source, the restore provisions a fresh dedicated backing database at the source database's own specification. +When restoring a DocumentsDB or VectorsDB database from a dedicated source, the restore provisions a fresh dedicated backing database at the source database's own specification and lands the data there. An in-place restore swaps the database onto that backing only once the restore has succeeded, and retires the backing it displaced only once that swap is confirmed, so the source keeps serving its own data until the restored data is in place and any failure leaves it untouched. A serverless source has no dedicated backing to clone and restores onto the archived database instead. `) .requiredOption(`--archive-id `, `Backup archive ID to restore`) .requiredOption(`--services [services...]`, `Array of services to restore`) - .option(`--new-resource-id `, `Destination resource ID. Omit to generate a new ID, or pass the archived resource ID to overwrite it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.`) + .option(`--new-resource-id `, `Destination resource ID. Omit to restore the archived resource in place, or pass a different ID to restore alongside it as a new resource. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.`) .option(`--new-resource-name `, `Database name. Max length: 128 chars.`) .action( actionRunner( diff --git a/lib/commands/services/storage.ts b/lib/commands/services/storage.ts index e168b4e..bba6616 100644 --- a/lib/commands/services/storage.ts +++ b/lib/commands/services/storage.ts @@ -221,10 +221,11 @@ If you're creating a new file using one of the Appwrite SDKs, all the chunking l .requiredOption(`--file-id `, `File ID. Choose a custom ID or generate a random ID with \`ID.unique()\`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.`) .requiredOption(`--file `, `Binary file. Appwrite SDKs provide helpers to handle file input. Learn about file input (https://appwrite.io/docs/products/storage/upload-download#input-file).`) .option(`--permissions [permissions...]`, `An array of permission strings. By default, only the current user is granted all permissions. Learn more about permissions (https://appwrite.io/docs/permissions).`) + .option(`--folder `, `Virtual folder to place the file in, for example "photos/2026". Nest folders with \`/\`. Defaults to the bucket root.`) .action( actionRunner( - async ({ bucketId, fileId, file, permissions }) => - parse(await (await getStorageClient()).createFile(bucketId, fileId, file !== undefined ? await resolveFileParam(file) : undefined, permissions)), + async ({ bucketId, fileId, file, permissions, folder }) => + parse(await (await getStorageClient()).createFile(bucketId, fileId, file !== undefined ? await resolveFileParam(file) : undefined, permissions, folder)), ), ); diff --git a/lib/commands/services/tablesdb.ts b/lib/commands/services/tablesdb.ts index 0d1f427..883213f 100644 --- a/lib/commands/services/tablesdb.ts +++ b/lib/commands/services/tablesdb.ts @@ -210,11 +210,12 @@ const tablesDBUpdateCommand = tablesDB (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--specification `, `Database specification. Resizing between dedicated specifications changes cpu, memory, storage and the connection ceiling via a rolling cutover with zero downtime. Moving a \`serverless\` database onto a dedicated specification is a data migration, not a resize.`) .option(`--replicas `, `Number of high availability replicas (0-5) for the dedicated database backing this database. Only valid when the database is backed by a dedicated specification. High availability is enabled when greater than 0.`, parseInteger) .action( actionRunner( - async ({ databaseId, name, enabled, replicas }) => - parse(await (await getTablesDBClient()).update(databaseId, name, enabled, replicas)), + async ({ databaseId, name, enabled, specification, replicas }) => + parse(await (await getTablesDBClient()).update(databaseId, name, enabled, specification, replicas)), ), ); @@ -233,7 +234,7 @@ const tablesDBDeleteCommand = tablesDB const tablesDBCreateFailoverCommand = tablesDB .command(`create-failover`) - .description(`Trigger a manual failover for a dedicated database with high availability enabled. Promotes a replica to primary. The failover runs asynchronously; poll the database document for status updates.`) + .description(`Trigger a manual failover for a dedicated database with high availability enabled. Promotes a replica to primary. The failover runs asynchronously; poll the database document for status updates. A database left mid-operation by a failover that did not finish also accepts this call as a repair, provided \`targetReplicaId\` names the member to promote.`) .requiredOption(`--database-id `, `Database ID.`) .option(`--target-replica-id `, `Target replica ID to promote. If not specified, the healthiest replica is selected.`) .action( diff --git a/lib/constants.ts b/lib/constants.ts index f9029f1..76dbc76 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 = '23.2.0'; +export const SDK_VERSION = '24.0.0'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; diff --git a/lib/flags.ts b/lib/flags.ts index 44217f2..d295a9a 100644 --- a/lib/flags.ts +++ b/lib/flags.ts @@ -6,8 +6,6 @@ * and read it anywhere with isFlagEnabled(""). */ const FLAG_ENV_VARS = { - // Browser-based OAuth2 device login for Cloud (otherwise email/password). - oauthLogin: "APPWRITE_CLI_OAUTH_LOGIN", // Treat localhost/loopback endpoints as Cloud for OAuth login testing. devCloudLogin: "APPWRITE_CLI_DEV_CLOUD_LOGIN", } as const; diff --git a/lib/sdks.ts b/lib/sdks.ts index c61c64c..d70c803 100644 --- a/lib/sdks.ts +++ b/lib/sdks.ts @@ -14,7 +14,6 @@ import { } from "./constants.js"; import { warn } from "./parser.js"; import { isCloudHostname } from "./utils.js"; -import { isFlagEnabled } from "./flags.js"; import { getStoredRefreshToken, setStoredRefreshToken, @@ -87,8 +86,7 @@ export const getValidAccessToken = async ( let legacySessionWarningShown = false; const warnLegacySession = (): void => { - // Only nudge toward OAuth login when the feature is enabled. - if (legacySessionWarningShown || !isFlagEnabled("oauthLogin")) { + if (legacySessionWarningShown) { return; } diff --git a/package-lock.json b/package-lock.json index 8e4163f..b8c0934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "appwrite-cli", - "version": "23.2.0", + "version": "24.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "23.2.0", + "version": "24.0.0", "license": "BSD-3-Clause", "dependencies": { - "@appwrite.io/console": "15.6.0", + "@appwrite.io/console": "15.7.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", @@ -60,9 +60,9 @@ } }, "node_modules/@appwrite.io/console": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.6.0.tgz", - "integrity": "sha512-dSlNP01auhPeppdXEGg3mtg5nRTxqKX7H9aHsbxE/nOlJAzAGEcz9Lt/gwmY/NwzMcXrugU1+uLcXUA4Cu4XTQ==", + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.7.0.tgz", + "integrity": "sha512-43FVjVbqdedkoiMrAKCFpEnAhBb7YruW9fHqUJ/uvzMT2yg8g8cx+s5b0gkmiC276URALa5BzkYUBoTLw/Dnsw==", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" diff --git a/package.json b/package.json index f910e10..4ff852c 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": "23.2.0", + "version": "24.0.0", "license": "BSD-3-Clause", "main": "dist/index.cjs", "module": "dist/index.js", @@ -51,7 +51,7 @@ "windows-arm64": "bun build cli.ts --compile --minify --target=bun-windows-arm64 --outfile build/appwrite-cli-win-arm64.exe" }, "dependencies": { - "@appwrite.io/console": "15.6.0", + "@appwrite.io/console": "15.7.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", diff --git a/scoop/appwrite.config.json b/scoop/appwrite.config.json index 41ca51c..f955d79 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": "23.2.0", + "version": "24.0.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/23.2.0/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/24.0.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/23.2.0/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/24.0.0/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe", From a43f78a226514bee4af130fe77bc2fd413a971f3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 30 Jul 2026 13:28:54 +0530 Subject: [PATCH 2/2] chore: update Command Line SDK to 24.1.0 --- CHANGELOG.md | 17 + README.md | 4 +- bun.lock | 6 +- .../project/update-session-duration-policy.md | 2 +- .../project/update-user-limit-policy.md | 2 +- install.ps1 | 4 +- install.sh | 2 +- lib/auth/session.ts | 81 +++- lib/commands/config-validations.ts | 38 +- lib/commands/config.ts | 6 + lib/commands/generic.ts | 108 ++++- lib/commands/push.ts | 36 ++ lib/commands/services/project.ts | 8 +- lib/commands/services/tablesdb.ts | 10 +- lib/commands/utils/attributes.ts | 422 +++++++++++++++--- lib/commands/utils/pools.ts | 66 ++- lib/config.ts | 12 + lib/constants.ts | 2 +- lib/parser.ts | 39 +- lib/questions.ts | 17 + lib/sdks.ts | 6 +- package-lock.json | 25 +- package.json | 4 +- scoop/appwrite.config.json | 6 +- 24 files changed, 786 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29236f1..89abbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Change Log +## 24.1.0 + +* Added: `previousKey` on an attribute or column renames it in place on `push` +* Fixed: `client --endpoint` reuses or switches an existing session instead of minting an unauthenticated stub +* Fixed: `client --reset` asks for confirmation before signing out signed-in accounts +* Fixed: Commands that need a session now list signed-in accounts and suggest `login --switch` +* Fixed: Regional cloud endpoints match their canonical host when resolving a session +* Fixed: `push` updates attributes in place instead of recreating them where the API allows it +* Fixed: `push` resizes a `string` attribute in place instead of recreating it +* Fixed: `push` no longer recreates indexes whose attributes were renamed +* Fixed: `push` aborts on a failed attribute update instead of continuing past it +* Fixed: `push` waits for index deletion to finish before recreating indexes +* Fixed: `push` leaves `encrypt` untouched when the config omits it +* Fixed: `logout` signs out the selected account instead of the current session id +* Fixed: Table output wraps long reasons instead of truncating them mid-word +* Updated: Project policy limits for password history, session duration, session count, and users + ## 24.0.0 * Added: `login` against Cloud now signs in through your browser diff --git a/README.md b/README.md index a92f8bc..73949f1 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 -24.0.0 +24.1.0 ``` ### Install using prebuilt binaries @@ -83,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 -24.0.0 +24.1.0 ``` ## Getting Started diff --git a/bun.lock b/bun.lock index 7cba9a4..f2d1bac 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "appwrite-cli", "dependencies": { - "@appwrite.io/console": "15.7.0", + "@appwrite.io/console": "15.8.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", @@ -54,7 +54,7 @@ "tmp": "^0.2.6", }, "packages": { - "@appwrite.io/console": ["@appwrite.io/console@15.7.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-43FVjVbqdedkoiMrAKCFpEnAhBb7YruW9fHqUJ/uvzMT2yg8g8cx+s5b0gkmiC276URALa5BzkYUBoTLw/Dnsw=="], + "@appwrite.io/console": ["@appwrite.io/console@15.8.0", "", { "dependencies": { "json-bigint": "1.0.0" } }, "sha512-OJvWFBm7x2Si49UzzRfTy9tq5wT7M4AWJ7+ii/xJYfEpiw87wgaCZSDln8LAmPNGqRZNtvyXCalf8K1aaD+0ig=="], "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], @@ -296,7 +296,7 @@ "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], - "acorn": ["acorn@8.17.0", "", { "bin": "bin/acorn" }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "acorn": ["acorn@8.18.0", "", { "bin": "bin/acorn" }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], diff --git a/docs/examples/project/update-session-duration-policy.md b/docs/examples/project/update-session-duration-policy.md index dbb80ef..a454de1 100644 --- a/docs/examples/project/update-session-duration-policy.md +++ b/docs/examples/project/update-session-duration-policy.md @@ -1,4 +1,4 @@ ```bash appwrite project update-session-duration-policy \ - --duration 5 + --duration 60 ``` diff --git a/docs/examples/project/update-user-limit-policy.md b/docs/examples/project/update-user-limit-policy.md index 45e0fe9..fcdecd6 100644 --- a/docs/examples/project/update-user-limit-policy.md +++ b/docs/examples/project/update-user-limit-policy.md @@ -1,4 +1,4 @@ ```bash appwrite project update-user-limit-policy \ - --total 1 + --total 0 ``` diff --git a/install.ps1 b/install.ps1 index cc94c18..291334b 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/24.0.0/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/24.0.0/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/24.1.0/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/24.1.0/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index aec0f10..8c48296 100644 --- a/install.sh +++ b/install.sh @@ -120,7 +120,7 @@ verifyMacOSCodeSignature() { downloadBinary() { echo "[2/5] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="24.0.0" + GITHUB_LATEST_VERSION="24.1.0" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" diff --git a/lib/auth/session.ts b/lib/auth/session.ts index 369cfd6..d8c509f 100644 --- a/lib/auth/session.ts +++ b/lib/auth/session.ts @@ -1,4 +1,8 @@ -import { globalConfig, normalizeCloudConsoleEndpoint } from "../config.js"; +import { + endpointsMatch, + globalConfig, + normalizeCloudConsoleEndpoint, +} from "../config.js"; import type { SessionData } from "../types.js"; import ClientLegacy from "../client.js"; import { OAUTH2_CLIENT_ID } from "../constants.js"; @@ -32,6 +36,81 @@ export const createLegacyConsoleClient = ( export const hasAuthSession = (): boolean => globalConfig.getAccessToken() !== "" || globalConfig.getCookie() !== ""; +/** + * Whether a stored session record has console auth material (token or cookie), + * independent of which session is currently active. + */ +export const isAuthenticatedSession = (sessionId: string): boolean => { + const session = getSession(sessionId); + return Boolean(session?.accessToken || session?.cookie); +}; + +/** + * Find existing sessions that match an endpoint. Prefers authenticated + * sessions over endpoint-only stubs so `client --endpoint` can reuse login + * state instead of minting a new unauthenticated record. + */ +export const findSessionForEndpoint = ( + endpoint: string, +): { authenticated?: string; endpointOnly?: string } => { + let authenticated: string | undefined; + let endpointOnly: string | undefined; + + for (const sessionId of globalConfig.getSessionIds()) { + const session = getSession(sessionId); + if (!session?.endpoint || !endpointsMatch(session.endpoint, endpoint)) { + continue; + } + + if (isAuthenticatedSession(sessionId)) { + if (!authenticated) { + authenticated = sessionId; + } + } else if (!endpointOnly) { + endpointOnly = sessionId; + } + } + + return { authenticated, endpointOnly }; +}; + +/** + * Deduped signed-in accounts (email + auth material) for recoverable + * messaging when the current session pointer is unauthenticated. + */ +export const getSignedInAccounts = (): Array<{ + id: string; + email: string; + endpoint: string; +}> => { + const accounts = new Map< + string, + { id: string; email: string; endpoint: string } + >(); + const current = globalConfig.getCurrentSession(); + + for (const sessionId of globalConfig.getSessionIds()) { + const session = getSession(sessionId); + if (!session?.email || !isAuthenticatedSession(sessionId)) { + continue; + } + + const endpoint = normalizeCloudConsoleEndpoint(session.endpoint ?? ""); + const key = `${session.email}|${endpoint}`; + const existing = accounts.get(key); + + if (!existing || sessionId === current || existing.id !== current) { + accounts.set(key, { + id: sessionId, + email: session.email, + endpoint: session.endpoint ?? endpoint, + }); + } + } + + return Array.from(accounts.values()); +}; + /** * A session that exists only in local config (no server-side credential to * revoke), e.g. an endpoint/key-only entry created by `client --endpoint`. diff --git a/lib/commands/config-validations.ts b/lib/commands/config-validations.ts index 1489228..f45acf1 100644 --- a/lib/commands/config-validations.ts +++ b/lib/commands/config-validations.ts @@ -50,6 +50,7 @@ export const validateStringSize = (data: { interface AttributeOrColumn { key: string; + previousKey?: string; } interface Index { @@ -63,7 +64,8 @@ interface CollectionOrTableData { } /** - * Validates duplicate keys in attributes/columns and indexes + * Validates duplicate keys in attributes/columns and indexes, + * plus previousKey rename-hint consistency. */ export const validateContainerDuplicates = ( data: CollectionOrTableData, @@ -88,6 +90,40 @@ export const validateContainerDuplicates = ( seenKeys.add(item.key); } }); + + // Validate previousKey rename hints + const seenPreviousKeys = new Set(); + items.forEach((item, index) => { + if (!item.previousKey) { + return; + } + + if (item.previousKey === item.key) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${itemType} '${item.key}' has previousKey equal to key. previousKey must be the old name, not the new one.`, + path: [itemPath, index, "previousKey"], + }); + } + + if (seenKeys.has(item.previousKey) && item.previousKey !== item.key) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${itemType} '${item.key}' has previousKey '${item.previousKey}', but another ${itemType.toLowerCase()} already uses that key. Rename cannot proceed while both names exist locally.`, + path: [itemPath, index, "previousKey"], + }); + } + + if (seenPreviousKeys.has(item.previousKey)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Multiple ${itemType.toLowerCase()}s share previousKey '${item.previousKey}'. Each previousKey must be unique within a ${itemType === "Attribute" ? "collection" : "table"}.`, + path: [itemPath, index, "previousKey"], + }); + } else { + seenPreviousKeys.add(item.previousKey); + } + }); } // Validate duplicate index keys diff --git a/lib/commands/config.ts b/lib/commands/config.ts index d4a148c..3933f5c 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -248,6 +248,9 @@ const AttributeSchema = z attributes: z.array(z.string()).optional(), orders: z.array(z.string()).optional(), encrypt: z.boolean().optional(), + // Local-only hint for in-place renames via the API's newKey parameter. + // Cleared automatically on pull; ignored once the rename has been applied. + previousKey: z.string().optional(), }) .strict() .refine(validateRequiredDefault, { @@ -330,6 +333,9 @@ const ColumnSchema = z columns: z.array(z.string()).optional(), orders: z.array(z.string()).optional(), encrypt: z.boolean().optional(), + // Local-only hint for in-place renames via the API's newKey parameter. + // Cleared automatically on pull; ignored once the rename has been applied. + previousKey: z.string().optional(), }) .strict() .refine(validateRequiredDefault, { diff --git a/lib/commands/generic.ts b/lib/commands/generic.ts index cb9e601..3d10fcf 100644 --- a/lib/commands/generic.ts +++ b/lib/commands/generic.ts @@ -1,7 +1,7 @@ import inquirer from "inquirer"; import { Command } from "commander"; import { Client } from "@appwrite.io/console"; -import { globalConfig, localConfig } from "../config.js"; +import { endpointsMatch, globalConfig, localConfig } from "../config.js"; import { EXECUTABLE_NAME } from "../constants.js"; import { actionRunner, @@ -11,14 +11,19 @@ import { error, parse, log, + warn, drawTable, cliConfig, } from "../parser.js"; import ID from "../id.js"; -import { questionsLogout } from "../questions.js"; +import { questionsClientReset, questionsLogout } from "../questions.js"; import { getCurrentAccount, loginCommand } from "../auth/login.js"; import { + findSessionForEndpoint, + getSession, + getSignedInAccounts, hasAuthSession, + isAuthenticatedSession, logoutSessions, planSessionLogout, restoreCurrentSessionFallback, @@ -35,20 +40,52 @@ const logMessages = { }, logoutSuccess: "Logged out successfully", clientConfigUpdated: "Client configuration updated", + noUserSignedIn: "No user is signed in. To sign in, run 'appwrite login'", } as const; +const hintSignedInAccounts = (): void => { + if (cliConfig.json || cliConfig.raw) { + return; + } + + const accounts = getSignedInAccounts(); + if (accounts.length === 0) { + return; + } + + log("Signed-in accounts are still available:"); + for (const account of accounts) { + log(` ${account.email} (${account.endpoint})`); + } + log(`Run '${EXECUTABLE_NAME} login --switch' to select one.`); +}; + +const warnDetachedAuthenticatedSession = (previousSessionId: string): void => { + if (!previousSessionId || !isAuthenticatedSession(previousSessionId)) { + return; + } + + const previous = getSession(previousSessionId); + const email = previous?.email ? ` (${previous.email})` : ""; + warn( + `Signed-in account${email} is still available but no longer active. Run '${EXECUTABLE_NAME} login --switch' to return to it.`, + ); +}; + export const whoami = new Command("whoami") .description(commandDescriptions["whoami"]) .action( actionRunner(async () => { if (globalConfig.getEndpoint() === "" || !hasAuthSession()) { - error("No user is signed in. To sign in, run 'appwrite login'"); + error(logMessages.noUserSignedIn); + hintSignedInAccounts(); return; } const account = await getCurrentAccount(); if (!account) { - error("No user is signed in. To sign in, run 'appwrite login'"); + error(logMessages.noUserSignedIn); + hintSignedInAccounts(); return; } @@ -110,13 +147,17 @@ export const logout = new Command("logout") const current = globalConfig.getCurrentSession(); const originalCurrent = current; - if (current === "" || !sessions.length) { + // The picker only offers sessions with an email, so count those — counting + // endpoint-only entries would open a checkbox with no selectable options. + const accounts = sessions.filter((session) => session.email); + + if (current === "" || !accounts.length) { log(logMessages.noActiveSessions); return; } - if (sessions.length === 1) { + if (accounts.length === 1) { const { failed, failedIds, errors } = await logoutSessions( - planSessionLogout([current]), + planSessionLogout([accounts[0].id]), ); if (failed > 0) { @@ -245,7 +286,6 @@ export const client = new Command("client") if (endpoint !== undefined) { try { - const id = ID.unique(); const url = new URL(endpoint); if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error(); @@ -263,9 +303,40 @@ export const client = new Command("client") if (!response.version) { throw new Error(); } - globalConfig.setCurrentSession(id); - globalConfig.addSession(id, { endpoint }); - globalConfig.setEndpoint(endpoint); + + const previous = globalConfig.getCurrentSession(); + const match = findSessionForEndpoint(endpoint); + + if ( + previous && + endpointsMatch(getSession(previous)?.endpoint ?? "", endpoint) && + (isAuthenticatedSession(previous) || !match.authenticated) + ) { + // Already on the best available session for this endpoint — keep + // current and refresh the stored value so regional hosts stay as + // requested. + globalConfig.setEndpoint(endpoint); + } else if (match.authenticated) { + globalConfig.setCurrentSession(match.authenticated); + globalConfig.setEndpoint(endpoint); + const email = getSession(match.authenticated)?.email; + if (email) { + log(`Using signed-in account ${email}`); + } + } else if (match.endpointOnly) { + globalConfig.setCurrentSession(match.endpointOnly); + globalConfig.setEndpoint(endpoint); + warnDetachedAuthenticatedSession(previous); + } else if (previous && !isAuthenticatedSession(previous)) { + // Update an existing endpoint-only stub in place. + globalConfig.setEndpoint(endpoint); + } else { + const id = ID.unique(); + globalConfig.addSession(id, { endpoint }); + globalConfig.setCurrentSession(id); + globalConfig.setEndpoint(endpoint); + warnDetachedAuthenticatedSession(previous); + } } catch (_) { throw new Error( "Invalid endpoint or your Appwrite server is not running as expected.", @@ -296,6 +367,21 @@ export const client = new Command("client") } if (reset !== undefined) { + const accounts = getSignedInAccounts(); + if (accounts.length > 0 && !cliConfig.force) { + if (!process.stdin.isTTY) { + throw new Error( + `Resetting will sign out ${accounts.map((account) => account.email).join(", ")}. Re-run with --force to confirm.`, + ); + } + + const answers = await inquirer.prompt(questionsClientReset(accounts)); + if (!answers.confirm) { + log("Reset cancelled."); + return; + } + } + const originalCurrent = globalConfig.getCurrentSession(); const { failed, failedIds, errors } = await logoutSessions( globalConfig.getSessionIds(), diff --git a/lib/commands/push.ts b/lib/commands/push.ts index bef2aa5..0abbb76 100644 --- a/lib/commands/push.ts +++ b/lib/commands/push.ts @@ -2946,6 +2946,23 @@ export class Push { table.columns, table as Collection, ); + + // Server rewrites index column refs during rename; mirror that on the + // stale remote snapshot so the index pass does not spuriously recreate. + const renameMap = new Map( + columnsResult.renames.map((r) => [r.from, r.to]), + ); + if (renameMap.size && Array.isArray(table.remoteVersion.indexes)) { + table.remoteVersion.indexes = table.remoteVersion.indexes.map( + (idx: any) => ({ + ...idx, + columns: idx.columns?.map( + (c: string) => renameMap.get(c) ?? c, + ), + }), + ); + } + const indexesResult = await attributes.attributesToCreate( table.remoteVersion.indexes, table.indexes, @@ -3128,6 +3145,25 @@ export class Push { collection.attributes ?? [], collection as Collection, ); + + // Server rewrites index attribute refs during rename; mirror that on the + // stale remote snapshot so the index pass does not spuriously recreate. + const renameMap = new Map( + collectionAttributesResult.renames.map((r) => [r.from, r.to]), + ); + if ( + renameMap.size && + Array.isArray(collection.remoteVersion!.indexes) + ) { + collection.remoteVersion!.indexes = + collection.remoteVersion!.indexes.map((idx: any) => ({ + ...idx, + attributes: idx.attributes?.map( + (a: string) => renameMap.get(a) ?? a, + ), + })); + } + const indexesResult = await attributesHelper.attributesToCreate( collection.remoteVersion!.indexes, collection.indexes ?? [], diff --git a/lib/commands/services/project.ts b/lib/commands/services/project.ts index b7f2ead..a48edd2 100644 --- a/lib/commands/services/project.ts +++ b/lib/commands/services/project.ts @@ -1464,7 +1464,7 @@ const projectUpdatePasswordHistoryPolicyCommand = project .description(`Updates one of password strength policies. Based on total length configured, previous password hashes are stored, and users cannot choose a new password that is already stored in the passwird history list, when updating an user password, or setting new one through password recovery. Keep in mind, while password history policy is disabled, the history is not being stored. Enabling the policy will not have any history on existing users, and it will only start to collect and enforce the policy on password changes since the policy is enabled.`) - .requiredOption(`--total `, `Set the password history length per user. Value can be between 1 and 5000, or null to disable the limit.`, parseInteger) + .requiredOption(`--total `, `Set the password history length per user. Value can be between 1 and 20, or null to disable the limit.`, parseInteger) .action( actionRunner( async ({ total }) => @@ -1536,7 +1536,7 @@ const projectUpdateSessionAlertPolicyCommand = project const projectUpdateSessionDurationPolicyCommand = project .command(`update-session-duration-policy`) .description(`Update maximum duration how long sessions created within a project should stay active for.`) - .requiredOption(`--duration `, `Maximum session length in seconds. Minium allowed value is 5 second, and maximum is 1 year, which is 31536000 seconds.`, parseInteger) + .requiredOption(`--duration `, `Maximum session length in seconds. Minium allowed value is 60 seconds, and maximum is 1 year, which is 31536000 seconds.`, parseInteger) .action( actionRunner( async ({ duration }) => @@ -1560,7 +1560,7 @@ const projectUpdateSessionInvalidationPolicyCommand = project const projectUpdateSessionLimitPolicyCommand = project .command(`update-session-limit-policy`) .description(`Update the maximum number of sessions allowed per user. When the limit is hit, the oldest session will be deleted to make room for new one.`) - .requiredOption(`--total `, `Set the maximum number of sessions allowed per user. Value can be between 1 and 5000, or null to disable the limit.`, parseInteger) + .requiredOption(`--total `, `Set the maximum number of sessions allowed per user. Value can be between 1 and 100.`, parseInteger) .action( actionRunner( async ({ total }) => @@ -1572,7 +1572,7 @@ const projectUpdateSessionLimitPolicyCommand = project const projectUpdateUserLimitPolicyCommand = project .command(`update-user-limit-policy`) .description(`Update the maximum number of users in the project. When the limit is hit or amount of existing users already exceeded the limit, all users remain active, but new user sign up will be prohibited.`) - .requiredOption(`--total `, `Set the maximum number of users allowed in the project. Value can be between 1 and 5000, or null to disable the limit.`, parseInteger) + .requiredOption(`--total `, `Set the maximum number of users allowed in the project. Value can be between 0 and 10000. Use 0 or null to disable the limit.`, parseInteger) .action( actionRunner( async ({ total }) => diff --git a/lib/commands/services/tablesdb.ts b/lib/commands/services/tablesdb.ts index 883213f..0570ef5 100644 --- a/lib/commands/services/tablesdb.ts +++ b/lib/commands/services/tablesdb.ts @@ -76,10 +76,11 @@ const tablesDBCreateCommand = tablesDB ) .option(`--specification `, `Database specification. Defaults to \`serverless\`, which creates the database on the shared pool. Any other value provisions a dedicated database on that specification.`) .option(`--replicas `, `Number of high availability replicas (0-5) for the dedicated database backing this database. Requires a dedicated \`specification\`; must be 0 for a serverless database. High availability is enabled when greater than 0.`, parseInteger) + .option(`--sync-mode `, `Replication sync mode for the dedicated database backing this database. Requires a dedicated \`specification\`; the mode is only in force once there is at least one replica. Allowed values: async, sync, quorum.`) .action( actionRunner( - async ({ databaseId, name, enabled, specification, replicas }) => - parse(await (await getTablesDBClient()).create(databaseId, name, enabled, specification, replicas)), + async ({ databaseId, name, enabled, specification, replicas, syncMode }) => + parse(await (await getTablesDBClient()).create(databaseId, name, enabled, specification, replicas, syncMode)), ), ); @@ -212,10 +213,11 @@ const tablesDBUpdateCommand = tablesDB ) .option(`--specification `, `Database specification. Resizing between dedicated specifications changes cpu, memory, storage and the connection ceiling via a rolling cutover with zero downtime. Moving a \`serverless\` database onto a dedicated specification is a data migration, not a resize.`) .option(`--replicas `, `Number of high availability replicas (0-5) for the dedicated database backing this database. Only valid when the database is backed by a dedicated specification. High availability is enabled when greater than 0.`, parseInteger) + .option(`--sync-mode `, `Replication sync mode for the dedicated database backing this database. Only valid when the database is backed by a dedicated specification; the mode is only in force once there is at least one replica. Allowed values: async, sync, quorum.`) .action( actionRunner( - async ({ databaseId, name, enabled, specification, replicas }) => - parse(await (await getTablesDBClient()).update(databaseId, name, enabled, specification, replicas)), + async ({ databaseId, name, enabled, specification, replicas, syncMode }) => + parse(await (await getTablesDBClient()).update(databaseId, name, enabled, specification, replicas, syncMode)), ), ); diff --git a/lib/commands/utils/attributes.ts b/lib/commands/utils/attributes.ts index 3cda601..22c1376 100644 --- a/lib/commands/utils/attributes.ts +++ b/lib/commands/utils/attributes.ts @@ -1,21 +1,96 @@ import chalk from "chalk"; import { getDatabasesService } from "../../services.js"; -import { KeysAttributes } from "../../config.js"; -import { log, success, error, cliConfig, drawTable } from "../../parser.js"; +import { log, warn, success, cliConfig, drawTable } from "../../parser.js"; import { Pools } from "./pools.js"; import inquirer from "inquirer"; import type { Client } from "@appwrite.io/console"; -const changeableKeys = [ - "status", - "required", - "xdefault", - "elements", - "min", - "max", - "default", - "error", -]; +/** + * Per-type field rules for push diffing. + * - updatable: can be changed in place via update*Attribute / update*Column + * - recreate: require delete + recreate (no update API accepts them) + * Fields in neither set are ignored (server-derived or irrelevant). + */ +interface FieldRules { + updatable: string[]; + recreate: string[]; +} + +const COMMON_RECREATE_KEYS = ["type", "array", "encrypt", "format"]; + +const getAttributeFieldRules = (attribute: any): FieldRules => { + const type = attribute?.type; + const format = attribute?.format || ""; + + switch (type) { + case "string": + switch (format) { + case "enum": + return { + updatable: ["required", "default", "elements"], + recreate: COMMON_RECREATE_KEYS, + }; + case "email": + case "url": + case "ip": + return { + updatable: ["required", "default"], + recreate: COMMON_RECREATE_KEYS, + }; + default: + return { + updatable: ["required", "default", "size"], + recreate: COMMON_RECREATE_KEYS, + }; + } + case "varchar": + return { + updatable: ["required", "default", "size"], + recreate: COMMON_RECREATE_KEYS, + }; + case "text": + case "mediumtext": + case "longtext": + case "boolean": + case "datetime": + case "point": + case "linestring": + case "polygon": + return { + updatable: ["required", "default"], + recreate: COMMON_RECREATE_KEYS, + }; + case "integer": + case "bigint": + case "double": + return { + updatable: ["required", "default", "min", "max"], + recreate: COMMON_RECREATE_KEYS, + }; + case "relationship": + return { + updatable: ["onDelete"], + recreate: [ + "type", + "relatedTable", + "relatedCollection", + "relationType", + "twoWay", + "twoWayKey", + ], + }; + default: + return { + updatable: ["required", "default"], + recreate: COMMON_RECREATE_KEYS, + }; + } +}; + +const INDEX_FIELD_RULES: FieldRules = { + updatable: [], + recreate: ["type", "attributes", "columns", "orders"], +}; export interface AttributeChange { key: string; @@ -24,6 +99,13 @@ export interface AttributeChange { action: string; } +export interface AttributeRename { + from: string; + to: string; + /** Remote attribute snapshot used for the update* call (key = from). */ + attribute: any; +} + export interface Collection { $id: string; databaseId: string; @@ -113,33 +195,50 @@ export class Attributes { local: any, reason: string, key: string, + immutable: boolean = false, ): string => { + // Omitted local fields mean "leave remote as-is" (e.g. encrypt not in config). + if (local === undefined) { + return reason; + } + if (this.isEmpty(remote) && this.isEmpty(local)) { return reason; } + const suffix = immutable + ? " (cannot be changed in place, requires recreation)" + : ""; + if (Array.isArray(remote) && Array.isArray(local)) { if (JSON.stringify(remote) !== JSON.stringify(local)) { const bol = reason === "" ? "" : "\n"; - reason += `${bol}${key} changed from ${chalk.red(remote)} to ${chalk.green(local)}`; + reason += `${bol}${key} changed from ${chalk.red(remote)} to ${chalk.green(local)}${suffix}`; } } else if (!this.isEqual(remote, local)) { const bol = reason === "" ? "" : "\n"; - reason += `${bol}${key} changed from ${chalk.red(remote)} to ${chalk.green(local)}`; + reason += `${bol}${key} changed from ${chalk.red(remote)} to ${chalk.green(local)}${suffix}`; } return reason; }; + private getFieldRules = ( + entity: any, + isIndex: boolean = false, + ): FieldRules => (isIndex ? INDEX_FIELD_RULES : getAttributeFieldRules(entity)); + /** - * Check if attribute non-changeable fields has been changed - * If so return the differences as an object. + * Check if attribute fields have changed. + * When recreating=true, only immutable (recreate-forcing) fields are compared. + * When recreating=false, only updatable fields are compared. */ private checkAttributeChanges = ( remote: any, local: any, collection: Collection, recreating: boolean = true, + isIndex: boolean = false, ): AttributeChange | undefined => { if (local === undefined) { return undefined; @@ -149,24 +248,17 @@ export class Attributes { const action = chalk.cyan(recreating ? "recreating" : "changing"); let reason = ""; const attribute = recreating ? remote : local; + const rules = this.getFieldRules(local, isIndex); + const keys = recreating ? rules.recreate : rules.updatable; - for (const key of Object.keys(remote)) { - if (!KeysAttributes.has(key)) { - continue; - } - - if (changeableKeys.includes(key)) { - if (!recreating) { - reason = this.compareAttribute(remote[key], local[key], reason, key); - } - continue; - } - - if (!recreating) { - continue; - } - - reason = this.compareAttribute(remote[key], local[key], reason, key); + for (const key of keys) { + reason = this.compareAttribute( + remote[key], + local[key], + reason, + key, + recreating, + ); } return reason === "" @@ -386,11 +478,41 @@ export class Attributes { } }; + private formatUpdateError = (attribute: any, err: unknown): string => { + const message = String(err); + const key = attribute?.key ?? "unknown"; + const isResize = + message.includes("attribute_invalid_resize") || + message.includes("column_invalid_resize") || + message.includes("invalid_resize"); + + if (isResize) { + return ( + `Failed to update "${key}": existing values exceed the new size. ` + + `Increase the size, shorten existing data, or recreate the attribute. ` + + `(${message})` + ); + } + + return `Failed to update "${key}": ${message}`; + }; + public updateAttribute = async ( databaseId: string, collectionId: string, attribute: any, + newKey?: string, ): Promise => { + // Indexes have no update endpoint; callers must recreate them. + if ( + Array.isArray(attribute.attributes) || + Array.isArray(attribute.columns) + ) { + throw new Error( + `Indexes cannot be updated in place (key: ${attribute.key}). Recreate the index instead.`, + ); + } + const databasesService = await getDatabasesService(this.client); switch (attribute.type) { case "string": @@ -402,6 +524,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "url": return databasesService.updateUrlAttribute({ @@ -410,6 +533,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "ip": return databasesService.updateIpAttribute({ @@ -418,6 +542,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "enum": return databasesService.updateEnumAttribute({ @@ -427,6 +552,7 @@ export class Attributes { elements: attribute.elements, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); default: return databasesService.updateStringAttribute({ @@ -435,6 +561,8 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + size: attribute.size, + newKey: newKey, }); } case "varchar": @@ -445,6 +573,7 @@ export class Attributes { required: attribute.required, xdefault: attribute.default, size: attribute.size, + newKey: newKey, }); case "text": return databasesService.updateTextAttribute({ @@ -453,6 +582,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "mediumtext": return databasesService.updateMediumtextAttribute({ @@ -461,6 +591,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "longtext": return databasesService.updateLongtextAttribute({ @@ -469,6 +600,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "integer": return databasesService.updateIntegerAttribute({ @@ -479,6 +611,7 @@ export class Attributes { min: attribute.min, max: attribute.max, xdefault: attribute.default, + newKey: newKey, }); case "bigint": return databasesService.updateBigIntAttribute({ @@ -489,6 +622,7 @@ export class Attributes { min: attribute.min, max: attribute.max, xdefault: attribute.default, + newKey: newKey, }); case "double": return databasesService.updateFloatAttribute({ @@ -499,6 +633,7 @@ export class Attributes { min: attribute.min, max: attribute.max, xdefault: attribute.default, + newKey: newKey, }); case "boolean": return databasesService.updateBooleanAttribute({ @@ -507,6 +642,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "datetime": return databasesService.updateDatetimeAttribute({ @@ -515,6 +651,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "relationship": return databasesService.updateRelationshipAttribute({ @@ -522,6 +659,7 @@ export class Attributes { collectionId, key: attribute.key, onDelete: attribute.onDelete, + newKey: newKey, }); case "point": return databasesService.updatePointAttribute({ @@ -530,6 +668,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "linestring": return databasesService.updateLineAttribute({ @@ -538,6 +677,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); case "polygon": return databasesService.updatePolygonAttribute({ @@ -546,6 +686,7 @@ export class Attributes { key: attribute.key, required: attribute.required, xdefault: attribute.default, + newKey: newKey, }); default: throw new Error(`Unsupported attribute type: ${attribute.type}`); @@ -585,6 +726,67 @@ export class Attributes { private isChildSideRelationship = (attribute: any): boolean => attribute.type === "relationship" && attribute.side === "child"; + /** + * Resolve previousKey rename hints against the remote snapshot. + * Patches matched remote keys in place (before classification) so a pure + * rename does not surface as delete+add. API calls are executed later. + */ + private resolveRenames = ( + remoteAttributes: any[], + localAttributes: any[], + collection: Collection, + ): { renames: AttributeRename[]; renameChanges: AttributeChange[] } => { + const renames: AttributeRename[] = []; + const renameChanges: AttributeChange[] = []; + + for (const local of localAttributes) { + if (!local.previousKey || local.previousKey === local.key) { + continue; + } + + const remotePrevious = remoteAttributes.find( + (attr) => attr.key === local.previousKey, + ); + const remoteCurrent = remoteAttributes.find( + (attr) => attr.key === local.key, + ); + + if (remotePrevious && !remoteCurrent) { + // Pending rename: patch remote key so classification matches by new name. + renames.push({ + from: local.previousKey, + to: local.key, + attribute: { ...remotePrevious }, + }); + renameChanges.push({ + key: `${chalk.yellow(local.key)} in ${collection.name} (${collection["$id"]})`, + attribute: { ...remotePrevious }, + reason: `key renamed from ${chalk.red(local.previousKey)} to ${chalk.green(local.key)}`, + action: chalk.cyan("renaming"), + }); + remotePrevious.key = local.key; + continue; + } + + if (remoteCurrent && !remotePrevious) { + // Already renamed on the server; hint is stale and harmless. + continue; + } + + if (!remotePrevious && !remoteCurrent) { + // Fresh create; ignore the hint and let the add path handle it. + continue; + } + + // Both keys exist remotely — cannot rename without a collision. + warn( + `Ignoring previousKey "${local.previousKey}" for "${local.key}" in ${collection.name} (${collection["$id"]}): both keys already exist remotely. "${local.previousKey}" will be treated as a deletion if it is absent from the local config.`, + ); + } + + return { renames, renameChanges }; + }; + /** * Filter deleted and recreated attributes, * return list of attributes to create and whether any changes were made @@ -594,7 +796,11 @@ export class Attributes { localAttributes: any[], collection: Collection, isIndex: boolean = false, - ): Promise<{ attributes: any[]; hasChanges: boolean }> => { + ): Promise<{ + attributes: any[]; + hasChanges: boolean; + renames: AttributeRename[]; + }> => { // Filter out child-side relationships from both local and remote attributes for comparison // Child-side relationships are auto-generated by Appwrite when creating two-way relationships // from the parent side, so we should not compare or try to create them directly @@ -605,6 +811,16 @@ export class Attributes { (attr) => !this.isChildSideRelationship(attr), ); + // Resolve previousKey hints before classification so renames do not + // appear as delete+add. Indexes have no rename API — skip entirely. + const { renames, renameChanges } = isIndex + ? { renames: [] as AttributeRename[], renameChanges: [] as AttributeChange[] } + : this.resolveRenames( + filteredRemoteAttributes, + filteredLocalAttributes, + collection, + ); + const deleting = filteredRemoteAttributes .filter( (attribute) => @@ -623,6 +839,8 @@ export class Attributes { attribute, this.attributesContains(attribute, filteredLocalAttributes), collection, + true, + isIndex, ), ) .filter((attribute) => attribute !== undefined) as AttributeChange[]; @@ -633,6 +851,7 @@ export class Attributes { this.attributesContains(attribute, filteredLocalAttributes), collection, false, + isIndex, ), ) .filter((attribute) => attribute !== undefined) @@ -642,9 +861,15 @@ export class Attributes { ) as AttributeChange[]; let changedAttributes: any[] = []; - const changing = [...deleting, ...adding, ...conflicts, ...changes]; + const changing = [ + ...renameChanges, + ...deleting, + ...adding, + ...conflicts, + ...changes, + ]; if (changing.length === 0) { - return { attributes: changedAttributes, hasChanges: false }; + return { attributes: changedAttributes, hasChanges: false, renames: [] }; } log( @@ -690,7 +915,78 @@ export class Attributes { } if ((await this.getConfirmation()) !== true) { - return { attributes: changedAttributes, hasChanges: false }; + return { attributes: changedAttributes, hasChanges: false, renames: [] }; + } + } + + // Apply renames before field updates / deletions so a failed rename + // never leaves data half-destroyed. + if (renames.length > 0) { + const renameResults = await Promise.allSettled( + renames.map((rename) => + this.updateAttribute( + collection["databaseId"], + collection["$id"], + rename.attribute, + rename.to, + ), + ), + ); + + const renameFailures = renameResults + .map((result, index) => + result.status === "rejected" + ? this.formatUpdateError( + { key: renames[index].to }, + result.reason, + ) + : null, + ) + .filter((message): message is string => message !== null); + + if (renameFailures.length > 0) { + throw new Error( + `Error renaming attribute for ${collection["$id"]}:\n${renameFailures.join("\n")}`, + ); + } + + const renameReady = await this.pools.expectAttributes( + collection["databaseId"], + collection["$id"], + renames.map((rename) => rename.to), + ); + + if (!renameReady) { + throw new Error( + `Attribute rename timed out waiting for keys: ${renames.map((r) => r.to).join(", ")}`, + ); + } + } + + // Apply in-place updates first so failures abort before any deletions. + if (changes.length > 0) { + const updateResults = await Promise.allSettled( + changes.map((change) => + this.updateAttribute( + collection["databaseId"], + collection["$id"], + change.attribute, + ), + ), + ); + + const failures = updateResults + .map((result, index) => + result.status === "rejected" + ? this.formatUpdateError(changes[index].attribute, result.reason) + : null, + ) + .filter((message): message is string => message !== null); + + if (failures.length > 0) { + throw new Error( + `Error updating ${isIndex ? "index" : "attribute"} for ${collection["$id"]}:\n${failures.join("\n")}`, + ); } } @@ -706,45 +1002,33 @@ export class Attributes { ); } - if (changes.length > 0) { - changedAttributes = changes.map((change) => change.attribute); - try { - await Promise.all( - changedAttributes.map((changed) => - this.updateAttribute( - collection["databaseId"], - collection["$id"], - changed, - ), - ), - ); - } catch (err) { - error( - `Error updating attribute for ${collection["$id"]}: ${String(err)}`, - ); - } - } - const deletingAttributes = deleting.map((change) => change.attribute); await Promise.all( deletingAttributes.map((attribute) => this.deleteAttribute(collection, attribute, isIndex), ), ); - const attributeKeys = deletingAttributes.map( - (attribute: any) => attribute.key, - ); - if (attributeKeys.length) { - const deleteAttributesPoolStatus = - await this.pools.waitForAttributeDeletion( - collection["databaseId"], - collection["$id"], - attributeKeys, - ); + // Wait for both removals and recreate-driven deletes before creating. + const deletedKeys = [ + ...deletingAttributes, + ...conflicts.map((change) => change.attribute), + ].map((attribute: any) => attribute.key); - if (!deleteAttributesPoolStatus) { - throw new Error("Attribute deletion timed out."); + if (deletedKeys.length) { + const waitForDeletion = isIndex + ? this.pools.waitForIndexDeletion + : this.pools.waitForAttributeDeletion; + const deletePoolStatus = await waitForDeletion( + collection["databaseId"], + collection["$id"], + deletedKeys, + ); + + if (!deletePoolStatus) { + throw new Error( + `${isIndex ? "Index" : "Attribute"} deletion timed out.`, + ); } } @@ -752,7 +1036,7 @@ export class Attributes { (attribute) => !this.attributesContains(attribute, filteredRemoteAttributes), ); - return { attributes: newAttributes, hasChanges: true }; + return { attributes: newAttributes, hasChanges: true, renames }; }; public createIndexes = async ( diff --git a/lib/commands/utils/pools.ts b/lib/commands/utils/pools.ts index d2a9310..db27dd3 100644 --- a/lib/commands/utils/pools.ts +++ b/lib/commands/utils/pools.ts @@ -98,7 +98,7 @@ export class Pools { public waitForAttributeDeletion = async ( databaseId: string, collectionId: string, - attributeKeys: any[], + attributeKeys: string[], iteration: number = 1, ): Promise => { if (iteration > this.pollMaxDebounces) { @@ -138,11 +138,11 @@ export class Pools { "attributes", ); - const ready = attributeKeys.filter((attribute: any) => - attributes.some((a: any) => a.key === attribute.key), + const remaining = attributeKeys.filter((key) => + attributes.some((a: any) => a.key === key), ); - if (ready.length === 0) { + if (remaining.length === 0) { return true; } @@ -156,6 +156,64 @@ export class Pools { ); }; + public waitForIndexDeletion = async ( + databaseId: string, + collectionId: string, + indexKeys: string[], + iteration: number = 1, + ): Promise => { + if (iteration > this.pollMaxDebounces) { + return false; + } + + if (this.pollMaxDebounces === this.POLL_DEFAULT_VALUE) { + const steps = Math.max(1, Math.ceil(indexKeys.length / this.STEP_SIZE)); + if (steps > 1 && iteration === 1) { + this.pollMaxDebounces *= steps; + + log( + "Found a large number of indexes to be deleted. Increasing timeout to " + + (this.pollMaxDebounces * this.POLL_DEBOUNCE) / 1000 / 60 + + " minutes", + ); + } + } + + const { indexes } = await paginate( + async (args: any) => { + const databasesService = await getDatabasesService(this.client); + return await databasesService.listIndexes( + args.databaseId, + args.collectionId, + args.queries || [], + ); + }, + { + databaseId, + collectionId, + }, + 100, + "indexes", + ); + + const remaining = indexKeys.filter((key) => + indexes.some((index: any) => index.key === key), + ); + + if (remaining.length === 0) { + return true; + } + + await new Promise((resolve) => setTimeout(resolve, this.POLL_DEBOUNCE)); + + return await this.waitForIndexDeletion( + databaseId, + collectionId, + indexKeys, + iteration + 1, + ); + }; + public expectAttributes = async ( databaseId: string, collectionId: string, diff --git a/lib/config.ts b/lib/config.ts index 13dc807..07afc58 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -118,6 +118,18 @@ export function normalizeCloudConsoleEndpoint(endpoint: string): string { return endpoint; } +/** + * Compare two endpoints for session matching: normalize cloud regional hosts + * and ignore trailing slashes so `https://fra.cloud.appwrite.io/v1` matches + * `https://cloud.appwrite.io/v1`. + */ +export function endpointsMatch(a: string, b: string): boolean { + return ( + normalizeCloudConsoleEndpoint(a).replace(/\/+$/, "") === + normalizeCloudConsoleEndpoint(b).replace(/\/+$/, "") + ); +} + function ensureDirectoryForFile(filePath: string): void { const dir = _path.dirname(filePath); if (!fs.existsSync(dir)) { diff --git a/lib/constants.ts b/lib/constants.ts index 76dbc76..47c9d4d 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 = '24.0.0'; +export const SDK_VERSION = '24.1.0'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; diff --git a/lib/parser.ts b/lib/parser.ts index 5efc0a0..03227a9 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -6,6 +6,7 @@ BigInt.prototype.toJSON = function () { import chalk from "chalk"; import { InvalidArgumentError } from "commander"; import Table from "cli-table3"; +import stringWidth from "string-width"; import packageJson from "../package.json" with { type: "json" }; const { description } = packageJson; import { globalConfig } from "./config.js"; @@ -367,6 +368,33 @@ export const parse = (data: unknown): void => { const MAX_COL_WIDTH = 40; const MAX_COLUMNS = 6; +/** Visible-width cap for table cells; scales with terminal size. */ +const getMaxColWidth = (): number => { + const columns = process.stdout.columns || 120; + // Leave room for Key + Action + borders; give Reason most of the rest. + return Math.max(MAX_COL_WIDTH, Math.min(120, Math.floor(columns * 0.5))); +}; + +/** + * Truncate by visible width so chalk ANSI codes do not eat the budget and + * cut off readable text while the terminal still has space. + */ +const truncateToVisibleWidth = (str: string, max: number): string => { + if (stringWidth(str) <= max) { + return str; + } + + let result = ""; + for (const char of str) { + const next = result + char; + if (stringWidth(next) > max - 1) { + break; + } + result = next; + } + return `${result}…`; +}; + type NamedTableOptions = { indent?: string; sectionName?: string; @@ -397,7 +425,7 @@ const formatCellValue = (value: unknown): string => { ) ) { const joinedValue = value.map((item) => String(item)).join(", "); - if (joinedValue.length <= MAX_COL_WIDTH) { + if (stringWidth(joinedValue) <= getMaxColWidth()) { return joinedValue; } } @@ -409,11 +437,7 @@ const formatCellValue = (value: unknown): string => { if (keys.length === 0) return "{}"; return `{${keys.length} keys}`; } - const str = String(value); - if (str.length > MAX_COL_WIDTH) { - return str.slice(0, MAX_COL_WIDTH - 1) + "…"; - } - return str; + return truncateToVisibleWidth(String(value), getMaxColWidth()); }; const formatKeyValue = (key: string, value: unknown): string => { @@ -614,7 +638,8 @@ export const drawTable = ( const table = new Table({ head: columns.map((c) => chalk.cyan.italic.bold(c)), colWidths: columns.map(() => null) as (number | null)[], - wordWrap: false, + wordWrap: true, + wrapOnWordBoundary: true, chars: { top: " ", "top-mid": " ", diff --git a/lib/questions.ts b/lib/questions.ts index a6d8b5f..fcd8888 100644 --- a/lib/questions.ts +++ b/lib/questions.ts @@ -938,6 +938,23 @@ export const questionsLogout: Question[] = [ }, ]; +export const questionsClientReset = ( + accounts: Array<{ email: string; endpoint: string }>, +): Question[] => [ + { + type: "confirm", + name: "confirm", + message: `This will sign out ${accounts + .map((account) => + account.endpoint + ? `${account.email} (${account.endpoint})` + : account.email, + ) + .join(", ")}. Continue?`, + default: false, + }, +]; + export const questionsPushResources: Question[] = [ { type: "list", diff --git a/lib/sdks.ts b/lib/sdks.ts index d70c803..79c7a3d 100644 --- a/lib/sdks.ts +++ b/lib/sdks.ts @@ -1,4 +1,5 @@ import { + endpointsMatch, globalConfig, localConfig, normalizeCloudConsoleEndpoint, @@ -21,10 +22,7 @@ import { export const assertSessionEndpointMatches = (endpoint: string): void => { const sessionEndpoint = globalConfig.getEndpoint() || DEFAULT_ENDPOINT; - if ( - normalizeCloudConsoleEndpoint(endpoint).replace(/\/+$/, "") !== - normalizeCloudConsoleEndpoint(sessionEndpoint).replace(/\/+$/, "") - ) { + if (!endpointsMatch(endpoint, sessionEndpoint)) { throw new Error( `Endpoint ${endpoint} does not match the current login session endpoint ${sessionEndpoint}. Switch to an account for this environment with \`${EXECUTABLE_NAME} login --switch\`.`, ); diff --git a/package-lock.json b/package-lock.json index b8c0934..60e887d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "appwrite-cli", - "version": "24.0.0", + "version": "24.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "24.0.0", + "version": "24.1.0", "license": "BSD-3-Clause", "dependencies": { - "@appwrite.io/console": "15.7.0", + "@appwrite.io/console": "15.8.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", @@ -50,19 +50,12 @@ "tsx": "^4.21.0", "typescript": "^5.3.3", "typescript-eslint": "^8.0.0" - }, - "overrides": { - "phin": "3.7.1", - "@xmldom/xmldom": "^0.9.10", - "tmp": "^0.2.6", - "esbuild": "^0.28.1", - "brace-expansion": "5.0.8" } }, "node_modules/@appwrite.io/console": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.7.0.tgz", - "integrity": "sha512-43FVjVbqdedkoiMrAKCFpEnAhBb7YruW9fHqUJ/uvzMT2yg8g8cx+s5b0gkmiC276URALa5BzkYUBoTLw/Dnsw==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-15.8.0.tgz", + "integrity": "sha512-OJvWFBm7x2Si49UzzRfTy9tq5wT7M4AWJ7+ii/xJYfEpiw87wgaCZSDln8LAmPNGqRZNtvyXCalf8K1aaD+0ig==", "license": "BSD-3-Clause", "dependencies": { "json-bigint": "1.0.0" @@ -2395,9 +2388,9 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 4ff852c..18410c3 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": "24.0.0", + "version": "24.1.0", "license": "BSD-3-Clause", "main": "dist/index.cjs", "module": "dist/index.js", @@ -51,7 +51,7 @@ "windows-arm64": "bun build cli.ts --compile --minify --target=bun-windows-arm64 --outfile build/appwrite-cli-win-arm64.exe" }, "dependencies": { - "@appwrite.io/console": "15.7.0", + "@appwrite.io/console": "15.8.0", "@napi-rs/keyring": "^1.3.0", "chalk": "4.1.2", "chokidar": "^3.6.0", diff --git a/scoop/appwrite.config.json b/scoop/appwrite.config.json index f955d79..0ea2889 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": "24.0.0", + "version": "24.1.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/24.0.0/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/24.1.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/24.0.0/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/24.1.0/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe",