diff --git a/CHANGELOG.md b/CHANGELOG.md index 89abbfd0..408c34b4 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 73949f16..9c8d12b1 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 f2d1bac9..870eecca 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 43bdd7bb..e8c0c90a 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 46d3ce4d..00000000 --- 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 342c96b7..00000000 --- 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 b394355f..00000000 --- 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 6a78a602..00000000 --- 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 1e97154f..00000000 --- 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 fcadc72e..00000000 --- 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 ce72c9ad..00000000 --- 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 edcd969a..00000000 --- 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 789484b4..00000000 --- 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 d2d1ae9c..00000000 --- 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 d2f85aac..00000000 --- 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 af94485a..00000000 --- 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 a9d6a62a..00000000 --- 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 4a966b5b..00000000 --- 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 d651ec30..00000000 --- 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 b410542c..00000000 --- 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 54a1835b..00000000 --- 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 5202b75f..00000000 --- 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 928013fc..00000000 --- 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 7970176a..00000000 --- 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 678bd6d8..00000000 --- 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 fbd963c2..00000000 --- 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 c86f41af..00000000 --- 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 47963ee9..00000000 --- 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 81fa57fe..00000000 --- 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 f67386d2..00000000 --- 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 a7eeccbd..00000000 --- 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 200d8e4d..00000000 --- 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 5d8f9d1d..00000000 --- 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 c7a95a84..00000000 --- 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 ca0c5ea1..00000000 --- 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 759c2546..00000000 --- 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 0aa2ae3d..00000000 --- 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 793d3552..00000000 --- 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 d8c54fd6..00000000 --- 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 2aa29184..00000000 --- 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 3822d59a..00000000 --- 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 93152087..00000000 --- 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 de579849..00000000 --- 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 623743ed..00000000 --- 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 291334b7..01d4586d 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 8c482962..feaf0590 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 de562abd..c70662d0 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 3d10fcf7..a14d32f2 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 b6bd00e4..cf260ff5 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 b5c79e85..b30e39c7 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 0abbb760..5a7178c2 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 ea7ef493..54e4b243 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 938cf31c..8a787465 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 0d71ffb8..73eb1dd6 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 2c493e90..00000000 --- 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 a48edd28..54781edc 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 95fff812..43652621 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 ad83fe78..00000000 --- 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 47c9d4df..b98330e0 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 00000000..3e5acb3a --- /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 00000000..d328013f --- /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 00000000..d8ee106c --- /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 03227a9b..c64df654 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 fcd88889..949d677f 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 d9a41432..d26f4595 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 79c7a3d1..9ba53e42 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 e2342c1d..cff7098c 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 0407261d..389c97bd 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 d92efc27..0c76703d 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 60e887d0..fc453328 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 18410c3f..90ea39eb 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 0ea2889f..fdc505b8 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",