From 4959927e16366910c99866b169a3661fb8935b47 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 28 Jul 2026 22:31:45 +0530 Subject: [PATCH 1/3] 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/3] 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", From ec40cd70800fc6aa0f604d407e2e1bf615dd4db6 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 31 Jul 2026 13:52:26 +0530 Subject: [PATCH 3/3] chore: update Command Line SDK to 25.0.0 --- CHANGELOG.md | 19 + README.md | 4 +- bun.lock | 2 +- cli.ts | 39 +- docs/examples/organizations/add-credit.md | 5 - .../organizations/cancel-downgrade.md | 4 - .../organizations/confirm-addon-payment.md | 5 - .../organizations/create-baa-addon.md | 4 - .../create-downgrade-feedback.md | 8 - .../organizations/create-invoice-payment.md | 6 - .../create-premium-geo-db-addon.md | 4 - docs/examples/organizations/create.md | 6 - docs/examples/organizations/delete-addon.md | 5 - .../delete-backup-payment-method.md | 4 - .../delete-default-payment-method.md | 4 - docs/examples/organizations/delete.md | 4 - .../estimation-create-organization.md | 4 - .../estimation-delete-organization.md | 4 - .../organizations/estimation-update-plan.md | 5 - .../examples/organizations/get-addon-price.md | 5 - docs/examples/organizations/get-addon.md | 5 - .../examples/organizations/get-aggregation.md | 5 - .../organizations/get-available-credits.md | 4 - docs/examples/organizations/get-credit.md | 5 - .../organizations/get-invoice-download.md | 5 - .../organizations/get-invoice-view.md | 5 - docs/examples/organizations/get-invoice.md | 5 - docs/examples/organizations/get-plan.md | 4 - docs/examples/organizations/get-scopes.md | 4 - docs/examples/organizations/get-usage.md | 4 - docs/examples/organizations/list-addons.md | 4 - .../organizations/list-aggregations.md | 5 - docs/examples/organizations/list-credits.md | 5 - docs/examples/organizations/list-regions.md | 4 - docs/examples/organizations/list.md | 4 - .../set-backup-payment-method.md | 5 - .../organizations/set-billing-address.md | 5 - .../organizations/set-billing-email.md | 5 - .../organizations/set-billing-tax-id.md | 5 - .../set-default-payment-method.md | 5 - docs/examples/organizations/update-budget.md | 5 - docs/examples/organizations/update-plan.md | 5 - .../organizations/validate-invoice.md | 5 - .../organizations/validate-payment.md | 4 - install.ps1 | 4 +- install.sh | 2 +- lib/auth/login.ts | 11 +- lib/commands/generic.ts | 9 +- lib/commands/init.ts | 14 +- lib/commands/pull.ts | 24 +- lib/commands/push.ts | 37 +- lib/commands/services/notifications.ts | 4 +- lib/commands/services/oauth2.ts | 16 +- lib/commands/services/organization.ts | 130 ++-- lib/commands/services/organizations.ts | 624 ------------------ lib/commands/services/project.ts | 528 +++++++++------ lib/commands/services/teams.ts | 2 +- lib/config-filters.ts | 79 --- lib/constants.ts | 2 +- lib/context.ts | 109 +++ lib/help.ts | 287 ++++++++ lib/hints.ts | 27 + lib/parser.ts | 122 +++- lib/questions.ts | 15 +- lib/response-config.ts | 315 ++++++++- lib/sdks.ts | 27 +- lib/services.ts | 4 +- lib/types.ts | 1 + lib/utils.ts | 15 + package-lock.json | 32 +- package.json | 2 +- scoop/appwrite.config.json | 6 +- 72 files changed, 1432 insertions(+), 1269 deletions(-) delete mode 100644 docs/examples/organizations/add-credit.md delete mode 100644 docs/examples/organizations/cancel-downgrade.md delete mode 100644 docs/examples/organizations/confirm-addon-payment.md delete mode 100644 docs/examples/organizations/create-baa-addon.md delete mode 100644 docs/examples/organizations/create-downgrade-feedback.md delete mode 100644 docs/examples/organizations/create-invoice-payment.md delete mode 100644 docs/examples/organizations/create-premium-geo-db-addon.md delete mode 100644 docs/examples/organizations/create.md delete mode 100644 docs/examples/organizations/delete-addon.md delete mode 100644 docs/examples/organizations/delete-backup-payment-method.md delete mode 100644 docs/examples/organizations/delete-default-payment-method.md delete mode 100644 docs/examples/organizations/delete.md delete mode 100644 docs/examples/organizations/estimation-create-organization.md delete mode 100644 docs/examples/organizations/estimation-delete-organization.md delete mode 100644 docs/examples/organizations/estimation-update-plan.md delete mode 100644 docs/examples/organizations/get-addon-price.md delete mode 100644 docs/examples/organizations/get-addon.md delete mode 100644 docs/examples/organizations/get-aggregation.md delete mode 100644 docs/examples/organizations/get-available-credits.md delete mode 100644 docs/examples/organizations/get-credit.md delete mode 100644 docs/examples/organizations/get-invoice-download.md delete mode 100644 docs/examples/organizations/get-invoice-view.md delete mode 100644 docs/examples/organizations/get-invoice.md delete mode 100644 docs/examples/organizations/get-plan.md delete mode 100644 docs/examples/organizations/get-scopes.md delete mode 100644 docs/examples/organizations/get-usage.md delete mode 100644 docs/examples/organizations/list-addons.md delete mode 100644 docs/examples/organizations/list-aggregations.md delete mode 100644 docs/examples/organizations/list-credits.md delete mode 100644 docs/examples/organizations/list-regions.md delete mode 100644 docs/examples/organizations/list.md delete mode 100644 docs/examples/organizations/set-backup-payment-method.md delete mode 100644 docs/examples/organizations/set-billing-address.md delete mode 100644 docs/examples/organizations/set-billing-email.md delete mode 100644 docs/examples/organizations/set-billing-tax-id.md delete mode 100644 docs/examples/organizations/set-default-payment-method.md delete mode 100644 docs/examples/organizations/update-budget.md delete mode 100644 docs/examples/organizations/update-plan.md delete mode 100644 docs/examples/organizations/validate-invoice.md delete mode 100644 docs/examples/organizations/validate-payment.md delete mode 100644 lib/commands/services/organizations.ts delete mode 100644 lib/config-filters.ts create mode 100644 lib/context.ts create mode 100644 lib/help.ts create mode 100644 lib/hints.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 89abbfd..408c34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Change Log +## 25.0.0 + +* Breaking: Removed the `organizations` command group covering billing, plans, invoices, and add-ons +* Added: `organization` commands accept `--organization-id` to target another organization +* Added: `project` commands accept `--project-id` to target another project +* Added: Grouped `--help` screen with follow-up command hints after each action +* Added: `push` and `pull` document `--all` and `--id` in their own help +* Added: `client` reports the configured `organizationId` +* Fixed: `login` warns and signs in to the base Cloud endpoint instead of failing on a regional one +* Fixed: `init` no longer stacks a region prefix onto an already regional endpoint +* Fixed: `notifications` and `oauth2 list-organizations`, `list-projects` call the console, not the linked project +* Fixed: Passwords are masked in full instead of revealing a tail +* Updated: Timestamps, durations, sizes, and large counts render in human-readable form +* Updated: Enabled and disabled lists collapse into wrapped groups instead of wide tables +* Updated: Condensed output notes how many fields `--raw` would add +* Updated: Prompts and errors listing several accounts print one account per line +* Updated: `teams` hints and `register` visibility point at `organization` and `login` +* Updated: Dropped the `deploy` placeholder command, which only printed a "use `push`" warning + ## 24.1.0 * Added: `previousKey` on an attribute or column renames it in place on `push` diff --git a/README.md b/README.md index 73949f1..9c8d12b 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.1.0 +25.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 -24.1.0 +25.0.0 ``` ## Getting Started diff --git a/bun.lock b/bun.lock index f2d1bac..870eecc 100644 --- a/bun.lock +++ b/bun.lock @@ -450,7 +450,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.4.3", "", {}, "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], diff --git a/cli.ts b/cli.ts index 43bdd7b..e8c0c90 100644 --- a/cli.ts +++ b/cli.ts @@ -5,12 +5,14 @@ const oldWidth = process.stdout.columns; process.stdout.columns = 100; /** ---------------------------------------------- */ -import { program } from 'commander'; +import { program, Option } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import packageJson from './package.json' with { type: 'json' }; import { commandDescriptions, cliConfig } from './lib/parser.js'; +import { followUpHintFor } from './lib/hints.js'; +import { formatMainHelp } from './lib/help.js'; import { getLatestVersionForCurrentInstallation, compareVersions, @@ -30,7 +32,7 @@ import { init } from './lib/commands/init.js'; import { types } from './lib/commands/types.js'; import { pull } from './lib/commands/pull.js'; import { run } from './lib/commands/run.js'; -import { push, deploy } from './lib/commands/push.js'; +import { push } from './lib/commands/push.js'; import { update } from './lib/commands/update.js'; import { generate } from './lib/commands/generate.js'; @@ -47,7 +49,6 @@ import { migrations } from './lib/commands/services/migrations.js'; import { notifications } from './lib/commands/services/notifications.js'; import { oauth2 } from './lib/commands/services/oauth2.js'; import { organization } from './lib/commands/services/organization.js'; -import { organizations } from './lib/commands/services/organizations.js'; import { presences } from './lib/commands/services/presences.js'; import { project } from './lib/commands/services/project.js'; import { proxy } from './lib/commands/services/proxy.js'; @@ -142,14 +143,18 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { .description(commandDescriptions['main']) .configureHelp({ helpWidth: process.stdout.columns || 80, + // The grouped screen sets its own order, but `sortSubcommands` + // also drives Help.visibleCommands, which the completion + // scripts enumerate — keep it so their output stays stable. sortSubcommands: true, + formatHelp: formatMainHelp, }) - .helpOption('-h, --help', 'Display help for command') - .version(version, '-v, --version', 'Output the version number') - .option('-V, --verbose', 'Show complete error log') - .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') + .helpOption('-h, --help', 'Display help for a command') + .version(version, '-v, --version', 'Output the CLI version') + .option('-V, --verbose', 'Show full error stack traces') + .option('-j, --json', 'Output filtered JSON (empty values omitted)') + .option('-R, --raw', 'Output the full raw JSON response') + .option('--show-secrets', 'Reveal secrets and tokens in output (redacted by default)') .hook('preAction', async (_thisCommand, actionCommand) => { if (isCompletionCommand(actionCommand)) { return; @@ -157,10 +162,12 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { 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') - .option('--report', 'Enable reporting in case of CLI errors') + .option('-f, --force', 'Skip confirmation prompts') + // Parsed at the root so `appwrite --all push` keeps working, but + // documented on `push` and `pull`, which are what they act on. + .addOption(new Option('-a, --all', 'Push or pull every resource').hideHelp()) + .addOption(new Option('--id [id...]', 'Limit the action to these resource ids').hideHelp()) + .option('--report', 'Print a prefilled bug report link on error') .hook('preAction', (_thisCommand, actionCommand) => { const commandConfig = actionCommand as typeof actionCommand & { outputFields?: string[]; @@ -168,6 +175,7 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { cliConfig.displayFields = Array.isArray(commandConfig.outputFields) ? commandConfig.outputFields : []; + cliConfig.followUpHint = followUpHintFor(actionCommand); }) .on('option:json', () => { cliConfig.json = true; @@ -196,13 +204,13 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { }) .showSuggestionAfterError() .addCommand(whoami) - .addCommand(register) + // `login` is the entry point; `register` only prints a signup link. + .addCommand(register, { hidden: true }) .addCommand(login) .addCommand(init) .addCommand(pull) .addCommand(push) .addCommand(types) - .addCommand(deploy) .addCommand(run) .addCommand(update) .addCommand(generate) @@ -220,7 +228,6 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) { .addCommand(notifications) .addCommand(oauth2) .addCommand(organization) - .addCommand(organizations) .addCommand(presences) .addCommand(project) .addCommand(proxy) diff --git a/docs/examples/organizations/add-credit.md b/docs/examples/organizations/add-credit.md deleted file mode 100644 index 46d3ce4..0000000 --- a/docs/examples/organizations/add-credit.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations add-credit \ - --organization-id \ - --coupon-id -``` diff --git a/docs/examples/organizations/cancel-downgrade.md b/docs/examples/organizations/cancel-downgrade.md deleted file mode 100644 index 342c96b..0000000 --- a/docs/examples/organizations/cancel-downgrade.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations cancel-downgrade \ - --organization-id -``` diff --git a/docs/examples/organizations/confirm-addon-payment.md b/docs/examples/organizations/confirm-addon-payment.md deleted file mode 100644 index b394355..0000000 --- a/docs/examples/organizations/confirm-addon-payment.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations confirm-addon-payment \ - --organization-id \ - --addon-id -``` diff --git a/docs/examples/organizations/create-baa-addon.md b/docs/examples/organizations/create-baa-addon.md deleted file mode 100644 index 6a78a60..0000000 --- a/docs/examples/organizations/create-baa-addon.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations create-baa-addon \ - --organization-id -``` diff --git a/docs/examples/organizations/create-downgrade-feedback.md b/docs/examples/organizations/create-downgrade-feedback.md deleted file mode 100644 index 1e97154..0000000 --- a/docs/examples/organizations/create-downgrade-feedback.md +++ /dev/null @@ -1,8 +0,0 @@ -```bash -appwrite organizations create-downgrade-feedback \ - --organization-id \ - --reason \ - --message \ - --from-plan-id \ - --to-plan-id -``` diff --git a/docs/examples/organizations/create-invoice-payment.md b/docs/examples/organizations/create-invoice-payment.md deleted file mode 100644 index fcadc72..0000000 --- a/docs/examples/organizations/create-invoice-payment.md +++ /dev/null @@ -1,6 +0,0 @@ -```bash -appwrite organizations create-invoice-payment \ - --organization-id \ - --invoice-id \ - --payment-method-id -``` diff --git a/docs/examples/organizations/create-premium-geo-db-addon.md b/docs/examples/organizations/create-premium-geo-db-addon.md deleted file mode 100644 index ce72c9a..0000000 --- a/docs/examples/organizations/create-premium-geo-db-addon.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations create-premium-geo-db-addon \ - --organization-id -``` diff --git a/docs/examples/organizations/create.md b/docs/examples/organizations/create.md deleted file mode 100644 index edcd969..0000000 --- a/docs/examples/organizations/create.md +++ /dev/null @@ -1,6 +0,0 @@ -```bash -appwrite organizations create \ - --organization-id \ - --name \ - --billing-plan tier-0 -``` diff --git a/docs/examples/organizations/delete-addon.md b/docs/examples/organizations/delete-addon.md deleted file mode 100644 index 789484b..0000000 --- a/docs/examples/organizations/delete-addon.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations delete-addon \ - --organization-id \ - --addon-id -``` diff --git a/docs/examples/organizations/delete-backup-payment-method.md b/docs/examples/organizations/delete-backup-payment-method.md deleted file mode 100644 index d2d1ae9..0000000 --- a/docs/examples/organizations/delete-backup-payment-method.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations delete-backup-payment-method \ - --organization-id -``` diff --git a/docs/examples/organizations/delete-default-payment-method.md b/docs/examples/organizations/delete-default-payment-method.md deleted file mode 100644 index d2f85aa..0000000 --- a/docs/examples/organizations/delete-default-payment-method.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations delete-default-payment-method \ - --organization-id -``` diff --git a/docs/examples/organizations/delete.md b/docs/examples/organizations/delete.md deleted file mode 100644 index af94485..0000000 --- a/docs/examples/organizations/delete.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations delete \ - --organization-id -``` diff --git a/docs/examples/organizations/estimation-create-organization.md b/docs/examples/organizations/estimation-create-organization.md deleted file mode 100644 index a9d6a62..0000000 --- a/docs/examples/organizations/estimation-create-organization.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations estimation-create-organization \ - --billing-plan tier-0 -``` diff --git a/docs/examples/organizations/estimation-delete-organization.md b/docs/examples/organizations/estimation-delete-organization.md deleted file mode 100644 index 4a966b5..0000000 --- a/docs/examples/organizations/estimation-delete-organization.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations estimation-delete-organization \ - --organization-id -``` diff --git a/docs/examples/organizations/estimation-update-plan.md b/docs/examples/organizations/estimation-update-plan.md deleted file mode 100644 index d651ec3..0000000 --- a/docs/examples/organizations/estimation-update-plan.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations estimation-update-plan \ - --organization-id \ - --billing-plan tier-0 -``` diff --git a/docs/examples/organizations/get-addon-price.md b/docs/examples/organizations/get-addon-price.md deleted file mode 100644 index b410542..0000000 --- a/docs/examples/organizations/get-addon-price.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-addon-price \ - --organization-id \ - --addon baa -``` diff --git a/docs/examples/organizations/get-addon.md b/docs/examples/organizations/get-addon.md deleted file mode 100644 index 54a1835..0000000 --- a/docs/examples/organizations/get-addon.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-addon \ - --organization-id \ - --addon-id -``` diff --git a/docs/examples/organizations/get-aggregation.md b/docs/examples/organizations/get-aggregation.md deleted file mode 100644 index 5202b75..0000000 --- a/docs/examples/organizations/get-aggregation.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-aggregation \ - --organization-id \ - --aggregation-id -``` diff --git a/docs/examples/organizations/get-available-credits.md b/docs/examples/organizations/get-available-credits.md deleted file mode 100644 index 928013f..0000000 --- a/docs/examples/organizations/get-available-credits.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations get-available-credits \ - --organization-id -``` diff --git a/docs/examples/organizations/get-credit.md b/docs/examples/organizations/get-credit.md deleted file mode 100644 index 7970176..0000000 --- a/docs/examples/organizations/get-credit.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-credit \ - --organization-id \ - --credit-id -``` diff --git a/docs/examples/organizations/get-invoice-download.md b/docs/examples/organizations/get-invoice-download.md deleted file mode 100644 index 678bd6d..0000000 --- a/docs/examples/organizations/get-invoice-download.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-invoice-download \ - --organization-id \ - --invoice-id -``` diff --git a/docs/examples/organizations/get-invoice-view.md b/docs/examples/organizations/get-invoice-view.md deleted file mode 100644 index fbd963c..0000000 --- a/docs/examples/organizations/get-invoice-view.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-invoice-view \ - --organization-id \ - --invoice-id -``` diff --git a/docs/examples/organizations/get-invoice.md b/docs/examples/organizations/get-invoice.md deleted file mode 100644 index c86f41a..0000000 --- a/docs/examples/organizations/get-invoice.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations get-invoice \ - --organization-id \ - --invoice-id -``` diff --git a/docs/examples/organizations/get-plan.md b/docs/examples/organizations/get-plan.md deleted file mode 100644 index 47963ee..0000000 --- a/docs/examples/organizations/get-plan.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations get-plan \ - --organization-id -``` diff --git a/docs/examples/organizations/get-scopes.md b/docs/examples/organizations/get-scopes.md deleted file mode 100644 index 81fa57f..0000000 --- a/docs/examples/organizations/get-scopes.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations get-scopes \ - --organization-id -``` diff --git a/docs/examples/organizations/get-usage.md b/docs/examples/organizations/get-usage.md deleted file mode 100644 index f67386d..0000000 --- a/docs/examples/organizations/get-usage.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations get-usage \ - --organization-id -``` diff --git a/docs/examples/organizations/list-addons.md b/docs/examples/organizations/list-addons.md deleted file mode 100644 index a7eeccb..0000000 --- a/docs/examples/organizations/list-addons.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations list-addons \ - --organization-id -``` diff --git a/docs/examples/organizations/list-aggregations.md b/docs/examples/organizations/list-aggregations.md deleted file mode 100644 index 200d8e4..0000000 --- a/docs/examples/organizations/list-aggregations.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations list-aggregations \ - --organization-id \ - --limit 25 -``` diff --git a/docs/examples/organizations/list-credits.md b/docs/examples/organizations/list-credits.md deleted file mode 100644 index 5d8f9d1..0000000 --- a/docs/examples/organizations/list-credits.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations list-credits \ - --organization-id \ - --limit 25 -``` diff --git a/docs/examples/organizations/list-regions.md b/docs/examples/organizations/list-regions.md deleted file mode 100644 index c7a95a8..0000000 --- a/docs/examples/organizations/list-regions.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations list-regions \ - --organization-id -``` diff --git a/docs/examples/organizations/list.md b/docs/examples/organizations/list.md deleted file mode 100644 index ca0c5ea..0000000 --- a/docs/examples/organizations/list.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations list \ - --limit 25 -``` diff --git a/docs/examples/organizations/set-backup-payment-method.md b/docs/examples/organizations/set-backup-payment-method.md deleted file mode 100644 index 759c254..0000000 --- a/docs/examples/organizations/set-backup-payment-method.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations set-backup-payment-method \ - --organization-id \ - --payment-method-id -``` diff --git a/docs/examples/organizations/set-billing-address.md b/docs/examples/organizations/set-billing-address.md deleted file mode 100644 index 0aa2ae3..0000000 --- a/docs/examples/organizations/set-billing-address.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations set-billing-address \ - --organization-id \ - --billing-address-id -``` diff --git a/docs/examples/organizations/set-billing-email.md b/docs/examples/organizations/set-billing-email.md deleted file mode 100644 index 793d355..0000000 --- a/docs/examples/organizations/set-billing-email.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations set-billing-email \ - --organization-id \ - --billing-email email@example.com -``` diff --git a/docs/examples/organizations/set-billing-tax-id.md b/docs/examples/organizations/set-billing-tax-id.md deleted file mode 100644 index d8c54fd..0000000 --- a/docs/examples/organizations/set-billing-tax-id.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations set-billing-tax-id \ - --organization-id \ - --tax-id -``` diff --git a/docs/examples/organizations/set-default-payment-method.md b/docs/examples/organizations/set-default-payment-method.md deleted file mode 100644 index 2aa2918..0000000 --- a/docs/examples/organizations/set-default-payment-method.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations set-default-payment-method \ - --organization-id \ - --payment-method-id -``` diff --git a/docs/examples/organizations/update-budget.md b/docs/examples/organizations/update-budget.md deleted file mode 100644 index 3822d59..0000000 --- a/docs/examples/organizations/update-budget.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations update-budget \ - --organization-id \ - --budget 0 -``` diff --git a/docs/examples/organizations/update-plan.md b/docs/examples/organizations/update-plan.md deleted file mode 100644 index 9315208..0000000 --- a/docs/examples/organizations/update-plan.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations update-plan \ - --organization-id \ - --billing-plan tier-0 -``` diff --git a/docs/examples/organizations/validate-invoice.md b/docs/examples/organizations/validate-invoice.md deleted file mode 100644 index de57984..0000000 --- a/docs/examples/organizations/validate-invoice.md +++ /dev/null @@ -1,5 +0,0 @@ -```bash -appwrite organizations validate-invoice \ - --organization-id \ - --invoice-id -``` diff --git a/docs/examples/organizations/validate-payment.md b/docs/examples/organizations/validate-payment.md deleted file mode 100644 index 623743e..0000000 --- a/docs/examples/organizations/validate-payment.md +++ /dev/null @@ -1,4 +0,0 @@ -```bash -appwrite organizations validate-payment \ - --organization-id -``` diff --git a/install.ps1 b/install.ps1 index 291334b..01d4586 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.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" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/25.0.0/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/25.0.0/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index 8c48296..feaf059 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.1.0" + GITHUB_LATEST_VERSION="25.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 de562ab..c70662d 100644 --- a/lib/auth/login.ts +++ b/lib/auth/login.ts @@ -466,15 +466,16 @@ export const loginCommand = async ({ throw new Error("Use either --switch or --new, not both."); } + const configEndpoint = normalizeCloudConsoleEndpoint( + (endpoint ?? globalConfig.getEndpoint()) || DEFAULT_ENDPOINT, + ); + if (endpoint && isRegionalCloudEndpoint(endpoint)) { - throw new Error( - `Cloud login uses ${DEFAULT_ENDPOINT}. Regional Cloud endpoints are for project API calls, not account login.`, + warn( + `Regional Cloud endpoints are for project API calls, so signing in to ${configEndpoint} instead. Set the regional endpoint in ${EXECUTABLE_NAME}.config.json.`, ); } - const configEndpoint = normalizeCloudConsoleEndpoint( - (endpoint ?? globalConfig.getEndpoint()) || DEFAULT_ENDPOINT, - ); const shouldUseCloudLogin = isCloudLoginEndpoint(configEndpoint); if (shouldUseCloudLogin && (email || password || mfa || code)) { diff --git a/lib/commands/generic.ts b/lib/commands/generic.ts index 3d10fcf..a14d32f 100644 --- a/lib/commands/generic.ts +++ b/lib/commands/generic.ts @@ -2,6 +2,7 @@ import inquirer from "inquirer"; import { Command } from "commander"; import { Client } from "@appwrite.io/console"; import { endpointsMatch, globalConfig, localConfig } from "../config.js"; +import { configuredOrganizationId } from "../context.js"; import { EXECUTABLE_NAME } from "../constants.js"; import { actionRunner, @@ -16,6 +17,7 @@ import { cliConfig, } from "../parser.js"; import ID from "../id.js"; +import { formatAccountList } from "../utils.js"; import { questionsClientReset, questionsLogout } from "../questions.js"; import { getCurrentAccount, loginCommand } from "../auth/login.js"; import { @@ -278,6 +280,7 @@ export const client = new Command("client") key: maskedKey, accessToken: maskedAccessToken, selfSigned: globalConfig.getSelfSigned(), + organizationId: configuredOrganizationId(), projectId: project.projectId ?? "", projectName: project.projectName ?? "", }; @@ -371,11 +374,13 @@ export const client = new Command("client") 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.`, + `Resetting will sign out:\n${formatAccountList(accounts)}\nRe-run with --force to confirm.`, ); } - const answers = await inquirer.prompt(questionsClientReset(accounts)); + const answers = await inquirer.prompt( + questionsClientReset(accounts), + ); if (!answers.confirm) { log("Reset cancelled."); return; diff --git a/lib/commands/init.ts b/lib/commands/init.ts index b6bd00e..cf260ff 100644 --- a/lib/commands/init.ts +++ b/lib/commands/init.ts @@ -7,7 +7,11 @@ import chalk from "chalk"; import { getOrganizationService, getSitesService } from "../services.js"; import { pullResources } from "./pull.js"; import ID from "../id.js"; -import { localConfig, globalConfig } from "../config.js"; +import { + localConfig, + globalConfig, + normalizeCloudConsoleEndpoint, +} from "../config.js"; import { questionsCreateFunction, questionsCreateFunctionSelectTemplate, @@ -124,7 +128,13 @@ const getExistingProjectSummary = async ( }; const getRegionalCloudEndpoint = (region: string): string => { - const url = new URL(globalConfig.getEndpoint() || DEFAULT_ENDPOINT); + // The session endpoint may already be regional, so start from the base host to + // avoid producing something like `fra.sgp.cloud.appwrite.io`. + const url = new URL( + normalizeCloudConsoleEndpoint( + globalConfig.getEndpoint() || DEFAULT_ENDPOINT, + ), + ); url.hostname = `${region}.${url.hostname}`; return url.toString().replace(/\/$/, ""); }; diff --git a/lib/commands/pull.ts b/lib/commands/pull.ts index b5c79e8..b30e39c 100644 --- a/lib/commands/pull.ts +++ b/lib/commands/pull.ts @@ -21,7 +21,7 @@ import { import { getFunctionsService, getSitesService } from "../services.js"; import { sdkForProject, sdkForConsole } from "../sdks.js"; import { localConfig } from "../config.js"; -import { applyConfigFilters } from "../config-filters.js"; +import { resolveOrganizationId } from "../context.js"; import { canUseConsole, requireConsoleAuth, @@ -274,13 +274,11 @@ export class Pull { ): Promise { this.log("Pulling project settings ..."); - await applyConfigFilters({ - config: { - organizationId, - projectId, - }, - consoleClient: this.consoleClient, - }); + this.consoleClient.headers["X-Appwrite-Organization"] = + await resolveOrganizationId({ + override: organizationId, + consoleClient: this.consoleClient, + }); const organizationService = new Organization(this.consoleClient); const projectService = new Project(this.projectClient); const project = await organizationService.getProject({ @@ -1118,6 +1116,16 @@ const pullMessagingTopic = async (): Promise => { export const pull = new Command("pull") .description(commandDescriptions["pull"]) + // Also registered on the root program so `appwrite --all pull` keeps working; + // declared here too so they are documented where they actually apply. + .option("-a, --all", "Pull every resource in the project") + .option("--id [id...]", "Limit the pull to these resource ids") + .on("option:all", () => { + cliConfig.all = true; + }) + .on("option:id", function (this: Command) { + cliConfig.ids = this.opts()["id"] as string[]; + }) .action(actionRunner(() => pullResources({ skipDeprecated: true }))); pull diff --git a/lib/commands/push.ts b/lib/commands/push.ts index 0abbb76..5a7178c 100644 --- a/lib/commands/push.ts +++ b/lib/commands/push.ts @@ -20,7 +20,7 @@ import { KeysCollection, KeysTable, } from "../config.js"; -import { applyConfigFilters } from "../config-filters.js"; +import { resolveOrganizationId } from "../context.js"; import { canUseConsole, isAuthScopeError, @@ -1307,10 +1307,11 @@ export class Push { settings?: SettingsType; }): Promise { requireConsoleAuth("Pushing project settings"); - await applyConfigFilters({ - config, - consoleClient: this.consoleClient, - }); + this.consoleClient.headers["X-Appwrite-Organization"] = + await resolveOrganizationId({ + override: config.organizationId, + consoleClient: this.consoleClient, + }); const organizationService = await getOrganizationService( this.consoleClient, ); @@ -3363,11 +3364,11 @@ const pushSettings = async (): Promise => { try { const project = localConfig.getProject(); const consoleClient = await sdkForConsole({ requiresAuth: true }); - await applyConfigFilters({ - config: project, + resolvedOrganizationId = await resolveOrganizationId({ + override: project.organizationId, consoleClient, }); - resolvedOrganizationId = consoleClient.headers["X-Appwrite-Organization"]; + consoleClient.headers["X-Appwrite-Organization"] = resolvedOrganizationId; const organizationService = await getOrganizationService(consoleClient); const projectService = await getProjectService(); const projectId = project.projectId; @@ -4266,6 +4267,16 @@ const pushMessagingTopic = async (): Promise => { export const push = new Command("push") .description(commandDescriptions["push"]) + // Also registered on the root program so `appwrite --all push` keeps working; + // declared here too so they are documented where they actually apply. + .option("-a, --all", "Push every resource in the project config") + .option("--id [id...]", "Limit the push to these resource ids") + .on("option:all", () => { + cliConfig.all = true; + }) + .on("option:id", function (this: Command) { + cliConfig.ids = this.opts()["id"] as string[]; + }) .action(actionRunner(() => pushResources({ skipDeprecated: true }))); push @@ -4367,13 +4378,3 @@ push .alias("topics") .description("Push messaging topics in the current project.") .action(actionRunner(pushMessagingTopic)); - -export const deploy = new Command("deploy") - .description(`Removed. Use ${EXECUTABLE_NAME} push instead`) - .action( - actionRunner(async () => { - warn( - `${EXECUTABLE_NAME} deploy has been removed. Please use '${EXECUTABLE_NAME} push' instead`, - ); - }), - ); diff --git a/lib/commands/services/notifications.ts b/lib/commands/services/notifications.ts index ea7ef49..54e4b24 100644 --- a/lib/commands/services/notifications.ts +++ b/lib/commands/services/notifications.ts @@ -5,7 +5,7 @@ import { parseDeprecatedWhereQuery, parseFilterQuery, } from "../utils/query.js"; -import { sdkForProject } from "../../sdks.js"; +import { sdkForConsole } from "../../sdks.js"; import { actionRunner, commandDescriptions, @@ -20,7 +20,7 @@ let notificationsClient: Notifications | null = null; const getNotificationsClient = async (): Promise => { if (!notificationsClient) { - const sdkClient = await sdkForProject(); + const sdkClient = await sdkForConsole(); notificationsClient = new Notifications(sdkClient); } return notificationsClient; diff --git a/lib/commands/services/oauth2.ts b/lib/commands/services/oauth2.ts index 938cf31..8a78746 100644 --- a/lib/commands/services/oauth2.ts +++ b/lib/commands/services/oauth2.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { sdkForProject } from "../../sdks.js"; +import { sdkForConsole, sdkForProject } from "../../sdks.js"; import { actionRunner, commandDescriptions, @@ -20,6 +20,16 @@ const getOauth2Client = async (): Promise => { return oauth2Client; }; +let oauth2ConsoleClient: Oauth2 | null = null; + +// A few endpoints on this service are only served on the console project. +const getOauth2ConsoleClient = async (): Promise => { + if (!oauth2ConsoleClient) { + oauth2ConsoleClient = new Oauth2(await sdkForConsole()); + } + return oauth2ConsoleClient; +}; + export const oauth2 = new Command("oauth2") .description(commandDescriptions["oauth2"] || "The OAuth2 service allows you to authorize apps and issue standards-based OAuth2 and OpenID Connect tokens.") .configureHelp({ @@ -159,7 +169,7 @@ const oauth2ListOrganizationsCommand = oauth2 .action( actionRunner( async ({ limit, offset, search }) => - parse(await (await getOauth2Client()).listOrganizations(limit, offset, search)), + parse(await (await getOauth2ConsoleClient()).listOrganizations(limit, offset, search)), ), ); @@ -197,7 +207,7 @@ const oauth2ListProjectsCommand = oauth2 .action( actionRunner( async ({ limit, offset, search }) => - parse(await (await getOauth2Client()).listProjects(limit, offset, search)), + parse(await (await getOauth2ConsoleClient()).listProjects(limit, offset, search)), ), ); diff --git a/lib/commands/services/organization.ts b/lib/commands/services/organization.ts index 0d71ffb..73eb1dd 100644 --- a/lib/commands/services/organization.ts +++ b/lib/commands/services/organization.ts @@ -5,7 +5,7 @@ import { parseDeprecatedWhereQuery, parseFilterQuery, } from "../utils/query.js"; -import { sdkForConsole } from "../../sdks.js"; +import { sdkForConsoleWithOrganization } from "../../sdks.js"; import { actionRunner, commandDescriptions, @@ -16,15 +16,12 @@ import { } from "../../parser.js"; import { Organization } from "@appwrite.io/console"; -let organizationClient: Organization | null = null; - -const getOrganizationClient = async (): Promise => { - if (!organizationClient) { - const sdkClient = await sdkForConsole(); - organizationClient = new Organization(sdkClient); - } - return organizationClient; -}; +// Every endpoint here targets one organization, so the client is built per +// command rather than cached across differing --organization-id values. +const getOrganizationClient = async ( + organizationId?: string, +): Promise => + new Organization(await sdkForConsoleWithOrganization(organizationId)); export const organization = new Command("organization") .description(commandDescriptions["organization"] || "The Organization service allows you to manage organization-level projects.") @@ -35,9 +32,11 @@ export const organization = new Command("organization") const organizationGetCommand = organization .command(`get`) .description(`Get the current organization.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async () => parse(await (await getOrganizationClient()).get()), + async ({ organizationId }) => + parse(await (await getOrganizationClient(organizationId)).get()), ), ); @@ -46,10 +45,11 @@ const organizationUpdateCommand = organization .command(`update`) .description(`Update the current organization's name.`) .requiredOption(`--name `, `New organization name. Max length: 128 chars.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ name }) => - parse(await (await getOrganizationClient()).update(name)), + async ({ name, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).update(name)), ), ); @@ -57,9 +57,11 @@ const organizationUpdateCommand = organization const organizationDeleteCommand = organization .command(`delete`) .description(`Delete the current organization. All projects that belong to the organization are deleted as well.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async () => parse(await (await getOrganizationClient()).delete()), + async ({ organizationId }) => + parse(await (await getOrganizationClient(organizationId)).delete()), ), ); @@ -82,10 +84,11 @@ const organizationListInstallationsCommand = organization .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationClient()).listInstallations(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), + async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).listInstallations(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), ), ); @@ -95,10 +98,11 @@ const organizationCreateInstallationCommand = organization .description(`Install an app on the organization. Only organization members with the owner role can install apps. The installation is granted the scopes the app currently requests.`) .requiredOption(`--app-id `, `Application unique ID.`) .option(`--authorization-details `, `Authorization details granted to the installation as a JSON array of objects, each with a \`type\` and app-defined fields. The Appwrite Console stores authorized project IDs here.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ appId, authorizationDetails }) => - parse(await (await getOrganizationClient()).createInstallation(appId, authorizationDetails)), + async ({ appId, authorizationDetails, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).createInstallation(appId, authorizationDetails)), ), ); @@ -107,10 +111,11 @@ const organizationGetInstallationCommand = organization .command(`get-installation`) .description(`Get an app installation on the organization by its unique ID. Any organization member can read installations.`) .requiredOption(`--installation-id `, `Installation unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ installationId }) => - parse(await (await getOrganizationClient()).getInstallation(installationId)), + async ({ installationId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).getInstallation(installationId)), ), ); @@ -120,10 +125,11 @@ const organizationUpdateInstallationCommand = organization .description(`Update an app installation on the organization. Only organization members with the owner role can update installations. The installation's granted scopes are refreshed to the scopes the app currently requests; previously issued installation access tokens are revoked.`) .requiredOption(`--installation-id `, `Installation unique ID.`) .option(`--authorization-details `, `Authorization details granted to the installation as a JSON array of objects, each with a \`type\` and app-defined fields. Omit to keep the current value.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ installationId, authorizationDetails }) => - parse(await (await getOrganizationClient()).updateInstallation(installationId, authorizationDetails)), + async ({ installationId, authorizationDetails, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).updateInstallation(installationId, authorizationDetails)), ), ); @@ -132,10 +138,11 @@ const organizationDeleteInstallationCommand = organization .command(`delete-installation`) .description(`Uninstall an app from the organization by its installation ID. Only organization members with the owner role can remove installations. Previously issued installation access tokens are revoked.`) .requiredOption(`--installation-id `, `Installation unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ installationId }) => - parse(await (await getOrganizationClient()).deleteInstallation(installationId)), + async ({ installationId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).deleteInstallation(installationId)), ), ); @@ -158,10 +165,11 @@ const organizationListKeysCommand = organization .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationClient()).listKeys(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), + async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).listKeys(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), ), ); @@ -173,10 +181,11 @@ const organizationCreateKeyCommand = organization .requiredOption(`--name `, `Key name. Max length: 128 chars.`) .requiredOption(`--scopes [scopes...]`, `Key scopes list. Maximum of 200 scopes are allowed.`) .option(`--expire `, `Expiration time in ISO 8601 (https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId, name, scopes, expire }) => - parse(await (await getOrganizationClient()).createKey(keyId, name, scopes, expire)), + async ({ keyId, name, scopes, expire, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).createKey(keyId, name, scopes, expire)), ), ); @@ -185,10 +194,11 @@ const organizationGetKeyCommand = organization .command(`get-key`) .description(`Get a key by its unique ID. This endpoint returns details about a specific API key in your organization including its scopes.`) .requiredOption(`--key-id `, `Key unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId }) => - parse(await (await getOrganizationClient()).getKey(keyId)), + async ({ keyId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).getKey(keyId)), ), ); @@ -200,10 +210,11 @@ const organizationUpdateKeyCommand = organization .requiredOption(`--name `, `Key name. Max length: 128 chars.`) .requiredOption(`--scopes [scopes...]`, `Key scopes list. Maximum of 200 scopes are allowed.`) .option(`--expire `, `Expiration time in ISO 8601 (https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId, name, scopes, expire }) => - parse(await (await getOrganizationClient()).updateKey(keyId, name, scopes, expire)), + async ({ keyId, name, scopes, expire, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).updateKey(keyId, name, scopes, expire)), ), ); @@ -212,10 +223,11 @@ const organizationDeleteKeyCommand = organization .command(`delete-key`) .description(`Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.`) .requiredOption(`--key-id `, `Key unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId }) => - parse(await (await getOrganizationClient()).deleteKey(keyId)), + async ({ keyId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).deleteKey(keyId)), ), ); @@ -239,10 +251,11 @@ const organizationListMembershipsCommand = organization .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, search, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationClient()).listMemberships(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), search, total)), + async ({ queries, search, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).listMemberships(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), search, total)), ), ); @@ -256,10 +269,11 @@ const organizationCreateMembershipCommand = organization .option(`--phone `, `Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.`) .option(`--url `, `URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied.`) .option(`--name `, `Name of the new organization member. Max length: 128 chars.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ roles, email, userId, phone, url, name }) => - parse(await (await getOrganizationClient()).createMembership(roles, email, userId, phone, url, name)), + async ({ roles, email, userId, phone, url, name, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).createMembership(roles, email, userId, phone, url, name)), ), ); @@ -268,10 +282,11 @@ const organizationGetMembershipCommand = organization .command(`get-membership`) .description(`Get a membership from the current organization by its unique ID.`) .requiredOption(`--membership-id `, `Membership ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ membershipId }) => - parse(await (await getOrganizationClient()).getMembership(membershipId)), + async ({ membershipId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).getMembership(membershipId)), ), ); @@ -281,10 +296,11 @@ const organizationUpdateMembershipCommand = organization .description(`Modify the roles of a member in the current organization.`) .requiredOption(`--membership-id `, `Membership ID.`) .requiredOption(`--roles [roles...]`, `An array of strings. Use this param to set the user's roles in the organization. A role can be any string. Learn more about roles and permissions (https://appwrite.io/docs/permissions). Maximum of 100 roles are allowed, each 81 characters long.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ membershipId, roles }) => - parse(await (await getOrganizationClient()).updateMembership(membershipId, roles)), + async ({ membershipId, roles, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).updateMembership(membershipId, roles)), ), ); @@ -293,10 +309,11 @@ const organizationDeleteMembershipCommand = organization .command(`delete-membership`) .description(`Remove a member from the current organization. The member is removed whether they accepted the invitation or not; a pending invitation is revoked.`) .requiredOption(`--membership-id `, `Membership ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ membershipId }) => - parse(await (await getOrganizationClient()).deleteMembership(membershipId)), + async ({ membershipId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).deleteMembership(membershipId)), ), ); @@ -320,10 +337,11 @@ const organizationListProjectsCommand = organization .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, search, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationClient()).listProjects(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), search, total)), + async ({ queries, search, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).listProjects(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), search, total)), ), ); @@ -334,10 +352,11 @@ const organizationCreateProjectCommand = organization .requiredOption(`--project-id `, `Unique Id. Choose a custom ID or generate a random ID with \`ID.unique()\`. Valid chars are a-z, and hyphen. Can't start with a special char. Max length is 36 chars.`) .requiredOption(`--name `, `Project name. Max length: 128 chars.`) .option(`--region `, `Project Region.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ projectId, name, region }) => - parse(await (await getOrganizationClient()).createProject(projectId, name, region)), + async ({ projectId, name, region, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).createProject(projectId, name, region)), ), ); @@ -346,10 +365,11 @@ const organizationGetProjectCommand = organization .command(`get-project`) .description(`Get a project.`) .requiredOption(`--project-id `, `Project unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ projectId }) => - parse(await (await getOrganizationClient()).getProject(projectId)), + async ({ projectId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).getProject(projectId)), ), ); @@ -359,10 +379,11 @@ const organizationUpdateProjectCommand = organization .description(`Update a project by its unique ID.`) .requiredOption(`--project-id `, `Project unique ID.`) .requiredOption(`--name `, `Project name. Max length: 128 chars.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ projectId, name }) => - parse(await (await getOrganizationClient()).updateProject(projectId, name)), + async ({ projectId, name, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).updateProject(projectId, name)), ), ); @@ -371,10 +392,11 @@ const organizationDeleteProjectCommand = organization .command(`delete-project`) .description(`Delete a project by its unique ID.`) .requiredOption(`--project-id `, `Project unique ID.`) + .option(`--organization-id `, `Organization to act on. Defaults to the organization linked in appwrite.config.json.`) .action( actionRunner( - async ({ projectId }) => - parse(await (await getOrganizationClient()).deleteProject(projectId)), + async ({ projectId, organizationId }) => + parse(await (await getOrganizationClient(organizationId)).deleteProject(projectId)), ), ); diff --git a/lib/commands/services/organizations.ts b/lib/commands/services/organizations.ts deleted file mode 100644 index 2c493e9..0000000 --- a/lib/commands/services/organizations.ts +++ /dev/null @@ -1,624 +0,0 @@ -import { Command } from "commander"; -import fs from "fs"; -import { - buildQueries, - collectQueryValue, - parseDeprecatedWhereQuery, - parseFilterQuery, -} from "../utils/query.js"; -import { sdkForConsole } from "../../sdks.js"; -import { - actionRunner, - commandDescriptions, - success, - parse, - parseBool, - parseInteger, -} from "../../parser.js"; -import { Organizations } from "@appwrite.io/console"; - -let organizationsClient: Organizations | null = null; - -const getOrganizationsClient = async (): Promise => { - if (!organizationsClient) { - const sdkClient = await sdkForConsole(); - organizationsClient = new Organizations(sdkClient); - } - return organizationsClient; -}; - -export const organizations = new Command("organizations") - .description(commandDescriptions["organizations"] || "The Organizations service allows you to manage organization billing, plans, invoices, and add-ons.") - .configureHelp({ - helpWidth: process.stdout.columns || 80, - }); - -const organizationsListCommand = organizations - .command(`list`) - .description(`Get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.`) - .option(`--queries [queries...]`, `Raw Appwrite JSON query strings (legacy). Use this for advanced queries or automation; for common filtering, sorting, and pagination prefer --filter, --sort-asc, --sort-desc, --limit, and --offset. When mixed, raw --queries are sent before generated flag queries. Array of query strings generated using the Query class provided by the SDK. Learn more about queries (https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: name, total, billingPlan, paymentMethodId, backupPaymentMethodId, platform`) - .option(`--search `, `Search term to filter your list results. Max length: 256 chars.`) - .option(`--filter `, `Filter using a simple comparison expression. Repeat for multiple filters. Supports field=value, field!=value, field>value, field>=value, field collectQueryValue(parseFilterQuery(value), previous)) - .option(`--where `, `Deprecated. Use --filter instead. Filter using a simple comparison expression. Repeat for multiple filters.`, (value: string, previous: string[] | undefined) => collectQueryValue(parseDeprecatedWhereQuery(value), previous)) - .option(`--sort-asc `, `Sort results by an attribute in ascending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--sort-desc `, `Sort results by an attribute in descending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--limit `, `Maximum number of results to return.`, parseInteger) - .option(`--offset `, `Number of results to skip.`, parseInteger) - .option(`--cursor-after `, `Return results after this cursor ID.`) - .option(`--cursor-before `, `Return results before this cursor ID.`) - .action( - actionRunner( - async ({ queries, search, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationsClient()).list(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), search)), - ), - ); - - -const organizationsCreateCommand = organizations - .command(`create`) - .description(`Create a new organization. -`) - .requiredOption(`--organization-id `, `Organization 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(`--name `, `Organization name. Max length: 128 chars.`) - .requiredOption(`--billing-plan `, `Organization billing plan chosen`) - .option(`--payment-method-id `, `Payment method ID. Required for pro plans when trial is not available and user doesn't have default payment method set.`) - .option(`--billing-address-id `, `Unique ID of billing address`) - .option(`--invites [invites...]`, `Additional member invites`) - .option(`--coupon-id `, `Coupon id`) - .option(`--tax-id `, `Tax Id associated to billing.`) - .option(`--budget `, `Budget limit for additional usage set for the organization`, parseInteger) - .option(`--platform `, `Platform type`) - .action( - actionRunner( - async ({ organizationId, name, billingPlan, paymentMethodId, billingAddressId, invites, couponId, taxId, budget, platform }) => - parse(await (await getOrganizationsClient()).create(organizationId, name, billingPlan, paymentMethodId, billingAddressId, invites, couponId, taxId, budget, platform)), - ), - ); - - -const organizationsEstimationCreateOrganizationCommand = organizations - .command(`estimation-create-organization`) - .description(`Get estimation for creating an organization.`) - .requiredOption(`--billing-plan `, `Organization billing plan chosen`) - .option(`--payment-method-id `, `Payment method ID. Required for pro plans when trial is not available and user doesn't have default payment method set.`) - .option(`--invites [invites...]`, `Additional member invites`) - .option(`--coupon-id `, `Coupon id`) - .option(`--platform `, `Platform type`) - .action( - actionRunner( - async ({ billingPlan, paymentMethodId, invites, couponId, platform }) => - parse(await (await getOrganizationsClient()).estimationCreateOrganization(billingPlan, paymentMethodId, invites, couponId, platform)), - ), - ); - - -const organizationsDeleteCommand = organizations - .command(`delete`) - .description(`Delete an organization.`) - .requiredOption(`--organization-id `, `Team ID.`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).delete(organizationId)), - ), - ); - - -const organizationsListAddonsCommand = organizations - .command(`list-addons`) - .description(`List all billing addons for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).listAddons(organizationId)), - ), - ); - - -const organizationsCreateBaaAddonCommand = organizations - .command(`create-baa-addon`) - .description(`Create the BAA billing addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).createBaaAddon(organizationId)), - ), - ); - - -const organizationsCreatePremiumGeoDBAddonCommand = organizations - .command(`create-premium-geo-db-addon`) - .description(`Create a Premium Geo DB addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).createPremiumGeoDBAddon(organizationId)), - ), - ); - - -const organizationsGetAddonCommand = organizations - .command(`get-addon`) - .description(`Get the details of a billing addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--addon-id `, `Addon ID`) - .action( - actionRunner( - async ({ organizationId, addonId }) => - parse(await (await getOrganizationsClient()).getAddon(organizationId, addonId)), - ), - ); - - -const organizationsDeleteAddonCommand = organizations - .command(`delete-addon`) - .description(`Delete a billing addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--addon-id `, `Addon ID`) - .action( - actionRunner( - async ({ organizationId, addonId }) => - parse(await (await getOrganizationsClient()).deleteAddon(organizationId, addonId)), - ), - ); - - -const organizationsConfirmAddonPaymentCommand = organizations - .command(`confirm-addon-payment`) - .description(`Confirm payment for a billing addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--addon-id `, `Addon ID`) - .action( - actionRunner( - async ({ organizationId, addonId }) => - parse(await (await getOrganizationsClient()).confirmAddonPayment(organizationId, addonId)), - ), - ); - - -const organizationsGetAddonPriceCommand = organizations - .command(`get-addon-price`) - .description(`Get the price details for a billing addon for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--addon `, `Addon key identifier (e.g. baa).`) - .action( - actionRunner( - async ({ organizationId, addon }) => - parse(await (await getOrganizationsClient()).getAddonPrice(organizationId, addon)), - ), - ); - - -const organizationsListAggregationsCommand = organizations - .command(`list-aggregations`) - .description(`Get a list of all aggregations for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .option(`--queries [queries...]`, `Raw Appwrite JSON query strings (legacy). Use this for advanced queries or automation; for common filtering, sorting, and pagination prefer --filter, --sort-asc, --sort-desc, --limit, and --offset. When mixed, raw --queries are sent before generated flag queries. Array of query strings generated using the Query class provided by the SDK. Learn more about queries (https://appwrite.io/docs/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: teamId, aggregationId, from, to`) - .option(`--filter `, `Filter using a simple comparison expression. Repeat for multiple filters. Supports field=value, field!=value, field>value, field>=value, field collectQueryValue(parseFilterQuery(value), previous)) - .option(`--where `, `Deprecated. Use --filter instead. Filter using a simple comparison expression. Repeat for multiple filters.`, (value: string, previous: string[] | undefined) => collectQueryValue(parseDeprecatedWhereQuery(value), previous)) - .option(`--sort-asc `, `Sort results by an attribute in ascending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--sort-desc `, `Sort results by an attribute in descending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--limit `, `Maximum number of results to return.`, parseInteger) - .option(`--offset `, `Number of results to skip.`, parseInteger) - .option(`--cursor-after `, `Return results after this cursor ID.`) - .option(`--cursor-before `, `Return results before this cursor ID.`) - .action( - actionRunner( - async ({ organizationId, queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationsClient()).listAggregations(organizationId, buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }))), - ), - ); - - -const organizationsGetAggregationCommand = organizations - .command(`get-aggregation`) - .description(`Get a specific aggregation using it's aggregation ID.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--aggregation-id `, `Invoice unique ID`) - .option(`--limit `, `Maximum number of project aggregations to return in response. By default will return maximum 5 results. Maximum of 10 results allowed per request.`, parseInteger) - .option(`--offset `, `Offset value. The default value is 0. Use this param to manage pagination.`, parseInteger) - .action( - actionRunner( - async ({ organizationId, aggregationId, limit, offset }) => - parse(await (await getOrganizationsClient()).getAggregation(organizationId, aggregationId, limit, offset)), - ), - ); - - -const organizationsSetBillingAddressCommand = organizations - .command(`set-billing-address`) - .description(`Set a billing address for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--billing-address-id `, `Unique ID of billing address`) - .action( - actionRunner( - async ({ organizationId, billingAddressId }) => - parse(await (await getOrganizationsClient()).setBillingAddress(organizationId, billingAddressId)), - ), - ); - - -const organizationsSetBillingEmailCommand = organizations - .command(`set-billing-email`) - .description(`Set the current billing email for the organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--billing-email `, `Billing email for the organization.`) - .action( - actionRunner( - async ({ organizationId, billingEmail }) => - parse(await (await getOrganizationsClient()).setBillingEmail(organizationId, billingEmail)), - ), - ); - - -const organizationsUpdateBudgetCommand = organizations - .command(`update-budget`) - .description(`Update the budget limit for an organization.`) - .requiredOption(`--organization-id `, `Organization Unique ID`) - .requiredOption(`--budget `, `Budget limit for additional usage set for the organization`, parseInteger) - .option(`--alerts [alerts...]`, `Budget alert limit percentage`) - .action( - actionRunner( - async ({ organizationId, budget, alerts }) => - parse(await (await getOrganizationsClient()).updateBudget(organizationId, budget, alerts)), - ), - ); - - -const organizationsListCreditsCommand = organizations - .command(`list-credits`) - .description(`List all credits for an organization. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .option(`--queries [queries...]`, `Raw Appwrite JSON query strings (legacy). Use this for advanced queries or automation; for common filtering, sorting, and pagination prefer --filter, --sort-asc, --sort-desc, --limit, and --offset. When mixed, raw --queries are sent before generated flag queries. Array of query strings generated using the Query class provided by the SDK. Learn more about queries (https://appwrite.io/docs/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: teamId, couponId, credits, expiration, status`) - .option(`--filter `, `Filter using a simple comparison expression. Repeat for multiple filters. Supports field=value, field!=value, field>value, field>=value, field collectQueryValue(parseFilterQuery(value), previous)) - .option(`--where `, `Deprecated. Use --filter instead. Filter using a simple comparison expression. Repeat for multiple filters.`, (value: string, previous: string[] | undefined) => collectQueryValue(parseDeprecatedWhereQuery(value), previous)) - .option(`--sort-asc `, `Sort results by an attribute in ascending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--sort-desc `, `Sort results by an attribute in descending order. Repeat for multiple sort fields.`, (value: string, previous: string[] | undefined) => collectQueryValue(value, previous)) - .option(`--limit `, `Maximum number of results to return.`, parseInteger) - .option(`--offset `, `Number of results to skip.`, parseInteger) - .option(`--cursor-after `, `Return results after this cursor ID.`) - .option(`--cursor-before `, `Return results before this cursor ID.`) - .action( - actionRunner( - async ({ organizationId, queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getOrganizationsClient()).listCredits(organizationId, buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }))), - ), - ); - - -const organizationsAddCreditCommand = organizations - .command(`add-credit`) - .description(`Add credit to an organization using a coupon.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--coupon-id `, `ID of the coupon`) - .action( - actionRunner( - async ({ organizationId, couponId }) => - parse(await (await getOrganizationsClient()).addCredit(organizationId, couponId)), - ), - ); - - -const organizationsGetAvailableCreditsCommand = organizations - .command(`get-available-credits`) - .description(`Get total available valid credits for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).getAvailableCredits(organizationId)), - ), - ); - - -const organizationsGetCreditCommand = organizations - .command(`get-credit`) - .description(`Get credit details.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--credit-id `, `Credit Unique ID`) - .action( - actionRunner( - async ({ organizationId, creditId }) => - parse(await (await getOrganizationsClient()).getCredit(organizationId, creditId)), - ), - ); - - -const organizationsEstimationDeleteOrganizationCommand = organizations - .command(`estimation-delete-organization`) - .description(`Get estimation for deleting an organization.`) - .requiredOption(`--organization-id `, `Team ID.`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).estimationDeleteOrganization(organizationId)), - ), - ); - - -const organizationsEstimationUpdatePlanCommand = organizations - .command(`estimation-update-plan`) - .description(`Get estimation for updating the organization plan.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--billing-plan `, `Organization billing plan chosen`) - .option(`--invites [invites...]`, `Additional member invites`) - .option(`--coupon-id `, `Coupon id`) - .action( - actionRunner( - async ({ organizationId, billingPlan, invites, couponId }) => - parse(await (await getOrganizationsClient()).estimationUpdatePlan(organizationId, billingPlan, invites, couponId)), - ), - ); - - -const organizationsCreateDowngradeFeedbackCommand = organizations - .command(`create-downgrade-feedback`) - .description(`Submit feedback about downgrading from a paid plan to a lower tier. This helps the team understand user experience and improve the platform. -`) - .requiredOption(`--organization-id `, `Organization Unique ID`) - .requiredOption(`--reason `, `Feedback reason`) - .requiredOption(`--message `, `Feedback message`) - .requiredOption(`--from-plan-id `, `Plan downgrading from`) - .requiredOption(`--to-plan-id `, `Plan downgrading to`) - .action( - actionRunner( - async ({ organizationId, reason, message, fromPlanId, toPlanId }) => - parse(await (await getOrganizationsClient()).createDowngradeFeedback(organizationId, reason, message, fromPlanId, toPlanId)), - ), - ); - - -const organizationsGetInvoiceCommand = organizations - .command(`get-invoice`) - .description(`Get an invoice by its unique ID.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--invoice-id `, `Invoice unique ID`) - .action( - actionRunner( - async ({ organizationId, invoiceId }) => - parse(await (await getOrganizationsClient()).getInvoice(organizationId, invoiceId)), - ), - ); - - -const organizationsGetInvoiceDownloadCommand = organizations - .command(`get-invoice-download`) - .description(`Download invoice in PDF`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--invoice-id `, `Invoice unique ID`) - .requiredOption(`--destination `, `Path to save the file to.`) - .action( - actionRunner( - async ({ organizationId, invoiceId, destination }) => { - const url = await (await getOrganizationsClient()).getInvoiceDownload(organizationId, invoiceId); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to download file: ${response.status} ${response.statusText}`); - } - const buffer = Buffer.from(await response.arrayBuffer()); - fs.writeFileSync(destination, buffer); - success(`File saved to ${destination}`); - }, - ), - ); - - -const organizationsCreateInvoicePaymentCommand = organizations - .command(`create-invoice-payment`) - .description(`Initiate payment for failed invoice to pay live from console`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--invoice-id `, `Invoice unique ID`) - .requiredOption(`--payment-method-id `, `Payment method ID`) - .action( - actionRunner( - async ({ organizationId, invoiceId, paymentMethodId }) => - parse(await (await getOrganizationsClient()).createInvoicePayment(organizationId, invoiceId, paymentMethodId)), - ), - ); - - -const organizationsValidateInvoiceCommand = organizations - .command(`validate-invoice`) - .description(`Validates the payment linked with the invoice and updates the invoice status if the payment status is changed.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--invoice-id `, `Invoice unique ID`) - .action( - actionRunner( - async ({ organizationId, invoiceId }) => - parse(await (await getOrganizationsClient()).validateInvoice(organizationId, invoiceId)), - ), - ); - - -const organizationsGetInvoiceViewCommand = organizations - .command(`get-invoice-view`) - .description(`View invoice in PDF`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--invoice-id `, `Invoice unique ID`) - .requiredOption(`--destination `, `Path to save the file to.`) - .action( - actionRunner( - async ({ organizationId, invoiceId, destination }) => { - const url = await (await getOrganizationsClient()).getInvoiceView(organizationId, invoiceId); - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to download file: ${response.status} ${response.statusText}`); - } - const buffer = Buffer.from(await response.arrayBuffer()); - fs.writeFileSync(destination, buffer); - success(`File saved to ${destination}`); - }, - ), - ); - - -const organizationsSetDefaultPaymentMethodCommand = organizations - .command(`set-default-payment-method`) - .description(`Set a organization's default payment method.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--payment-method-id `, `Unique ID of payment method`) - .action( - actionRunner( - async ({ organizationId, paymentMethodId }) => - parse(await (await getOrganizationsClient()).setDefaultPaymentMethod(organizationId, paymentMethodId)), - ), - ); - - -const organizationsDeleteDefaultPaymentMethodCommand = organizations - .command(`delete-default-payment-method`) - .description(`Delete the default payment method for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).deleteDefaultPaymentMethod(organizationId)), - ), - ); - - -const organizationsSetBackupPaymentMethodCommand = organizations - .command(`set-backup-payment-method`) - .description(`Set an organization's backup payment method. -`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--payment-method-id `, `Unique ID of payment method`) - .action( - actionRunner( - async ({ organizationId, paymentMethodId }) => - parse(await (await getOrganizationsClient()).setBackupPaymentMethod(organizationId, paymentMethodId)), - ), - ); - - -const organizationsDeleteBackupPaymentMethodCommand = organizations - .command(`delete-backup-payment-method`) - .description(`Delete a backup payment method for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).deleteBackupPaymentMethod(organizationId)), - ), - ); - - -const organizationsGetPlanCommand = organizations - .command(`get-plan`) - .description(`Get the details of the current billing plan for an organization.`) - .requiredOption(`--organization-id `, `Organization Unique ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).getPlan(organizationId)), - ), - ); - - -const organizationsUpdatePlanCommand = organizations - .command(`update-plan`) - .description(`Update the billing plan for an organization.`) - .requiredOption(`--organization-id `, `Organization Unique ID`) - .requiredOption(`--billing-plan `, `Organization billing plan chosen`) - .option(`--payment-method-id `, `Payment method ID. Required for pro plans when trial is not available and user doesn't have default payment method set.`) - .option(`--billing-address-id `, `Unique ID of billing address`) - .option(`--invites [invites...]`, `Additional member invites`) - .option(`--coupon-id `, `Coupon id`) - .option(`--tax-id `, `Tax Id associated to billing.`) - .option(`--budget `, `Budget limit for additional usage set for the organization`, parseInteger) - .action( - actionRunner( - async ({ organizationId, billingPlan, paymentMethodId, billingAddressId, invites, couponId, taxId, budget }) => - parse(await (await getOrganizationsClient()).updatePlan(organizationId, billingPlan, paymentMethodId, billingAddressId, invites, couponId, taxId, budget)), - ), - ); - - -const organizationsCancelDowngradeCommand = organizations - .command(`cancel-downgrade`) - .description(`Cancel the downgrade initiated for an organization.`) - .requiredOption(`--organization-id `, `Organization Unique ID`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).cancelDowngrade(organizationId)), - ), - ); - - -const organizationsListRegionsCommand = organizations - .command(`list-regions`) - .description(`Get all available regions for an organization.`) - .requiredOption(`--organization-id `, `Team ID.`) - .action( - actionRunner( - async ({ organizationId }) => - parse(await (await getOrganizationsClient()).listRegions(organizationId)), - ), - ); - - -const organizationsGetScopesCommand = organizations - .command(`get-scopes`) - .description(`Get Scopes`) - .requiredOption(`--organization-id `, `Organization id`) - .option(`--project-id `, `Project id`) - .action( - actionRunner( - async ({ organizationId, projectId }) => - parse(await (await getOrganizationsClient()).getScopes(organizationId, projectId)), - ), - ); - - -const organizationsSetBillingTaxIdCommand = organizations - .command(`set-billing-tax-id`) - .description(`Set an organization's billing tax ID.`) - .requiredOption(`--organization-id `, `Organization ID`) - .requiredOption(`--tax-id `, `Tax Id associated to billing.`) - .action( - actionRunner( - async ({ organizationId, taxId }) => - parse(await (await getOrganizationsClient()).setBillingTaxId(organizationId, taxId)), - ), - ); - - -const organizationsGetUsageCommand = organizations - .command(`get-usage`) - .description(`Get the usage data for an organization.`) - .requiredOption(`--organization-id `, `Organization ID`) - .option(`--start-date `, `Starting date for the usage`) - .option(`--end-date `, `End date for the usage`) - .action( - actionRunner( - async ({ organizationId, startDate, endDate }) => - parse(await (await getOrganizationsClient()).getUsage(organizationId, startDate, endDate)), - ), - ); - - -const organizationsValidatePaymentCommand = organizations - .command(`validate-payment`) - .description(`Validate payment for team after creation or upgrade.`) - .requiredOption(`--organization-id `, `Organization ID`) - .option(`--invites [invites...]`, `Additional member invites`) - .action( - actionRunner( - async ({ organizationId, invites }) => - parse(await (await getOrganizationsClient()).validatePayment(organizationId, invites)), - ), - ); - - diff --git a/lib/commands/services/project.ts b/lib/commands/services/project.ts index a48edd2..54781ed 100644 --- a/lib/commands/services/project.ts +++ b/lib/commands/services/project.ts @@ -16,15 +16,12 @@ import { } from "../../parser.js"; import { Project } from "@appwrite.io/console"; -let projectClient: Project | null = null; - -const getProjectClient = async (): Promise => { - if (!projectClient) { - const sdkClient = await sdkForProject(); - projectClient = new Project(sdkClient); - } - return projectClient; -}; +// Every endpoint here targets one project, so the client is built per +// command rather than cached across differing --project-id values. +const getProjectClient = async ( + projectId?: string, +): Promise => + new Project(await sdkForProject(projectId)); export const project = new Command("project") .description(commandDescriptions["project"] || "The Project service allows you to manage all the projects in your Appwrite server.") @@ -35,9 +32,11 @@ export const project = new Command("project") const projectGetCommand = project .command(`get`) .description(`Get a project.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async () => parse(await (await getProjectClient()).get()), + async ({ projectId }) => + parse(await (await getProjectClient(projectId)).get()), ), ); @@ -45,9 +44,11 @@ const projectGetCommand = project const projectDeleteCommand = project .command(`delete`) .description(`Delete a project.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async () => parse(await (await getProjectClient()).delete()), + async ({ projectId }) => + parse(await (await getProjectClient(projectId)).delete()), ), ); @@ -57,10 +58,11 @@ const projectUpdateAuthMethodCommand = project .description(`Update properties of a specific auth method. Use this endpoint to enable or disable a method in your project. `) .requiredOption(`--method-id `, `Auth Method ID. Possible values: email-password,magic-url,email-otp,anonymous,invites,jwt,phone`) .requiredOption(`--enabled `, `Auth method status.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ methodId, enabled }) => - parse(await (await getProjectClient()).updateAuthMethod(methodId, enabled)), + async ({ methodId, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateAuthMethod(methodId, enabled)), ), ); @@ -83,10 +85,11 @@ const projectListKeysCommand = project .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getProjectClient()).listKeys(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), + async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listKeys(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), ), ); @@ -100,10 +103,11 @@ You can also create an ephemeral API key if you need a short-lived key instead.` .requiredOption(`--name `, `Key name. Max length: 128 chars.`) .requiredOption(`--scopes [scopes...]`, `Key scopes list. Maximum of 200 scopes are allowed.`) .option(`--expire `, `Expiration time in ISO 8601 (https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId, name, scopes, expire }) => - parse(await (await getProjectClient()).createKey(keyId, name, scopes, expire)), + async ({ keyId, name, scopes, expire, projectId }) => + parse(await (await getProjectClient(projectId)).createKey(keyId, name, scopes, expire)), ), ); @@ -115,10 +119,11 @@ const projectCreateEphemeralKeyCommand = project You can also create a standard API key if you need a longer-lived key instead.`) .requiredOption(`--scopes [scopes...]`, `Key scopes list. Maximum of 200 scopes are allowed.`) .requiredOption(`--duration `, `Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ scopes, duration }) => - parse(await (await getProjectClient()).createEphemeralKey(scopes, duration)), + async ({ scopes, duration, projectId }) => + parse(await (await getProjectClient(projectId)).createEphemeralKey(scopes, duration)), ), ); @@ -127,10 +132,11 @@ const projectGetKeyCommand = project .command(`get-key`) .description(`Get a key by its unique ID. `) .requiredOption(`--key-id `, `Key ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId }) => - parse(await (await getProjectClient()).getKey(keyId)), + async ({ keyId, projectId }) => + parse(await (await getProjectClient(projectId)).getKey(keyId)), ), ); @@ -142,10 +148,11 @@ const projectUpdateKeyCommand = project .requiredOption(`--name `, `Key name. Max length: 128 chars.`) .requiredOption(`--scopes [scopes...]`, `Key scopes list. Maximum of 200 scopes are allowed.`) .option(`--expire `, `Expiration time in ISO 8601 (https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId, name, scopes, expire }) => - parse(await (await getProjectClient()).updateKey(keyId, name, scopes, expire)), + async ({ keyId, name, scopes, expire, projectId }) => + parse(await (await getProjectClient(projectId)).updateKey(keyId, name, scopes, expire)), ), ); @@ -154,10 +161,11 @@ const projectDeleteKeyCommand = project .command(`delete-key`) .description(`Delete a key by its unique ID. Once deleted, the key can no longer be used to authenticate API calls.`) .requiredOption(`--key-id `, `Key ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyId }) => - parse(await (await getProjectClient()).deleteKey(keyId)), + async ({ keyId, projectId }) => + parse(await (await getProjectClient(projectId)).deleteKey(keyId)), ), ); @@ -166,10 +174,11 @@ const projectUpdateLabelsCommand = project .command(`update-labels`) .description(`Update the project labels. Labels can be used to easily filter projects in an organization.`) .requiredOption(`--labels [labels...]`, `Array of project labels. Replaces the previous labels. Maximum of 1000 labels are allowed, each up to 36 alphanumeric characters long.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ labels }) => - parse(await (await getProjectClient()).updateLabels(labels)), + async ({ labels, projectId }) => + parse(await (await getProjectClient(projectId)).updateLabels(labels)), ), ); @@ -186,10 +195,11 @@ const projectListMockPhonesCommand = project ) .option(`--limit `, `Maximum number of results to return.`, parseInteger) .option(`--offset `, `Number of results to skip.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, limit, offset }) => - parse(await (await getProjectClient()).listMockPhones(buildQueries({ queries, limit, offset }), total)), + async ({ queries, total, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listMockPhones(buildQueries({ queries, limit, offset }), total)), ), ); @@ -199,10 +209,11 @@ const projectCreateMockPhoneCommand = project .description(`Create a new mock phone for your project. Use this endpoint to register a mock phone number and its sign-in OTP for your testers.`) .requiredOption(`--number `, `Phone number to associate with the mock phone. Must be a valid E.164 formatted phone number.`) .requiredOption(`--otp `, `One-time password (OTP) to associate with the mock phone. Must be a 6-digit numeric code.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ number, otp }) => - parse(await (await getProjectClient()).createMockPhone(number, otp)), + async ({ number, otp, projectId }) => + parse(await (await getProjectClient(projectId)).createMockPhone(number, otp)), ), ); @@ -211,10 +222,11 @@ const projectGetMockPhoneCommand = project .command(`get-mock-phone`) .description(`Get a mock phone by its unique number. This endpoint returns the mock phone's OTP.`) .requiredOption(`--number `, `Phone number associated with the mock phone. Must be a valid E.164 formatted phone number.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ number }) => - parse(await (await getProjectClient()).getMockPhone(number)), + async ({ number, projectId }) => + parse(await (await getProjectClient(projectId)).getMockPhone(number)), ), ); @@ -224,10 +236,11 @@ const projectUpdateMockPhoneCommand = project .description(`Update a mock phone by its unique number. Use this endpoint to update the mock phone's OTP.`) .requiredOption(`--number `, `Phone number associated with the mock phone. Must be a valid E.164 formatted phone number.`) .requiredOption(`--otp `, `One-time password (OTP) to associate with the mock phone. Must be a 6-digit numeric code.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ number, otp }) => - parse(await (await getProjectClient()).updateMockPhone(number, otp)), + async ({ number, otp, projectId }) => + parse(await (await getProjectClient(projectId)).updateMockPhone(number, otp)), ), ); @@ -236,10 +249,11 @@ const projectDeleteMockPhoneCommand = project .command(`delete-mock-phone`) .description(`Delete a mock phone by its unique number. This endpoint removes the mock phone and its OTP configuration from the project.`) .requiredOption(`--number `, `Phone number associated with the mock phone. Must be a valid E.164 formatted phone number.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ number }) => - parse(await (await getProjectClient()).deleteMockPhone(number)), + async ({ number, projectId }) => + parse(await (await getProjectClient(projectId)).deleteMockPhone(number)), ), ); @@ -256,10 +270,11 @@ const projectListOAuth2ProvidersCommand = project ) .option(`--limit `, `Maximum number of results to return.`, parseInteger) .option(`--offset `, `Number of results to skip.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, limit, offset }) => - parse(await (await getProjectClient()).listOAuth2Providers(buildQueries({ queries, limit, offset }), total)), + async ({ queries, total, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listOAuth2Providers(buildQueries({ queries, limit, offset }), total)), ), ); @@ -287,10 +302,11 @@ const projectUpdateOAuth2ServerCommand = project .option(`--user-code-format `, `Character set for device flow user codes: \`numeric\` (digits only — best for numeric keypads and TV remotes), \`alphabetic\` (letters only), or \`alphanumeric\` (letters and digits — highest entropy per character). Defaults to \`alphanumeric\`.`) .option(`--device-code-duration `, `Lifetime in seconds of device flow device codes and user codes. Device codes are intentionally short-lived. Leave empty to use default 600.`, parseInteger) .option(`--default-scopes [default-scopes...]`, `List of OAuth2 scopes used when an authorization request omits the scope parameter. Every default scope must also be allowed by the OAuth2 server. Maximum of 100 scopes are allowed, each up to 128 characters long.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled, authorizationUrl, scopes, authorizationDetailsTypes, accessTokenDuration, refreshTokenDuration, publicAccessTokenDuration, publicRefreshTokenDuration, installationAccessTokenDuration, confidentialPkce, verificationUrl, userCodeLength, userCodeFormat, deviceCodeDuration, defaultScopes }) => - parse(await (await getProjectClient()).updateOAuth2Server(enabled, authorizationUrl, scopes, authorizationDetailsTypes, accessTokenDuration, refreshTokenDuration, publicAccessTokenDuration, publicRefreshTokenDuration, installationAccessTokenDuration, confidentialPkce, verificationUrl, userCodeLength, userCodeFormat, deviceCodeDuration, defaultScopes)), + async ({ enabled, authorizationUrl, scopes, authorizationDetailsTypes, accessTokenDuration, refreshTokenDuration, publicAccessTokenDuration, publicRefreshTokenDuration, installationAccessTokenDuration, confidentialPkce, verificationUrl, userCodeLength, userCodeFormat, deviceCodeDuration, defaultScopes, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Server(enabled, authorizationUrl, scopes, authorizationDetailsTypes, accessTokenDuration, refreshTokenDuration, publicAccessTokenDuration, publicRefreshTokenDuration, installationAccessTokenDuration, confidentialPkce, verificationUrl, userCodeLength, userCodeFormat, deviceCodeDuration, defaultScopes)), ), ); @@ -306,10 +322,11 @@ const projectUpdateOAuth2AmazonCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Amazon(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Amazon(clientId, clientSecret, enabled)), ), ); @@ -327,10 +344,11 @@ const projectUpdateOAuth2AppleCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ serviceId, keyId, teamId, p8File, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Apple(serviceId, keyId, teamId, p8File, enabled)), + async ({ serviceId, keyId, teamId, p8File, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Apple(serviceId, keyId, teamId, p8File, enabled)), ), ); @@ -346,10 +364,11 @@ const projectUpdateOAuth2AppwriteCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Appwrite(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Appwrite(clientId, clientSecret, enabled)), ), ); @@ -366,10 +385,11 @@ const projectUpdateOAuth2Auth0Command = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, endpoint, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Auth0(clientId, clientSecret, endpoint, enabled)), + async ({ clientId, clientSecret, endpoint, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Auth0(clientId, clientSecret, endpoint, enabled)), ), ); @@ -386,10 +406,11 @@ const projectUpdateOAuth2AuthentikCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, endpoint, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Authentik(clientId, clientSecret, endpoint, enabled)), + async ({ clientId, clientSecret, endpoint, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Authentik(clientId, clientSecret, endpoint, enabled)), ), ); @@ -405,10 +426,11 @@ const projectUpdateOAuth2AutodeskCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Autodesk(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Autodesk(clientId, clientSecret, enabled)), ), ); @@ -424,10 +446,11 @@ const projectUpdateOAuth2BitbucketCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ key, secret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Bitbucket(key, secret, enabled)), + async ({ key, secret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Bitbucket(key, secret, enabled)), ), ); @@ -443,10 +466,11 @@ const projectUpdateOAuth2BitlyCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Bitly(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Bitly(clientId, clientSecret, enabled)), ), ); @@ -462,10 +486,11 @@ const projectUpdateOAuth2BoxCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Box(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Box(clientId, clientSecret, enabled)), ), ); @@ -481,10 +506,11 @@ const projectUpdateOAuth2DailymotionCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ apiKey, apiSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Dailymotion(apiKey, apiSecret, enabled)), + async ({ apiKey, apiSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Dailymotion(apiKey, apiSecret, enabled)), ), ); @@ -500,10 +526,11 @@ const projectUpdateOAuth2DiscordCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Discord(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Discord(clientId, clientSecret, enabled)), ), ); @@ -519,10 +546,11 @@ const projectUpdateOAuth2DisqusCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ publicKey, secretKey, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Disqus(publicKey, secretKey, enabled)), + async ({ publicKey, secretKey, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Disqus(publicKey, secretKey, enabled)), ), ); @@ -538,10 +566,11 @@ const projectUpdateOAuth2DropboxCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ appKey, appSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Dropbox(appKey, appSecret, enabled)), + async ({ appKey, appSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Dropbox(appKey, appSecret, enabled)), ), ); @@ -557,10 +586,11 @@ const projectUpdateOAuth2EtsyCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ keyString, sharedSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Etsy(keyString, sharedSecret, enabled)), + async ({ keyString, sharedSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Etsy(keyString, sharedSecret, enabled)), ), ); @@ -576,10 +606,11 @@ const projectUpdateOAuth2FacebookCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ appId, appSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Facebook(appId, appSecret, enabled)), + async ({ appId, appSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Facebook(appId, appSecret, enabled)), ), ); @@ -595,10 +626,11 @@ const projectUpdateOAuth2FigmaCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Figma(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Figma(clientId, clientSecret, enabled)), ), ); @@ -615,10 +647,11 @@ const projectUpdateOAuth2FusionAuthCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, endpoint, enabled }) => - parse(await (await getProjectClient()).updateOAuth2FusionAuth(clientId, clientSecret, endpoint, enabled)), + async ({ clientId, clientSecret, endpoint, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2FusionAuth(clientId, clientSecret, endpoint, enabled)), ), ); @@ -634,10 +667,11 @@ const projectUpdateOAuth2GitHubCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2GitHub(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2GitHub(clientId, clientSecret, enabled)), ), ); @@ -654,10 +688,11 @@ const projectUpdateOAuth2GitlabCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ applicationId, secret, endpoint, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Gitlab(applicationId, secret, endpoint, enabled)), + async ({ applicationId, secret, endpoint, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Gitlab(applicationId, secret, endpoint, enabled)), ), ); @@ -674,10 +709,11 @@ const projectUpdateOAuth2GoogleCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, prompt, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Google(clientId, clientSecret, prompt, enabled)), + async ({ clientId, clientSecret, prompt, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Google(clientId, clientSecret, prompt, enabled)), ), ); @@ -695,10 +731,11 @@ const projectUpdateOAuth2KeycloakCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, endpoint, realmName, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Keycloak(clientId, clientSecret, endpoint, realmName, enabled)), + async ({ clientId, clientSecret, endpoint, realmName, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Keycloak(clientId, clientSecret, endpoint, realmName, enabled)), ), ); @@ -714,10 +751,11 @@ const projectUpdateOAuth2KickCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Kick(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Kick(clientId, clientSecret, enabled)), ), ); @@ -733,10 +771,11 @@ const projectUpdateOAuth2LinkedinCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, primaryClientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Linkedin(clientId, primaryClientSecret, enabled)), + async ({ clientId, primaryClientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Linkedin(clientId, primaryClientSecret, enabled)), ), ); @@ -753,10 +792,11 @@ const projectUpdateOAuth2MicrosoftCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ applicationId, applicationSecret, tenant, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Microsoft(applicationId, applicationSecret, tenant, enabled)), + async ({ applicationId, applicationSecret, tenant, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Microsoft(applicationId, applicationSecret, tenant, enabled)), ), ); @@ -772,10 +812,11 @@ const projectUpdateOAuth2NotionCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ oauthClientId, oauthClientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Notion(oauthClientId, oauthClientSecret, enabled)), + async ({ oauthClientId, oauthClientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Notion(oauthClientId, oauthClientSecret, enabled)), ), ); @@ -797,10 +838,11 @@ const projectUpdateOAuth2OidcCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, wellKnownUrl, authorizationUrl, tokenUrl, userInfoUrl, prompt, maxAge, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Oidc(clientId, clientSecret, wellKnownUrl, authorizationUrl, tokenUrl, userInfoUrl, prompt, maxAge, enabled)), + async ({ clientId, clientSecret, wellKnownUrl, authorizationUrl, tokenUrl, userInfoUrl, prompt, maxAge, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Oidc(clientId, clientSecret, wellKnownUrl, authorizationUrl, tokenUrl, userInfoUrl, prompt, maxAge, enabled)), ), ); @@ -818,10 +860,11 @@ const projectUpdateOAuth2OktaCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, domain, authorizationServerId, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Okta(clientId, clientSecret, domain, authorizationServerId, enabled)), + async ({ clientId, clientSecret, domain, authorizationServerId, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Okta(clientId, clientSecret, domain, authorizationServerId, enabled)), ), ); @@ -837,10 +880,11 @@ const projectUpdateOAuth2PaypalCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, secretKey, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Paypal(clientId, secretKey, enabled)), + async ({ clientId, secretKey, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Paypal(clientId, secretKey, enabled)), ), ); @@ -856,10 +900,11 @@ const projectUpdateOAuth2PaypalSandboxCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, secretKey, enabled }) => - parse(await (await getProjectClient()).updateOAuth2PaypalSandbox(clientId, secretKey, enabled)), + async ({ clientId, secretKey, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2PaypalSandbox(clientId, secretKey, enabled)), ), ); @@ -875,10 +920,11 @@ const projectUpdateOAuth2PodioCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Podio(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Podio(clientId, clientSecret, enabled)), ), ); @@ -894,10 +940,11 @@ const projectUpdateOAuth2SalesforceCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ customerKey, customerSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Salesforce(customerKey, customerSecret, enabled)), + async ({ customerKey, customerSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Salesforce(customerKey, customerSecret, enabled)), ), ); @@ -913,10 +960,11 @@ const projectUpdateOAuth2SlackCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Slack(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Slack(clientId, clientSecret, enabled)), ), ); @@ -932,10 +980,11 @@ const projectUpdateOAuth2SpotifyCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Spotify(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Spotify(clientId, clientSecret, enabled)), ), ); @@ -951,10 +1000,11 @@ const projectUpdateOAuth2StripeCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, apiSecretKey, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Stripe(clientId, apiSecretKey, enabled)), + async ({ clientId, apiSecretKey, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Stripe(clientId, apiSecretKey, enabled)), ), ); @@ -970,10 +1020,11 @@ const projectUpdateOAuth2TradeshiftCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ oauth2ClientId, oauth2ClientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Tradeshift(oauth2ClientId, oauth2ClientSecret, enabled)), + async ({ oauth2ClientId, oauth2ClientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Tradeshift(oauth2ClientId, oauth2ClientSecret, enabled)), ), ); @@ -989,10 +1040,11 @@ const projectUpdateOAuth2TradeshiftSandboxCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ oauth2ClientId, oauth2ClientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2TradeshiftSandbox(oauth2ClientId, oauth2ClientSecret, enabled)), + async ({ oauth2ClientId, oauth2ClientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2TradeshiftSandbox(oauth2ClientId, oauth2ClientSecret, enabled)), ), ); @@ -1008,10 +1060,11 @@ const projectUpdateOAuth2TwitchCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Twitch(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Twitch(clientId, clientSecret, enabled)), ), ); @@ -1027,10 +1080,11 @@ const projectUpdateOAuth2WordPressCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2WordPress(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2WordPress(clientId, clientSecret, enabled)), ), ); @@ -1046,10 +1100,11 @@ const projectUpdateOAuth2XCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ customerKey, secretKey, enabled }) => - parse(await (await getProjectClient()).updateOAuth2X(customerKey, secretKey, enabled)), + async ({ customerKey, secretKey, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2X(customerKey, secretKey, enabled)), ), ); @@ -1065,10 +1120,11 @@ const projectUpdateOAuth2YahooCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Yahoo(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Yahoo(clientId, clientSecret, enabled)), ), ); @@ -1084,10 +1140,11 @@ const projectUpdateOAuth2YandexCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Yandex(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Yandex(clientId, clientSecret, enabled)), ), ); @@ -1103,10 +1160,11 @@ const projectUpdateOAuth2ZohoCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Zoho(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Zoho(clientId, clientSecret, enabled)), ), ); @@ -1122,10 +1180,11 @@ const projectUpdateOAuth2ZoomCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ clientId, clientSecret, enabled }) => - parse(await (await getProjectClient()).updateOAuth2Zoom(clientId, clientSecret, enabled)), + async ({ clientId, clientSecret, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateOAuth2Zoom(clientId, clientSecret, enabled)), ), ); @@ -1134,10 +1193,11 @@ const projectGetOAuth2ProviderCommand = project .command(`get-o-auth-2-provider`) .description(`Get a single OAuth2 provider configuration. Credential fields (client secret, p8 file, key/team IDs) are write-only and always returned empty.`) .requiredOption(`--provider-id `, `OAuth2 provider key. For example: github, google, apple.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ providerId }) => - parse(await (await getProjectClient()).getOAuth2Provider(providerId)), + async ({ providerId, projectId }) => + parse(await (await getProjectClient(projectId)).getOAuth2Provider(providerId)), ), ); @@ -1160,10 +1220,11 @@ const projectListPlatformsCommand = project .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getProjectClient()).listPlatforms(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), + async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listPlatforms(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), ), ); @@ -1174,10 +1235,11 @@ const projectCreateAndroidPlatformCommand = project .requiredOption(`--platform-id `, `Platform 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(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--application-id `, `Android application ID. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, applicationId }) => - parse(await (await getProjectClient()).createAndroidPlatform(platformId, name, applicationId)), + async ({ platformId, name, applicationId, projectId }) => + parse(await (await getProjectClient(projectId)).createAndroidPlatform(platformId, name, applicationId)), ), ); @@ -1188,10 +1250,11 @@ const projectUpdateAndroidPlatformCommand = project .requiredOption(`--platform-id `, `Platform ID.`) .requiredOption(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--application-id `, `Android application ID. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, applicationId }) => - parse(await (await getProjectClient()).updateAndroidPlatform(platformId, name, applicationId)), + async ({ platformId, name, applicationId, projectId }) => + parse(await (await getProjectClient(projectId)).updateAndroidPlatform(platformId, name, applicationId)), ), ); @@ -1202,10 +1265,11 @@ const projectCreateApplePlatformCommand = project .requiredOption(`--platform-id `, `Platform 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(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--bundle-identifier `, `Apple bundle identifier. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, bundleIdentifier }) => - parse(await (await getProjectClient()).createApplePlatform(platformId, name, bundleIdentifier)), + async ({ platformId, name, bundleIdentifier, projectId }) => + parse(await (await getProjectClient(projectId)).createApplePlatform(platformId, name, bundleIdentifier)), ), ); @@ -1216,10 +1280,11 @@ const projectUpdateApplePlatformCommand = project .requiredOption(`--platform-id `, `Platform ID.`) .requiredOption(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--bundle-identifier `, `Apple bundle identifier. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, bundleIdentifier }) => - parse(await (await getProjectClient()).updateApplePlatform(platformId, name, bundleIdentifier)), + async ({ platformId, name, bundleIdentifier, projectId }) => + parse(await (await getProjectClient(projectId)).updateApplePlatform(platformId, name, bundleIdentifier)), ), ); @@ -1230,10 +1295,11 @@ const projectCreateLinuxPlatformCommand = project .requiredOption(`--platform-id `, `Platform 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(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--package-name `, `Linux package name. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, packageName }) => - parse(await (await getProjectClient()).createLinuxPlatform(platformId, name, packageName)), + async ({ platformId, name, packageName, projectId }) => + parse(await (await getProjectClient(projectId)).createLinuxPlatform(platformId, name, packageName)), ), ); @@ -1244,10 +1310,11 @@ const projectUpdateLinuxPlatformCommand = project .requiredOption(`--platform-id `, `Platform ID.`) .requiredOption(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--package-name `, `Linux package name. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, packageName }) => - parse(await (await getProjectClient()).updateLinuxPlatform(platformId, name, packageName)), + async ({ platformId, name, packageName, projectId }) => + parse(await (await getProjectClient(projectId)).updateLinuxPlatform(platformId, name, packageName)), ), ); @@ -1258,10 +1325,11 @@ const projectCreateWebPlatformCommand = project .requiredOption(`--platform-id `, `Platform 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(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--hostname `, `Platform web hostname. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, hostname }) => - parse(await (await getProjectClient()).createWebPlatform(platformId, name, hostname)), + async ({ platformId, name, hostname, projectId }) => + parse(await (await getProjectClient(projectId)).createWebPlatform(platformId, name, hostname)), ), ); @@ -1272,10 +1340,11 @@ const projectUpdateWebPlatformCommand = project .requiredOption(`--platform-id `, `Platform ID.`) .requiredOption(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--hostname `, `Platform web hostname. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, hostname }) => - parse(await (await getProjectClient()).updateWebPlatform(platformId, name, hostname)), + async ({ platformId, name, hostname, projectId }) => + parse(await (await getProjectClient(projectId)).updateWebPlatform(platformId, name, hostname)), ), ); @@ -1286,10 +1355,11 @@ const projectCreateWindowsPlatformCommand = project .requiredOption(`--platform-id `, `Platform 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(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--package-identifier-name `, `Windows package identifier name. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, packageIdentifierName }) => - parse(await (await getProjectClient()).createWindowsPlatform(platformId, name, packageIdentifierName)), + async ({ platformId, name, packageIdentifierName, projectId }) => + parse(await (await getProjectClient(projectId)).createWindowsPlatform(platformId, name, packageIdentifierName)), ), ); @@ -1300,10 +1370,11 @@ const projectUpdateWindowsPlatformCommand = project .requiredOption(`--platform-id `, `Platform ID.`) .requiredOption(`--name `, `Platform name. Max length: 128 chars.`) .requiredOption(`--package-identifier-name `, `Windows package identifier name. Max length: 256 chars.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId, name, packageIdentifierName }) => - parse(await (await getProjectClient()).updateWindowsPlatform(platformId, name, packageIdentifierName)), + async ({ platformId, name, packageIdentifierName, projectId }) => + parse(await (await getProjectClient(projectId)).updateWindowsPlatform(platformId, name, packageIdentifierName)), ), ); @@ -1312,10 +1383,11 @@ const projectGetPlatformCommand = project .command(`get-platform`) .description(`Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.`) .requiredOption(`--platform-id `, `Platform ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId }) => - parse(await (await getProjectClient()).getPlatform(platformId)), + async ({ platformId, projectId }) => + parse(await (await getProjectClient(projectId)).getPlatform(platformId)), ), ); @@ -1324,10 +1396,11 @@ const projectDeletePlatformCommand = project .command(`delete-platform`) .description(`Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.`) .requiredOption(`--platform-id `, `Platform ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ platformId }) => - parse(await (await getProjectClient()).deletePlatform(platformId)), + async ({ platformId, projectId }) => + parse(await (await getProjectClient(projectId)).deletePlatform(platformId)), ), ); @@ -1344,10 +1417,11 @@ const projectListPoliciesCommand = project ) .option(`--limit `, `Maximum number of results to return.`, parseInteger) .option(`--offset `, `Number of results to skip.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, limit, offset }) => - parse(await (await getProjectClient()).listPolicies(buildQueries({ queries, limit, offset }), total)), + async ({ queries, total, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listPolicies(buildQueries({ queries, limit, offset }), total)), ), ); @@ -1356,10 +1430,11 @@ const projectUpdateDenyAliasedEmailPolicyCommand = project .command(`update-deny-aliased-email-policy`) .description(`Configures if aliased emails such as subaddresses and emails with suffixes are denied during new users sign-ups and email updates.`) .requiredOption(`--enabled `, `Set whether or not to block aliased emails during signup and email updates.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateDenyAliasedEmailPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateDenyAliasedEmailPolicy(enabled)), ), ); @@ -1368,10 +1443,11 @@ const projectUpdateDenyCorporateEmailPolicyCommand = project .command(`update-deny-corporate-email-policy`) .description(`Configures if only corporate email addresses (non-free and non-disposable domains) are allowed during new user sign-ups and email updates.`) .requiredOption(`--enabled `, `Set whether or not to restrict sign-ups and email updates to corporate email addresses only.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateDenyCorporateEmailPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateDenyCorporateEmailPolicy(enabled)), ), ); @@ -1380,10 +1456,11 @@ const projectUpdateDenyDisposableEmailPolicyCommand = project .command(`update-deny-disposable-email-policy`) .description(`Configures if disposable emails from known temporary domains are denied during new users sign-ups and email updates.`) .requiredOption(`--enabled `, `Set whether or not to block disposable email addresses during signup and email updates.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateDenyDisposableEmailPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateDenyDisposableEmailPolicy(enabled)), ), ); @@ -1392,10 +1469,11 @@ const projectUpdateDenyFreeEmailPolicyCommand = project .command(`update-deny-free-email-policy`) .description(`Configures if emails from free providers such as Gmail or Yahoo are denied during new users sign-ups and email updates.`) .requiredOption(`--enabled `, `Set whether or not to block free email addresses during signup and email updates.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateDenyFreeEmailPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateDenyFreeEmailPolicy(enabled)), ), ); @@ -1439,10 +1517,11 @@ const projectUpdateMembershipPrivacyPolicyCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ userId, userEmail, userPhone, userName, userMfa, userAccessedAt }) => - parse(await (await getProjectClient()).updateMembershipPrivacyPolicy(userId, userEmail, userPhone, userName, userMfa, userAccessedAt)), + async ({ userId, userEmail, userPhone, userName, userMfa, userAccessedAt, projectId }) => + parse(await (await getProjectClient(projectId)).updateMembershipPrivacyPolicy(userId, userEmail, userPhone, userName, userMfa, userAccessedAt)), ), ); @@ -1451,10 +1530,11 @@ const projectUpdatePasswordDictionaryPolicyCommand = project .command(`update-password-dictionary-policy`) .description(`Updating this policy allows you to control if new passwords are checked against most common passwords dictionary. When enabled, and user changes their password, password must not be contained in the dictionary.`) .requiredOption(`--enabled `, `Toggle password dictionary policy. Set to true if you want password change to block passwords in the dictionary, or false to allow them. When changing this policy, existing passwords remain valid.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updatePasswordDictionaryPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updatePasswordDictionaryPolicy(enabled)), ), ); @@ -1465,10 +1545,11 @@ const projectUpdatePasswordHistoryPolicyCommand = project 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 20, or null to disable the limit.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ total }) => - parse(await (await getProjectClient()).updatePasswordHistoryPolicy(total)), + async ({ total, projectId }) => + parse(await (await getProjectClient(projectId)).updatePasswordHistoryPolicy(total)), ), ); @@ -1477,10 +1558,11 @@ const projectUpdatePasswordPersonalDataPolicyCommand = project .command(`update-password-personal-data-policy`) .description(`Updating this policy allows you to control if password strength is checked against personal data. When enabled, and user sets or changes their password, the password must not contain user ID, name, email or phone number.`) .requiredOption(`--enabled `, `Toggle password personal data policy. Set to true if you want to block passwords including user's personal data, or false to allow it. When changing this policy, existing passwords remain valid.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updatePasswordPersonalDataPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updatePasswordPersonalDataPolicy(enabled)), ), ); @@ -1513,10 +1595,11 @@ const projectUpdatePasswordStrengthPolicyCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ min, uppercase, lowercase, number, symbols }) => - parse(await (await getProjectClient()).updatePasswordStrengthPolicy(min, uppercase, lowercase, number, symbols)), + async ({ min, uppercase, lowercase, number, symbols, projectId }) => + parse(await (await getProjectClient(projectId)).updatePasswordStrengthPolicy(min, uppercase, lowercase, number, symbols)), ), ); @@ -1525,10 +1608,11 @@ const projectUpdateSessionAlertPolicyCommand = project .command(`update-session-alert-policy`) .description(`Updating this policy allows you to control if email alert is sent upon session creation. When enabled, and user signs into their account, they will be sent an email notification. There is an exception, the first session after a new sign up does not trigger an alert, even if the policy is enabled.`) .requiredOption(`--enabled `, `Toggle session alert policy. Set to true if you want users to receive email notifications when a sessions are created for their users, or false to not send email alerts.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateSessionAlertPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateSessionAlertPolicy(enabled)), ), ); @@ -1537,10 +1621,11 @@ 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 60 seconds, and maximum is 1 year, which is 31536000 seconds.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ duration }) => - parse(await (await getProjectClient()).updateSessionDurationPolicy(duration)), + async ({ duration, projectId }) => + parse(await (await getProjectClient(projectId)).updateSessionDurationPolicy(duration)), ), ); @@ -1549,10 +1634,11 @@ const projectUpdateSessionInvalidationPolicyCommand = project .command(`update-session-invalidation-policy`) .description(`Updating this policy allows you to control if existing sessions should be invalidated when a password of a user is changed. When enabled, and user changes their password, they will be logged out of all their devices.`) .requiredOption(`--enabled `, `Toggle session invalidation policy. Set to true if you want password change to invalidate all sessions of an user, or false to keep sessions active.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ enabled }) => - parse(await (await getProjectClient()).updateSessionInvalidationPolicy(enabled)), + async ({ enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateSessionInvalidationPolicy(enabled)), ), ); @@ -1561,10 +1647,11 @@ 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 100.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ total }) => - parse(await (await getProjectClient()).updateSessionLimitPolicy(total)), + async ({ total, projectId }) => + parse(await (await getProjectClient(projectId)).updateSessionLimitPolicy(total)), ), ); @@ -1573,10 +1660,11 @@ 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 0 and 10000. Use 0 or null to disable the limit.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ total }) => - parse(await (await getProjectClient()).updateUserLimitPolicy(total)), + async ({ total, projectId }) => + parse(await (await getProjectClient(projectId)).updateUserLimitPolicy(total)), ), ); @@ -1585,10 +1673,11 @@ const projectGetPolicyCommand = project .command(`get-policy`) .description(`Get a policy by its unique ID. This endpoint returns the current configuration for the requested project policy.`) .requiredOption(`--policy-id `, `Policy ID. Can be one of: password-dictionary, password-history, password-strength, password-personal-data, session-alert, session-duration, session-invalidation, session-limit, user-limit, membership-privacy, deny-aliased-email, deny-disposable-email, deny-free-email, deny-corporate-email.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ policyId }) => - parse(await (await getProjectClient()).getPolicy(policyId)), + async ({ policyId, projectId }) => + parse(await (await getProjectClient(projectId)).getPolicy(policyId)), ), ); @@ -1598,10 +1687,11 @@ const projectUpdateProtocolCommand = project .description(`Update properties of a specific protocol. Use this endpoint to enable or disable a protocol in your project. `) .requiredOption(`--protocol-id `, `Protocol name. Can be one of: rest, graphql, websocket`) .requiredOption(`--enabled `, `Protocol status.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ protocolId, enabled }) => - parse(await (await getProjectClient()).updateProtocol(protocolId, enabled)), + async ({ protocolId, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateProtocol(protocolId, enabled)), ), ); @@ -1611,10 +1701,11 @@ const projectUpdateServiceCommand = project .description(`Update properties of a specific service. Use this endpoint to enable or disable a service in your project. `) .requiredOption(`--service-id `, `Service name. Can be one of: account, avatars, databases, tablesdb, locale, health, project, storage, teams, users, vcs, sites, functions, proxy, graphql, migrations, messaging, advisor, oauth2`) .requiredOption(`--enabled `, `Service status.`, parseBool) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ serviceId, enabled }) => - parse(await (await getProjectClient()).updateService(serviceId, enabled)), + async ({ serviceId, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateService(serviceId, enabled)), ), ); @@ -1637,10 +1728,11 @@ const projectUpdateSMTPCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ host, port, username, password, senderEmail, senderName, replyToEmail, replyToName, secure, enabled }) => - parse(await (await getProjectClient()).updateSMTP(host, port, username, password, senderEmail, senderName, replyToEmail, replyToName, secure, enabled)), + async ({ host, port, username, password, senderEmail, senderName, replyToEmail, replyToName, secure, enabled, projectId }) => + parse(await (await getProjectClient(projectId)).updateSMTP(host, port, username, password, senderEmail, senderName, replyToEmail, replyToName, secure, enabled)), ), ); @@ -1649,10 +1741,11 @@ const projectCreateSMTPTestCommand = project .command(`create-smtp-test`) .description(`Send a test email to verify SMTP configuration. `) .requiredOption(`--emails [emails...]`, `Array of emails to send test email to. Maximum of 10 emails are allowed.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ emails }) => - parse(await (await getProjectClient()).createSMTPTest(emails)), + async ({ emails, projectId }) => + parse(await (await getProjectClient(projectId)).createSMTPTest(emails)), ), ); @@ -1669,10 +1762,11 @@ const projectListEmailTemplatesCommand = project ) .option(`--limit `, `Maximum number of results to return.`, parseInteger) .option(`--offset `, `Number of results to skip.`, parseInteger) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, limit, offset }) => - parse(await (await getProjectClient()).listEmailTemplates(buildQueries({ queries, limit, offset }), total)), + async ({ queries, total, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listEmailTemplates(buildQueries({ queries, limit, offset }), total)), ), ); @@ -1688,10 +1782,11 @@ const projectUpdateEmailTemplateCommand = project .option(`--sender-email `, `Email of the sender. Pass an empty string to clear a previously set value.`) .option(`--reply-to-email `, `Reply to email. Pass an empty string to clear a previously set value.`) .option(`--reply-to-name `, `Reply to name.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ templateId, locale, subject, message, senderName, senderEmail, replyToEmail, replyToName }) => - parse(await (await getProjectClient()).updateEmailTemplate(templateId, locale, subject, message, senderName, senderEmail, replyToEmail, replyToName)), + async ({ templateId, locale, subject, message, senderName, senderEmail, replyToEmail, replyToName, projectId }) => + parse(await (await getProjectClient(projectId)).updateEmailTemplate(templateId, locale, subject, message, senderName, senderEmail, replyToEmail, replyToName)), ), ); @@ -1701,10 +1796,11 @@ const projectGetEmailTemplateCommand = project .description(`Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.`) .requiredOption(`--template-id `, `Custom email template type. Can be one of: verification, magicSession, recovery, invitation, mfaChallenge, sessionAlert, otpSession`) .option(`--locale `, `Custom email template locale. If left empty, the fallback locale (en) will be used.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ templateId, locale }) => - parse(await (await getProjectClient()).getEmailTemplate(templateId, locale)), + async ({ templateId, locale, projectId }) => + parse(await (await getProjectClient(projectId)).getEmailTemplate(templateId, locale)), ), ); @@ -1715,10 +1811,11 @@ const projectGetUsageCommand = project .requiredOption(`--start-date `, `Starting date for the usage`) .requiredOption(`--end-date `, `End date for the usage`) .option(`--period `, `Period used`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ startDate, endDate, period }) => - parse(await (await getProjectClient()).getUsage(startDate, endDate, period)), + async ({ startDate, endDate, period, projectId }) => + parse(await (await getProjectClient(projectId)).getUsage(startDate, endDate, period)), ), ); @@ -1741,10 +1838,11 @@ const projectListVariablesCommand = project .option(`--offset `, `Number of results to skip.`, parseInteger) .option(`--cursor-after `, `Return results after this cursor ID.`) .option(`--cursor-before `, `Return results before this cursor ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }) => - parse(await (await getProjectClient()).listVariables(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), + async ({ queries, total, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset, projectId }) => + parse(await (await getProjectClient(projectId)).listVariables(buildQueries({ queries, filter, where, sortAsc, sortDesc, cursorAfter, cursorBefore, limit, offset }), total)), ), ); @@ -1761,10 +1859,11 @@ const projectCreateVariableCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ variableId, key, value, secret }) => - parse(await (await getProjectClient()).createVariable(variableId, key, value, secret)), + async ({ variableId, key, value, secret, projectId }) => + parse(await (await getProjectClient(projectId)).createVariable(variableId, key, value, secret)), ), ); @@ -1773,10 +1872,11 @@ const projectGetVariableCommand = project .command(`get-variable`) .description(`Get a variable by its unique ID. `) .requiredOption(`--variable-id `, `Variable unique ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ variableId }) => - parse(await (await getProjectClient()).getVariable(variableId)), + async ({ variableId, projectId }) => + parse(await (await getProjectClient(projectId)).getVariable(variableId)), ), ); @@ -1793,10 +1893,11 @@ const projectUpdateVariableCommand = project (value: string | undefined) => value === undefined ? true : parseBool(value), ) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ variableId, key, value, secret }) => - parse(await (await getProjectClient()).updateVariable(variableId, key, value, secret)), + async ({ variableId, key, value, secret, projectId }) => + parse(await (await getProjectClient(projectId)).updateVariable(variableId, key, value, secret)), ), ); @@ -1805,10 +1906,11 @@ const projectDeleteVariableCommand = project .command(`delete-variable`) .description(`Delete a variable by its unique ID. `) .requiredOption(`--variable-id `, `Variable unique ID.`) + .option(`--project-id `, `Project to act on. Defaults to the project linked in appwrite.config.json.`) .action( actionRunner( - async ({ variableId }) => - parse(await (await getProjectClient()).deleteVariable(variableId)), + async ({ variableId, projectId }) => + parse(await (await getProjectClient(projectId)).deleteVariable(variableId)), ), ); diff --git a/lib/commands/services/teams.ts b/lib/commands/services/teams.ts index 95fff81..4365262 100644 --- a/lib/commands/services/teams.ts +++ b/lib/commands/services/teams.ts @@ -27,7 +27,7 @@ const getTeamsClient = async (): Promise => { sdkClient = await sdkForProject(); } catch (e) { if (e instanceof Error && e.message.includes("Project is not set")) { - hint(`To manage console-level teams, use the 'organizations' command instead.`); + hint(`To manage organization members, use the 'organization' command instead.`); } throw e; } diff --git a/lib/config-filters.ts b/lib/config-filters.ts deleted file mode 100644 index ad83fe7..0000000 --- a/lib/config-filters.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Client } from "@appwrite.io/console"; -import { warn } from "./parser.js"; - -type ConfigFilterContext = { - config: { - organizationId?: string; - projectId?: string; - }; - consoleClient: Client; -}; - -type ConfigFilter = { - id: string; - matches(context: ConfigFilterContext): boolean; - apply(context: ConfigFilterContext): Promise; -}; - -const warned = new Set(); - -const configFilters: ConfigFilter[] = [ - { - id: "organization-id-header", - matches({ config, consoleClient }) { - return ( - !consoleClient.headers["X-Appwrite-Organization"] && - (!!config.organizationId || !!config.projectId) - ); - }, - async apply({ config, consoleClient }) { - let organizationId = config.organizationId; - - if (!organizationId) { - if (!config.projectId) { - throw new Error( - "Project configuration not found. Please run 'appwrite init project' to initialize your project first.", - ); - } - - const project = await consoleClient.call( - "get", - new URL( - `${consoleClient.config.endpoint}/projects/${encodeURIComponent(config.projectId)}`, - ), - { - "X-Appwrite-Project": "console", - }, - {}, - ); - - organizationId = project.teamId; - - if (!organizationId || typeof organizationId !== "string") { - throw new Error( - "Unable to resolve organization for this project. Please run 'appwrite init project' to relink this project.", - ); - } - - if (!warned.has(this.id)) { - warned.add(this.id); - warn( - "Your appwrite.config.json is missing organizationId. The CLI resolved it for this command without changing your config. Run 'appwrite init project' to relink and persist it.", - ); - } - } - - consoleClient.headers["X-Appwrite-Organization"] = organizationId; - }, - }, -]; - -export const applyConfigFilters = async ( - context: ConfigFilterContext, -): Promise => { - for (const filter of configFilters) { - if (filter.matches(context)) { - await filter.apply(context); - } - } -}; diff --git a/lib/constants.ts b/lib/constants.ts index 47c9d4d..b98330e 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.1.0'; +export const SDK_VERSION = '25.0.0'; export const SDK_NAME = 'Command Line'; export const SDK_PLATFORM = 'console'; export const SDK_LANGUAGE = 'cli'; diff --git a/lib/context.ts b/lib/context.ts new file mode 100644 index 0000000..3e5acb3 --- /dev/null +++ b/lib/context.ts @@ -0,0 +1,109 @@ +import { Client } from "@appwrite.io/console"; +import { globalConfig, localConfig } from "./config.js"; +import { warn } from "./parser.js"; +import { EXECUTABLE_NAME } from "./constants.js"; + +/** + * Resolves the resource a command acts on. + * + * The `/project` and `/organization` endpoints carry no ID in their path — they + * act on whatever `X-Appwrite-Project` or `X-Appwrite-Organization` names — so + * the CLI has to decide which value to send. Precedence is the same for both: + * + * --project-id / --organization-id -> environment -> appwrite.config.json + * + * Every caller resolves through here, so an ID passed on the command line + * cannot apply to some commands and be silently ignored by others. + */ + +const ENV_PROJECT_ID = "APPWRITE_PROJECT_ID"; +const ENV_ORGANIZATION_ID = "APPWRITE_ORGANIZATION_ID"; + +export const resolveProjectId = (override?: string): string => + override || + process.env[ENV_PROJECT_ID] || + localConfig.getProject().projectId || + globalConfig.getProject() || + ""; + +/** + * The organization known without contacting the API: environment, then + * appwrite.config.json. Empty when it could only be derived from the project, + * so callers that must not perform I/O — `client --debug` — can report what is + * configured without triggering a lookup. + */ +export const configuredOrganizationId = (): string => + process.env[ENV_ORGANIZATION_ID] || + localConfig.getProject().organizationId || + ""; + +let derivedOrganizationWarned = false; + +/** + * Look up the organization that owns a project. + * + * `GET /projects/{projectId}` is not published in the spec, so there is no + * generated service method for it and the request has to be issued by hand. It + * must set `X-Appwrite-Project` itself — without it the API treats the call as + * a guest request and rejects it. Keeping the one raw call here means the rest + * of the CLI never repeats it. + */ +const fetchOrganizationForProject = async ( + consoleClient: Client, + projectId: string, +): Promise => { + const project = await consoleClient.call( + "get", + new URL( + `${consoleClient.config.endpoint}/projects/${encodeURIComponent(projectId)}`, + ), + { "X-Appwrite-Project": "console" }, + {}, + ); + + const organizationId = project?.teamId; + + if (!organizationId || typeof organizationId !== "string") { + throw new Error( + `Unable to resolve the organization for project ${projectId}. Pass --organization-id , or run \`${EXECUTABLE_NAME} init project\` to relink this directory.`, + ); + } + + return organizationId; +}; + +export const resolveOrganizationId = async ({ + override, + consoleClient, +}: { + override?: string; + consoleClient?: Client; +} = {}): Promise => { + const direct = override || configuredOrganizationId(); + + if (direct) { + return direct; + } + + const projectId = resolveProjectId(); + + if (!projectId || !consoleClient) { + throw new Error( + `Organization is not set. Pass --organization-id , or run \`${EXECUTABLE_NAME} init project\` to link this directory to a project.`, + ); + } + + const organizationId = await fetchOrganizationForProject( + consoleClient, + projectId, + ); + + if (!derivedOrganizationWarned) { + derivedOrganizationWarned = true; + warn( + `Resolved the organization for this command from project ${projectId}. Run \`${EXECUTABLE_NAME} init project\` to persist organizationId in ${EXECUTABLE_NAME}.config.json.`, + ); + } + + return organizationId; +}; diff --git a/lib/help.ts b/lib/help.ts new file mode 100644 index 0000000..d328013 --- /dev/null +++ b/lib/help.ts @@ -0,0 +1,287 @@ +import chalk from "chalk"; +import type { Command, Help } from "commander"; +import { EXECUTABLE_NAME, SDK_LOGO, SDK_TITLE } from "./constants.js"; + +/** + * The main help screen is grouped by intent rather than listed alphabetically. + * Entries are command paths as typed, so `oauth2 list-projects` can be + * surfaced next to `login` without moving it out of the oauth2 service. + * + * Anything not named here still shows up, under `OTHER`, so a service added + * to the spec can never silently disappear from `--help`. + */ +const groups: ReadonlyArray<{ + title: string; + commands: readonly string[]; + dim?: boolean; +}> = [ + { + title: "GET STARTED", + commands: [ + "login", + "oauth2 list-organizations", + "oauth2 list-projects", + "init", + "pull", + "push", + "run", + "whoami", + ], + }, + { + title: "PROJECT", + commands: ["organization", "project", "apps", "proxy", "vcs", "webhooks"], + }, + { + title: "RESOURCES", + commands: [ + "account", + "users", + "teams", + "tablesdb", + "storage", + "functions", + "sites", + "messaging", + "tokens", + "backups", + "presences", + ], + }, + { + title: "UTILITIES", + commands: [ + "graphql", + "generate", + "types", + "locale", + "activities", + "migrations", + "notifications", + "oauth2", + "client", + "completion", + "logout", + "update", + ], + }, + { + title: "DEPRECATED", + dim: true, + commands: ["databases"], + }, +]; + +/** + * One-line summaries for the listing. `.description()` stays the long form + * shown on a command's own help page. + * + * Written to fit one terminal line — keep them under 51 characters, in the + * imperative, with no trailing period. + */ +const summaries: Record = { + login: `Authenticate with your ${SDK_TITLE} account`, + "oauth2 list-organizations": "Organizations your session can access", + "oauth2 list-projects": "Projects your session can access", + init: "Scaffold a project, function, site, or resource", + pull: "Pull remote project resources into this directory", + push: "Push local project resources", + run: "Run the project locally for development", + whoami: "Show the currently authenticated account", + + organization: "Manage organization-level projects", + project: "Usage, variables, and project-level settings", + apps: "OAuth2 applications, keys, scopes, installations", + proxy: "Domain configuration beyond DNS", + vcs: "Connect and manage VCS repositories", + webhooks: "Project webhooks", + + account: "Manage your own user account", + users: "Manage project users", + teams: "Group users to share resource access", + tablesdb: "Structured tables of rows and columns", + storage: "Files and buckets", + functions: "Serverless functions, deployments, and executions", + sites: "Static and SSR sites and their deployments", + messaging: "Topics, subscribers, and message delivery", + tokens: "Resource tokens for secure file access", + backups: "Backup policies, archives, and restorations", + presences: "Real-time user presence tracking", + + graphql: "Query and mutate any resource via GraphQL", + generate: "Generate a type-safe SDK from your project config", + types: "Generate TypeScript types for your project", + locale: "Localize your app based on user location", + activities: "List and inspect project activity events", + migrations: "Migrate data between services", + notifications: "Console notifications", + oauth2: "Authorize apps and issue OAuth2 and OIDC tokens", + client: "Configure the CLI itself", + completion: "Generate shell completion scripts", + logout: `Log out of your ${SDK_TITLE} account`, + update: "Update the CLI to the latest version", + + databases: "Use `tablesdb` instead", +}; + +/** Order of the global flags, by long flag. Unlisted options are appended. */ +const optionOrder: readonly string[] = [ + "--version", + "--help", + "--json", + "--raw", + "--show-secrets", + "--verbose", + "--force", + "--all", + "--id", + "--report", +]; + +const MAX_WIDTH = 80; +const GAP = 2; +const INDENT = " "; + +type Row = { name: string; summary: string }; + +const isListed = (command: Command): boolean => + command.name() !== "help" && + !(command as Command & { _hidden?: boolean })._hidden; + +/** Resolve a space-separated command path against the tree, or null. */ +const resolve = (root: Command, path: string): Command | null => { + let current: Command | undefined = root; + + for (const segment of path.split(" ")) { + current = current?.commands.find( + (candidate) => + candidate.name() === segment || candidate.aliases().includes(segment), + ); + + if (!current) { + return null; + } + } + + return current ?? null; +}; + +/** + * Fall back to the first sentence of the description so a command with no + * summary still reads as one line rather than a paragraph. + */ +const summaryOf = (command: Command, path: string): string => { + const declared = summaries[path] ?? command.summary(); + + if (declared) { + return declared; + } + + const [sentence] = command.description().split(". "); + + return (sentence ?? "").replace(/\.$/, ""); +}; + +const renderRows = ( + rows: readonly Row[], + width: number, + dim: boolean, +): string => + rows + .map(({ name, summary }) => { + const line = `${INDENT}${name.padEnd(width)}${" ".repeat(GAP)}${summary}`; + + return dim ? chalk.dim(line) : line; + }) + .join("\n"); + +const renderOptions = (command: Command, helper: Help): string => { + const options = [...helper.visibleOptions(command)].sort((left, right) => { + const rank = (flag: string): number => { + const index = optionOrder.indexOf(flag); + + return index === -1 ? optionOrder.length : index; + }; + + return rank(left.long ?? "") - rank(right.long ?? ""); + }); + + const width = Math.max( + ...options.map((option) => helper.optionTerm(option).length), + ); + + return options + .map( + (option) => + `${INDENT}${helper.optionTerm(option).padEnd(width)}${" ".repeat(GAP)}${helper.optionDescription(option)}`, + ) + .join("\n"); +}; + +export const formatMainHelp = (command: Command, helper: Help): string => { + const width = Math.min(process.stdout.columns || MAX_WIDTH, MAX_WIDTH); + + const sections = groups + .map((group) => ({ + ...group, + rows: group.commands.flatMap((path) => { + const child = resolve(command, path); + + return child && isListed(child) + ? [{ name: path, summary: summaryOf(child, path) }] + : []; + }), + })) + .filter((group) => group.rows.length > 0); + + const claimed = new Set(groups.flatMap((group) => group.commands)); + + // Via the helper so this inherits commander's hidden-command filtering and + // the configured subcommand sort, rather than raw declaration order. + const other = helper + .visibleCommands(command) + .filter((child) => isListed(child) && !claimed.has(child.name())) + .map((child) => ({ + name: child.name(), + summary: summaryOf(child, child.name()), + })); + + if (other.length > 0) { + sections.push({ title: "OTHER", commands: [], rows: other }); + } + + const nameWidth = Math.max( + 0, + ...sections.flatMap((group) => group.rows.map((row) => row.name.length)), + ); + + const output = [ + chalk.redBright(SDK_LOGO.replace(/\n+$/, "")), + "", + INDENT + helper.wrap(command.description(), width - 2, 2, 2), + "", + chalk.bold("USAGE"), + `${INDENT}${EXECUTABLE_NAME} [options] [subcommand]`, + ]; + + for (const group of sections) { + output.push( + "", + chalk.bold(group.title), + renderRows(group.rows, nameWidth, group.dim === true), + ); + } + + output.push( + "", + chalk.bold("OPTIONS"), + renderOptions(command, helper), + "", + chalk.dim( + `Run \`${EXECUTABLE_NAME} --help\` for details on a specific command.`, + ), + "", + ); + + return output.join("\n"); +}; diff --git a/lib/hints.ts b/lib/hints.ts new file mode 100644 index 0000000..d8ee106 --- /dev/null +++ b/lib/hints.ts @@ -0,0 +1,27 @@ +import type { Command } from "commander"; +import { EXECUTABLE_NAME } from "./constants.js"; + +/** + * Commands whose response carries identifiers but no detail, mapped to the + * command that shows the full resource. Keys are the command path as typed, + * so they can be checked against `--help` output directly. + */ +const followUpHints: Record = { + "oauth2 list-projects": `Run \`${EXECUTABLE_NAME} project get --project-id \` to see a project's details.`, + "oauth2 list-organizations": `Run \`${EXECUTABLE_NAME} organization get --organization-id \` to see an organization's details.`, +}; + +/** Command path without the executable name, e.g. `oauth2 list-projects`. */ +const commandPath = (command: Command): string => { + const segments: string[] = []; + + for (let current: Command | null = command; current?.parent;) { + segments.unshift(current.name()); + current = current.parent; + } + + return segments.join(" "); +}; + +export const followUpHintFor = (command: Command): string => + followUpHints[commandPath(command)] ?? ""; diff --git a/lib/parser.ts b/lib/parser.ts index 03227a9..c64df65 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -20,7 +20,13 @@ import { SDK_LOGO, EXECUTABLE_NAME, } from "./constants.js"; -import { renderStructuredCollection } from "./response-config.js"; +import { + formatSectionField, + formatTimestamp, + humanizeSeconds, + renderStructuredCollection, + sectionFieldKeys, +} from "./response-config.js"; const cliConfig: CliConfig = { verbose: false, @@ -33,6 +39,7 @@ const cliConfig: CliConfig = { report: false, reportData: {}, displayFields: [], + followUpHint: "", }; type JsonObject = Record; @@ -67,8 +74,14 @@ const toJsonObject = (value: unknown): JsonObject | null => { return null; }; +/** + * Internal bookkeeping that carries no meaning for a CLI reader, plus fields + * the API returns twice under two names (`billingPlanId` === `billingPlan`). + */ +const NORMAL_VIEW_HIDDEN_KEYS = new Set(["onboarding", "billingPlanId"]); + const isNormalViewHiddenKey = (key: string): boolean => - key.startsWith("$") && key !== "$id"; + (key.startsWith("$") && key !== "$id") || NORMAL_VIEW_HIDDEN_KEYS.has(key); const printSpacerLine = (): void => { process.stdout.write(" \n"); }; @@ -113,6 +126,17 @@ const endRender = (): void => { hint(message); } } + + // Machine-readable output stays free of prose. Cleared once shown so commands + // that render more than once do not repeat themselves. + if (renderDepth === 0 && cliConfig.followUpHint !== "") { + const message = cliConfig.followUpHint; + cliConfig.followUpHint = ""; + + if (!cliConfig.json && !cliConfig.raw) { + hint(message); + } + } }; const withRender = (callback: () => T): T => { @@ -134,8 +158,10 @@ const isSensitiveKey = (key: string): boolean => { ); }; -const maskSensitiveString = (value: string): string => { - if (value.length <= 16) { +const maskSensitiveString = (value: string, key: string): string => { + // A key or token tail helps identify which credential is in play; a password + // tail is just a leak, so those are masked whole. + if (value.length <= 16 || /password/i.test(key)) { return HIDDEN_VALUE; } @@ -160,7 +186,7 @@ const maskSensitiveData = ( } if (typeof value === "string") { - return maskSensitiveString(value); + return maskSensitiveString(value, key); } if (value == null) { @@ -357,6 +383,7 @@ export const parse = (data: unknown): void => { drawTable([section.value as JsonObject], { indent: " ", sectionName: section.key, + keyValue: true, }); } @@ -398,6 +425,8 @@ const truncateToVisibleWidth = (str: string, max: number): string => { type NamedTableOptions = { indent?: string; sectionName?: string; + /** Render as stacked key/value lines rather than a column table. */ + keyValue?: boolean; }; type EntryRenderOptions = { @@ -442,7 +471,26 @@ const formatCellValue = (value: unknown): string => { const formatKeyValue = (key: string, value: unknown): string => { if (key === "status" && typeof value === "boolean") { - return value ? "active" : "inactive"; + return value ? chalk.green("active") : chalk.dim("inactive"); + } + + if (typeof value === "boolean") { + return value ? chalk.green("true") : chalk.dim("false"); + } + + // Durations come over the wire as raw seconds, which nobody reads at a glance. + if (typeof value === "number" && /duration$/i.test(key)) { + const humanized = humanizeSeconds(value); + if (humanized !== "") { + return `${value} ${chalk.dim(`(${humanized})`)}`; + } + } + + if (typeof value === "string") { + const timestamp = formatTimestamp(value); + if (timestamp !== null) { + return timestamp; + } } return String(value); @@ -524,14 +572,19 @@ const drawKeyValueEntries = ( } }; +const printWithheldFieldNote = (count: number, indent?: string): void => { + if (count <= 0) return; + + const label = count === 1 ? "field" : "fields"; + console.log( + `${indent ?? ""}${chalk.dim(`… ${count} more ${label} — pass --raw to show all`)}`, + ); +}; + const drawNamedObjectCollection = ( rows: JsonObject[], options: NamedTableOptions = {}, ): boolean => { - if (renderStructuredCollection(options.sectionName, rows, options)) { - return true; - } - const scalarEntries = rows.map((row) => toScalarEntries(row)); const flatScalarEntries = scalarEntries.flat(); @@ -589,7 +642,7 @@ export const drawTable = ( return; } - const rows = applyDisplayFilter( + const visibleRows = applyDisplayFilter( data.map((item): JsonObject => { const maskedItem = maskSensitiveData(item, undefined, false); const row = toJsonObject(maskedItem) ?? {}; @@ -604,6 +657,43 @@ export const drawTable = ( }), ); + // An explicit --display selection wins; the reader already said what they want. + const allowlist = + cliConfig.displayFields.length > 0 + ? undefined + : sectionFieldKeys(options.sectionName); + let withheldFields = 0; + + const rows = allowlist + ? visibleRows.map((row) => { + const kept: JsonObject = {}; + + for (const key of allowlist) { + if (Object.prototype.hasOwnProperty.call(row, key)) { + kept[key] = formatSectionField( + options.sectionName, + key, + row[key], + ); + } + } + + withheldFields += Object.keys(row).length - Object.keys(kept).length; + + return kept; + }) + : visibleRows; + + if (renderStructuredCollection(options.sectionName, rows, options)) { + printWithheldFieldNote(withheldFields, options.indent); + return; + } + + if (options.keyValue && drawNamedObjectCollection(rows, options)) { + printWithheldFieldNote(withheldFields, options.indent); + return; + } + // Create an object with all the keys in it const obj = rows.reduce((res, item) => ({ ...res, ...item }), {}); // Get those keys as an array @@ -616,6 +706,7 @@ export const drawTable = ( // If too many columns, show condensed key-value output with only scalar, non-empty fields if (allKeys.length > MAX_COLUMNS) { if (drawNamedObjectCollection(rows, options)) { + printWithheldFieldNote(withheldFields, options.indent); return; } @@ -667,6 +758,7 @@ export const drawTable = ( table.push(rowValues); }); console.log(table.toString()); + printWithheldFieldNote(withheldFields, options.indent); }); }; @@ -915,7 +1007,7 @@ export const commandDescriptions: Record = { locale: `The locale command allows you to customize your app based on your users' location.`, sites: `The sites command allows you to view, create and manage your Appwrite Sites.`, storage: `The storage command allows you to manage your project files.`, - teams: `The teams command allows you to group users of your project to enable them to share read and write access to your project resources. Requires a linked project. To manage console-level teams, use the 'organizations' command instead.`, + teams: `The teams command allows you to group users of your project to enable them to share read and write access to your project resources. Requires a linked project. To manage organization members, use the 'organization' command instead.`, update: `The update command allows you to update the ${SDK_TITLE} CLI to the latest version.`, users: `The users command allows you to manage your project users.`, projects: `The projects command allows you to manage your projects, add platforms, manage API keys, Dev Keys etc.`, @@ -930,14 +1022,16 @@ export const commandDescriptions: Record = { messaging: `The messaging command allows you to manage topics and targets and send messages.`, migrations: `The migrations command allows you to migrate data between services.`, notifications: `The notifications command allows you to read and manage your Appwrite Console notifications.`, - oauth2: `The oauth2 command allows you to authorize apps and issue standards-based OAuth2 and OpenID Connect tokens.`, + oauth2: `The oauth2 command allows you to authorize apps and issue standards-based OAuth2 and OpenID Connect tokens. The 'list-organizations' and 'list-projects' commands are console-level and report the organizations and projects your current session can access.`, organization: `The organization command allows you to manage organization-level projects.`, organizations: `The organizations command allows you to manage organization billing, plans, invoices, and add-ons.`, presences: `The presences command allows you to track and manage real-time user presence in your project.`, tokens: `The tokens command allows you to create and manage resource tokens for secure file access.`, vcs: `The vcs command allows you to interact with VCS providers and manage your code repositories.`, webhooks: `The webhooks command allows you to manage your project webhooks.`, - main: chalk.redBright(`${logo}${description}`), + // The logo is rendered by the help formatter, so this stays plain prose and + // can be reused wherever the CLI needs a one-paragraph description. + main: description, }; export { cliConfig }; diff --git a/lib/questions.ts b/lib/questions.ts index fcd8888..949d677 100644 --- a/lib/questions.ts +++ b/lib/questions.ts @@ -7,6 +7,8 @@ import { validateRequired } from "./validations.js"; import { paginate } from "./paginate.js"; import { checkDeployConditions, + formatAccountList, + getConsoleBaseUrl, getSafeDirectoryName, isCloud, } from "./utils.js"; @@ -270,7 +272,7 @@ export const questionsInitProject: Question[] = [ if (choices.length == 0) { throw new Error( - `No organizations found. Please create a new organization at ${globalConfig.getEndpoint().replace("/v1", "/console/onboarding")}`, + `No organizations found. Please create a new organization at ${getConsoleBaseUrl(globalConfig.getEndpoint())}/console/onboarding`, ); } @@ -345,10 +347,9 @@ export const questionsInitProject: Question[] = [ message: `Select your ${SDK_TITLE} Cloud region`, choices: async () => { const client = await sdkForConsole({ requiresAuth: true }); - const endpoint = globalConfig.getEndpoint() || DEFAULT_ENDPOINT; const response = (await client.call( "GET", - new URL(endpoint + "/console/regions"), + new URL(client.config.endpoint + "/console/regions"), )) as { regions: any[] }; const regions = response.regions || []; if (!regions.length) { @@ -944,13 +945,7 @@ export const questionsClientReset = ( { type: "confirm", name: "confirm", - message: `This will sign out ${accounts - .map((account) => - account.endpoint - ? `${account.email} (${account.endpoint})` - : account.email, - ) - .join(", ")}. Continue?`, + message: `This will sign out:\n${formatAccountList(accounts)}\nContinue?`, default: false, }, ]; diff --git a/lib/response-config.ts b/lib/response-config.ts index d9a4143..d26f459 100644 --- a/lib/response-config.ts +++ b/lib/response-config.ts @@ -33,6 +33,8 @@ const toTitleCase = (value: string): string => .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join(" "); +const COLUMN_GAP = " "; + const padColumn = (value: string, width: number): string => { const valueWidth = stringWidth(value); if (valueWidth >= width) return value; @@ -65,7 +67,7 @@ const renderAlignedColumns = ( return padColumn(value, widths[columnIndex]); }); - console.log(`${indent}${headerParts.join(" ")}`.trimEnd()); + console.log(`${indent}${headerParts.join(COLUMN_GAP)}`.trimEnd()); } for (let idx = 0; idx < columns[0].length; idx++) { @@ -78,8 +80,154 @@ const renderAlignedColumns = ( return padColumn(value, widths[columnIndex]); }); - console.log(`${indent}${parts.join(" ")}`.trimEnd()); + console.log(`${indent}${parts.join(COLUMN_GAP)}`.trimEnd()); + } +}; + +const wrapValues = (values: string[], width: number): string[] => { + const lines: string[] = []; + let current = ""; + + values.forEach((value, index) => { + const piece = index === values.length - 1 ? value : `${value},`; + + if (current === "") { + current = piece; + return; + } + + if (stringWidth(`${current} ${piece}`) > width) { + lines.push(current); + current = piece; + return; + } + + current = `${current} ${piece}`; + }); + + if (current !== "") { + lines.push(current); } + + return lines; +}; + +/** Plan quotas are quoted in decimal units, matching how the console reads them. */ +const SIZE_UNITS = ["MB", "GB", "TB", "PB"] as const; +type SizeUnit = (typeof SIZE_UNITS)[number]; + +const trimTrailingZeros = (value: number): string => + String(Number(value.toFixed(2))); + +const formatSize = (amount: number, unit: SizeUnit): string => { + const base = `${amount} ${unit}`; + let index = SIZE_UNITS.indexOf(unit); + let scaled = amount; + + while (scaled >= 1000 && index < SIZE_UNITS.length - 1) { + scaled /= 1000; + index++; + } + + if (SIZE_UNITS[index] === unit) { + return base; + } + + return `${base} ${chalk.dim(`(${trimTrailingZeros(scaled)} ${SIZE_UNITS[index]})`)}`; +}; + +const compactNumber = new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 2, +}); + +const formatCount = (amount: number): string => { + if (Math.abs(amount) < 10000) { + return String(amount); + } + + return `${amount} ${chalk.dim(`(${compactNumber.format(amount)})`)}`; +}; + +export const humanizeSeconds = (seconds: number): string => { + if (!Number.isFinite(seconds) || seconds <= 0) { + return ""; + } + + const units: Array<[string, number]> = [ + ["d", 86400], + ["h", 3600], + ["m", 60], + ["s", 1], + ]; + + const parts: string[] = []; + let remaining = Math.round(seconds); + + for (const [suffix, size] of units) { + const amount = Math.floor(remaining / size); + if (amount > 0) { + parts.push(`${amount}${suffix}`); + remaining -= amount * size; + } + } + + return parts.slice(0, 2).join(" "); +}; + +/** + * The offset is mandatory: ECMAScript reads an offset-less date-time as local + * time, so accepting one would mean labelling a local instant UTC. Values + * without an offset fall through and render as they arrived. + */ +const ISO_DATE_TIME = + /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$/; + +/** Coarsest-unit-wins tiers; approximate on purpose, this is a readability aid. */ +const RELATIVE_TIERS: Array<[string, number]> = [ + ["y", 31536000], + ["mo", 2592000], + ["d", 86400], + ["h", 3600], + ["m", 60], +]; + +const relativeTime = (date: Date): string => { + const deltaSeconds = (Date.now() - date.getTime()) / 1000; + const magnitude = Math.abs(deltaSeconds); + + if (magnitude < 45) { + return "just now"; + } + + const [suffix, size] = + RELATIVE_TIERS.find(([, tierSize]) => magnitude >= tierSize) ?? + RELATIVE_TIERS[RELATIVE_TIERS.length - 1]; + const label = `${Math.floor(magnitude / size)}${suffix}`; + + return deltaSeconds > 0 ? `${label} ago` : `in ${label}`; +}; + +/** + * Turns `2026-07-31T02:49:41.895+00:00` into `2026-07-31 02:49:41 UTC (2h ago)`. + * Returns null when the value is not an ISO timestamp, so callers can fall back. + */ +export const formatTimestamp = (value: string): string | null => { + const match = ISO_DATE_TIME.exec(value.trim()); + if (!match) { + return null; + } + + const [, date, time, offset] = match; + const zone = offset === "Z" || offset === "+00:00" ? " UTC" : ` ${offset}`; + const stamp = `${date} ${time}${zone}`; + const parsed = new Date(value); + + if (Number.isNaN(parsed.getTime())) { + return stamp; + } + + return `${stamp} ${chalk.dim(`(${relativeTime(parsed)})`)}`; }; const compactDate = (value: unknown): string => { @@ -603,26 +751,179 @@ const structuredCollectionRenderers: Record< ]), }; -export const renderStructuredCollection = ( +/** + * How a field's bare number should be annotated. Units belong to the section, + * not to the key: a plan's `fileSize` is megabytes, a bucket's is bytes. + */ +type FieldFormat = + /** Decimal size in the given unit, scaled up when that reads better. */ + | { kind: "size"; unit: SizeUnit } + /** Large tallies get a compact magnitude hint: 3500000 (3.5M). */ + | { kind: "count" } + /** A literal trailing label, e.g. `168 days`. */ + | { kind: "label"; label: string }; + +type SectionField = { + key: string; + format?: FieldFormat; +}; + +/** + * Embedded models that arrive with far more fields than a reader asked for. + * These are allowlists rather than denylists on purpose: the API keeps adding + * capability flags, and an allowlist stays correct without maintenance. Order + * here is the render order; anything absent is summarised as a footer count. + * + * Units are verified against how the console interprets the same plan fields — + * `bandwidth`/`storage` in GB, `fileSize` in MB. + */ +const sectionFields: Record = { + billingPlanDetails: [ + { key: "$id" }, + { key: "name" }, + { key: "group" }, + { key: "price" }, + { key: "currency" }, + { key: "trial", format: { kind: "label", label: "days" } }, + { key: "bandwidth", format: { kind: "size", unit: "GB" } }, + { key: "storage", format: { kind: "size", unit: "GB" } }, + { key: "fileSize", format: { kind: "size", unit: "MB" } }, + { key: "users", format: { kind: "count" } }, + { key: "executions", format: { kind: "count" } }, + { key: "GBHours", format: { kind: "label", label: "GB-hours" } }, + { key: "databasesReads", format: { kind: "count" } }, + { key: "databasesWrites", format: { kind: "count" } }, + { key: "realtime", format: { kind: "count" } }, + { key: "realtimeMessages", format: { kind: "count" } }, + { key: "messages", format: { kind: "count" } }, + { key: "domains", format: { kind: "count" } }, + ], +}; + +export const sectionFieldKeys = ( + sectionName: string | undefined, +): string[] | undefined => + sectionName + ? sectionFields[sectionName]?.map((field) => field.key) + : undefined; + +export const formatSectionField = ( sectionName: string | undefined, + key: string, + value: unknown, +): unknown => { + if (typeof value !== "number" || !Number.isFinite(value)) { + return value; + } + + const format = sectionName + ? sectionFields[sectionName]?.find((field) => field.key === key)?.format + : undefined; + + if (!format) { + return value; + } + + switch (format.kind) { + case "size": + return formatSize(value, format.unit); + case "count": + return formatCount(value); + case "label": + return `${value} ${format.label}`; + } +}; + +/** + * Rows that carry nothing but a name and an on/off switch — `authMethods`, + * `services`, `protocols` and friends. A two column table wastes a screen on + * them, so they collapse into wrapped enabled/disabled lists instead. + */ +const ToggleRowSchema = z + .strictObject({ + $id: z.string().optional(), + name: z.string().optional(), + key: z.string().optional(), + enabled: z.boolean(), + }) + .refine( + (row) => isPresent(row.$id) || isPresent(row.key) || isPresent(row.name), + { message: "Expected a toggle row with a name" }, + ); + +const toggleLabel = (row: JsonObject): string => + compactText( + valueFrom(row, "$id") ?? valueFrom(row, "key") ?? valueFrom(row, "name"), + ); + +const renderToggleCollection = ( rows: JsonObject[], options: StructuredCollectionRenderOptions = {}, ): boolean => { - if (!sectionName) { + if (rows.length === 0) { return false; } - const renderer = structuredCollectionRenderers[sectionName]; - if (!renderer) { + if (!rows.every((row) => ToggleRowSchema.safeParse(row).success)) { return false; } + const groups: Array<[string, string[], (value: string) => string]> = [ + ["enabled", [], chalk.green], + ["disabled", [], chalk.dim], + ]; + + for (const row of rows) { + groups[row.enabled === true ? 0 : 1][1].push(toggleLabel(row)); + } + + const populated = groups.filter(([, labels]) => labels.length > 0); + const indent = options.indent ?? ""; + const headings = populated.map( + ([group, labels]) => `${group} (${labels.length})`, + ); + const headingWidth = Math.max( + ...headings.map((heading) => stringWidth(heading)), + ); + const available = Math.max( + 40, + (process.stdout.columns || 100) - + stringWidth(indent) - + headingWidth - + COLUMN_GAP.length, + ); + + populated.forEach(([, labels, paint], groupIndex) => { + const heading = padColumn(paint(headings[groupIndex]), headingWidth); + + wrapValues(labels, available).forEach((line, lineIndex) => { + const prefix = lineIndex === 0 ? heading : " ".repeat(headingWidth); + console.log(`${indent}${prefix}${COLUMN_GAP}${line}`); + }); + }); + + return true; +}; + +export const renderStructuredCollection = ( + sectionName: string | undefined, + rows: JsonObject[], + options: StructuredCollectionRenderOptions = {}, +): boolean => { + const renderer = sectionName + ? structuredCollectionRenderers[sectionName] + : undefined; + + if (!renderer) { + return renderToggleCollection(rows, options); + } + const allRowsMatch = rows.every( (row) => renderer.itemSchema.safeParse(row).success, ); if (!allRowsMatch) { - return false; + return renderToggleCollection(rows, options); } const columns = renderer.columns.map((column) => diff --git a/lib/sdks.ts b/lib/sdks.ts index 79c7a3d..9ba53e4 100644 --- a/lib/sdks.ts +++ b/lib/sdks.ts @@ -14,6 +14,7 @@ import { SDK_VERSION, } from "./constants.js"; import { warn } from "./parser.js"; +import { resolveOrganizationId, resolveProjectId } from "./context.js"; import { isCloudHostname } from "./utils.js"; import { getStoredRefreshToken, @@ -164,16 +165,34 @@ export const sdkForConsole = async ({ return client; }; -export const sdkForProject = async (): Promise => { +/** + * The `/organization` endpoints carry no organization ID in their path and act + * on whichever organization `X-Appwrite-Organization` names, so resolve it from + * the current directory's config unless the caller names one explicitly. + */ +export const sdkForConsoleWithOrganization = async ( + organizationId?: string, +): Promise => { + const client = await sdkForConsole(); + + client.headers["X-Appwrite-Organization"] = await resolveOrganizationId({ + override: organizationId, + consoleClient: client, + }); + + return client; +}; + +export const sdkForProject = async ( + projectIdOverride?: string, +): Promise => { const client = new Client(); const endpoint = localConfig.getEndpoint() || globalConfig.getEndpoint() || DEFAULT_ENDPOINT; const isCloudEndpoint = isCloudHostname(new URL(endpoint).hostname); - const project = localConfig.getProject().projectId - ? localConfig.getProject().projectId - : globalConfig.getProject(); + const project = resolveProjectId(projectIdOverride); const key = globalConfig.getKey(); const accessToken = globalConfig.getAccessToken(); diff --git a/lib/services.ts b/lib/services.ts index e2342c1..cff7098 100644 --- a/lib/services.ts +++ b/lib/services.ts @@ -19,7 +19,7 @@ import { } from "@appwrite.io/console"; export const getConsoleService = async (sdk?: Client): Promise => { - const client = !sdk ? await sdkForProject() : sdk; + const client = !sdk ? await sdkForConsole() : sdk; return new Console(client); }; @@ -46,7 +46,7 @@ export const getOauth2Service = async (sdk?: Client): Promise => { export const getOrganizationsService = async ( sdk?: Client, ): Promise => { - const client = !sdk ? await sdkForProject() : sdk; + const client = !sdk ? await sdkForConsole() : sdk; return new Organizations(client); }; diff --git a/lib/types.ts b/lib/types.ts index 0407261..389c97b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -60,6 +60,7 @@ export interface CliConfig { report: boolean; reportData: Record; displayFields: string[]; + followUpHint: string; } export interface SessionData { diff --git a/lib/utils.ts b/lib/utils.ts index d92efc2..0c76703 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -521,6 +521,21 @@ export const getConsoleProjectSlug = ( } }; +/** + * One indented `email (endpoint)` per line, so a prompt or error listing several + * accounts stays readable instead of wrapping as one comma-joined run. + */ +export const formatAccountList = ( + accounts: Array<{ email: string; endpoint?: string }>, +): string => + accounts + .map((account) => + account.endpoint + ? ` ${account.email} (${account.endpoint})` + : ` ${account.email}`, + ) + .join("\n"); + export const getFunctionDeploymentConsoleUrl = ( endpoint: string, projectId: string, diff --git a/package-lock.json b/package-lock.json index 60e887d..fc45332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appwrite-cli", - "version": "24.1.0", + "version": "25.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appwrite-cli", - "version": "24.1.0", + "version": "25.0.0", "license": "BSD-3-Clause", "dependencies": { "@appwrite.io/console": "15.8.0", @@ -50,6 +50,13 @@ "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": { @@ -1918,6 +1925,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1934,6 +1944,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1950,6 +1963,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1966,6 +1982,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1982,6 +2001,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3521,9 +3543,9 @@ } }, "node_modules/flatted": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", - "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 18410c3..90ea39e 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.1.0", + "version": "25.0.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 0ea2889..fdc505b 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.1.0", + "version": "25.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/24.1.0/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/25.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/24.1.0/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/25.0.0/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe",