appwrite-cli 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Appwrite Command Line SDK
2
2
 
3
3
  ![License](https://img.shields.io/github/license/appwrite/sdk-for-cli.svg?style=flat-square)
4
- ![Version](https://img.shields.io/badge/api%20version-1.4.0-blue.svg?style=flat-square)
4
+ ![Version](https://img.shields.io/badge/api%20version-1.4.2-blue.svg?style=flat-square)
5
5
  [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator)
6
6
  [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
7
7
  [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord)
@@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using
29
29
 
30
30
  ```sh
31
31
  $ appwrite -v
32
- 3.0.0
32
+ 4.0.0
33
33
  ```
34
34
 
35
35
  ### Install using prebuilt binaries
@@ -63,7 +63,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc
63
63
  Once the installation completes, you can verify your install using
64
64
  ```
65
65
  $ appwrite -v
66
- 3.0.0
66
+ 4.0.0
67
67
  ```
68
68
 
69
69
  ## Getting Started
@@ -1,7 +1,7 @@
1
1
  appwrite functions create \
2
2
  --functionId [FUNCTION_ID] \
3
3
  --name [NAME] \
4
- --runtime node-14.5 \
4
+ --runtime node-18.0 \
5
5
 
6
6
 
7
7
 
@@ -1,7 +1,7 @@
1
1
  appwrite functions update \
2
2
  --functionId [FUNCTION_ID] \
3
3
  --name [NAME] \
4
- --runtime node-14.5 \
4
+
5
5
 
6
6
 
7
7
 
@@ -1,7 +1,7 @@
1
1
  appwrite teams createMembership \
2
2
  --teamId [TEAM_ID] \
3
3
  --roles one two three \
4
- --url https://example.com \
4
+
5
5
 
6
6
 
7
7
 
package/install.ps1 CHANGED
@@ -13,8 +13,8 @@
13
13
  # You can use "View source" of this page to see the full script.
14
14
 
15
15
  # REPO
16
- $GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/3.0.0/appwrite-cli-win-x64.exe"
17
- $GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/3.0.0/appwrite-cli-win-arm64.exe"
16
+ $GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/4.0.0/appwrite-cli-win-x64.exe"
17
+ $GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/4.0.0/appwrite-cli-win-arm64.exe"
18
18
 
19
19
  $APPWRITE_BINARY_NAME = "appwrite.exe"
20
20
 
package/install.sh CHANGED
@@ -97,7 +97,7 @@ printSuccess() {
97
97
  downloadBinary() {
98
98
  echo "[2/4] Downloading executable for $OS ($ARCH) ..."
99
99
 
100
- GITHUB_LATEST_VERSION="3.0.0"
100
+ GITHUB_LATEST_VERSION="4.0.0"
101
101
  GITHUB_FILE="appwrite-cli-${OS}-${ARCH}"
102
102
  GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE"
103
103
 
package/lib/client.js CHANGED
@@ -16,8 +16,8 @@ class Client {
16
16
  'x-sdk-name': 'Command Line',
17
17
  'x-sdk-platform': 'console',
18
18
  'x-sdk-language': 'cli',
19
- 'x-sdk-version': '3.0.0',
20
- 'user-agent' : `AppwriteCLI/3.0.0 (${os.type()} ${os.version()}; ${os.arch()})`,
19
+ 'x-sdk-version': '4.0.0',
20
+ 'user-agent' : `AppwriteCLI/4.0.0 (${os.type()} ${os.version()}; ${os.arch()})`,
21
21
  'X-Appwrite-Response-Format' : '1.4.0',
22
22
  };
23
23
  }
@@ -206,10 +206,14 @@ const deployFunction = async ({ functionId, all, yes } = {}) => {
206
206
  functionId: func['$id'],
207
207
  name: func.name,
208
208
  execute: func.execute,
209
- vars: JSON.stringify(response.vars),
210
209
  events: func.events,
211
210
  schedule: func.schedule,
212
211
  timeout: func.timeout,
212
+ enabled: func.enabled,
213
+ logging: func.logging,
214
+ entrypoint: func.entrypoint,
215
+ commands: func.commands,
216
+ vars: JSON.stringify(response.vars),
213
217
  parseOutput: false
214
218
  });
215
219
  } catch (e) {
@@ -220,10 +224,14 @@ const deployFunction = async ({ functionId, all, yes } = {}) => {
220
224
  name: func.name,
221
225
  runtime: func.runtime,
222
226
  execute: func.execute,
223
- vars: JSON.stringify(func.vars),
224
227
  events: func.events,
225
228
  schedule: func.schedule,
226
229
  timeout: func.timeout,
230
+ enabled: func.enabled,
231
+ logging: func.logging,
232
+ entrypoint: func.entrypoint,
233
+ commands: func.commands,
234
+ vars: JSON.stringify(func.vars),
227
235
  parseOutput: false
228
236
  });
229
237
 
@@ -292,6 +300,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => {
292
300
  response = await functionsCreateDeployment({
293
301
  functionId: func['$id'],
294
302
  entrypoint: func.entrypoint,
303
+ commands: func.commands,
295
304
  code: func.path,
296
305
  activate: true,
297
306
  parseOutput: false
@@ -470,47 +470,38 @@ const functionsCreateDeployment = async ({ functionId, code, activate, entrypoin
470
470
  });
471
471
  } else {
472
472
  const streamFilePath = payload['code'];
473
- let id = undefined;
474
473
 
475
- let counter = 0;
476
- const totalCounters = Math.ceil(size / libClient.CHUNK_SIZE);
477
-
478
- const headers = {
474
+ const apiHeaders = {
479
475
  'content-type': 'multipart/form-data',
480
476
  };
481
477
 
478
+ let offset = 0;
482
479
 
483
- for (counter; counter < totalCounters; counter++) {
484
- const start = (counter * libClient.CHUNK_SIZE);
485
- const end = Math.min((((counter * libClient.CHUNK_SIZE) + libClient.CHUNK_SIZE) - 1), size);
486
-
487
- headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size;
480
+ while (offset < size) {
481
+ let end = Math.min(offset + libClient.CHUNK_SIZE - 1, size - 1);
488
482
 
489
- if (id) {
490
- headers['x-appwrite-id'] = id;
483
+ apiHeaders['content-range'] = 'bytes ' + offset + '-' + end + '/' + size;
484
+ if (response && response.$id) {
485
+ apiHeaders['x-appwrite-id'] = response.$id;
491
486
  }
492
487
 
493
488
  const stream = fs.createReadStream(streamFilePath, {
494
- start,
489
+ start: offset,
495
490
  end
496
491
  });
497
492
  payload['code'] = stream;
493
+ response = await client.call('post', apiPath, apiHeaders, payload);
498
494
 
499
- response = await client.call('post', apiPath, headers, payload);
500
-
501
- if (!id) {
502
- id = response['$id'];
503
- }
504
-
505
- if (onProgress !== null) {
495
+ if (onProgress) {
506
496
  onProgress({
507
- $id: response['$id'],
508
- progress: Math.min((counter+1) * libClient.CHUNK_SIZE, size) / size * 100,
509
- sizeUploaded: end+1,
510
- chunksTotal: response['chunksTotal'],
511
- chunksUploaded: response['chunksUploaded']
497
+ $id: response.$id,
498
+ progress: ( offset / size ) * 100,
499
+ sizeUploaded: offset,
500
+ chunksTotal: response.chunksTotal,
501
+ chunksUploaded: response.chunksUploaded
512
502
  });
513
503
  }
504
+ offset += libClient.CHUNK_SIZE;
514
505
  }
515
506
  }
516
507
 
@@ -920,7 +911,7 @@ functions
920
911
  .description(`Update function by its unique ID.`)
921
912
  .requiredOption(`--functionId <functionId>`, `Function ID.`)
922
913
  .requiredOption(`--name <name>`, `Function name. Max length: 128 chars.`)
923
- .requiredOption(`--runtime <runtime>`, `Execution runtime.`)
914
+ .option(`--runtime <runtime>`, `Execution runtime.`)
924
915
  .option(`--execute [execute...]`, `An array of role strings with execution permissions. By default no user is granted with any execute permissions. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of 100 roles are allowed, each 64 characters long.`)
925
916
  .option(`--events [events...]`, `Events list. Maximum of 100 events are allowed.`)
926
917
  .option(`--schedule <schedule>`, `Schedule CRON syntax.`)
@@ -72,16 +72,22 @@ const initFunction = async () => {
72
72
  log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first deploy the function.`);
73
73
  }
74
74
 
75
+ if (!answers.runtime.commands) {
76
+ log(`Installation command for this runtime not found. You will be asked to configure the install command when you first deploy the function.`);
77
+ }
78
+
75
79
  let response = await functionsCreate({
76
80
  functionId: answers.id,
77
81
  name: answers.name,
78
82
  runtime: answers.runtime.id,
83
+ entrypoint: answers.runtime.entrypoint || '',
84
+ commands: answers.runtime.commands || '',
79
85
  parseOutput: false
80
86
  })
81
87
 
82
88
  fs.mkdirSync(functionDir, "777");
83
89
 
84
- let gitInitCommands = "git clone --depth 1 --sparse https://github.com/appwrite/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched
90
+ let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/appwrite/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched
85
91
 
86
92
  let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`;
87
93
 
@@ -138,13 +144,16 @@ const initFunction = async () => {
138
144
  $id: response['$id'],
139
145
  name: response.name,
140
146
  runtime: response.runtime,
141
- path: `functions/${answers.name}`,
142
- entrypoint: answers.runtime.entrypoint || '',
143
- ignore: answers.runtime.ignore || null,
144
147
  execute: response.execute,
145
148
  events: response.events,
146
149
  schedule: response.schedule,
147
150
  timeout: response.timeout,
151
+ enabled: response.enabled,
152
+ logging: response.logging,
153
+ entrypoint: response.entrypoint,
154
+ commands: response.commands,
155
+ ignore: answers.runtime.ignore || null,
156
+ path: `functions/${answers.name}`,
148
157
  };
149
158
 
150
159
  localConfig.addFunction(data);
@@ -300,54 +300,45 @@ const storageCreateFile = async ({ bucketId, fileId, file, permissions, parseOut
300
300
  }, payload)
301
301
  } else {
302
302
  const streamFilePath = payload['file'];
303
- let id = undefined;
304
303
 
305
- let counter = 0;
306
- const totalCounters = Math.ceil(size / libClient.CHUNK_SIZE);
307
-
308
- const headers = {
304
+ const apiHeaders = {
309
305
  'content-type': 'multipart/form-data',
310
306
  };
311
307
 
308
+ let offset = 0;
312
309
  if(fileId != 'unique()') {
313
310
  try {
314
- response = await client.call('get', apiPath + '/' + fileId, headers);
315
- counter = response.chunksUploaded;
311
+ response = await client.call('get', apiPath + '/' + fileId, apiHeaders);
312
+ offset = response.chunksUploaded * libClient.CHUNK_SIZE;
316
313
  } catch(e) {
317
314
  }
318
315
  }
319
316
 
320
- for (counter; counter < totalCounters; counter++) {
321
- const start = (counter * libClient.CHUNK_SIZE);
322
- const end = Math.min((((counter * libClient.CHUNK_SIZE) + libClient.CHUNK_SIZE) - 1), size);
323
-
324
- headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size;
317
+ while (offset < size) {
318
+ let end = Math.min(offset + libClient.CHUNK_SIZE - 1, size - 1);
325
319
 
326
- if (id) {
327
- headers['x-appwrite-id'] = id;
320
+ apiHeaders['content-range'] = 'bytes ' + offset + '-' + end + '/' + size;
321
+ if (response && response.$id) {
322
+ apiHeaders['x-appwrite-id'] = response.$id;
328
323
  }
329
324
 
330
325
  const stream = fs.createReadStream(streamFilePath, {
331
- start,
326
+ start: offset,
332
327
  end
333
328
  });
334
329
  payload['file'] = stream;
330
+ response = await client.call('post', apiPath, apiHeaders, payload);
335
331
 
336
- response = await client.call('post', apiPath, headers, payload);
337
-
338
- if (!id) {
339
- id = response['$id'];
340
- }
341
-
342
- if (onProgress !== null) {
332
+ if (onProgress) {
343
333
  onProgress({
344
- $id: response['$id'],
345
- progress: Math.min((counter+1) * libClient.CHUNK_SIZE, size) / size * 100,
346
- sizeUploaded: end+1,
347
- chunksTotal: response['chunksTotal'],
348
- chunksUploaded: response['chunksUploaded']
334
+ $id: response.$id,
335
+ progress: ( offset / size ) * 100,
336
+ sizeUploaded: offset,
337
+ chunksTotal: response.chunksTotal,
338
+ chunksUploaded: response.chunksUploaded
349
339
  });
350
340
  }
341
+ offset += libClient.CHUNK_SIZE;
351
342
  }
352
343
  }
353
344
 
@@ -193,13 +193,13 @@ const teamsListMemberships = async ({ teamId, queries, search, parseOutput = tru
193
193
  return response;
194
194
  }
195
195
 
196
- const teamsCreateMembership = async ({ teamId, roles, url, email, userId, phone, name, parseOutput = true, sdk = undefined}) => {
196
+ const teamsCreateMembership = async ({ teamId, roles, email, userId, phone, url, name, parseOutput = true, sdk = undefined}) => {
197
197
  /* @param {string} teamId */
198
198
  /* @param {string[]} roles */
199
- /* @param {string} url */
200
199
  /* @param {string} email */
201
200
  /* @param {string} userId */
202
201
  /* @param {string} phone */
202
+ /* @param {string} url */
203
203
  /* @param {string} name */
204
204
 
205
205
  let client = !sdk ? await sdkForProject() : sdk;
@@ -447,10 +447,10 @@ teams
447
447
  .description(`Invite a new member to join your team. Provide an ID for existing users, or invite unregistered users using an email or phone number. If initiated from a Client SDK, Appwrite will send an email or sms with a link to join the team to the invited user, and an account will be created for them if one doesn't exist. If initiated from a Server SDK, the new member will be added automatically to the team. You only need to provide one of a user ID, email, or phone number. Appwrite will prioritize accepting the user ID > email > phone number if you provide more than one of these parameters. Use the 'url' parameter to redirect the user from the invitation email to your app. After the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) Appwrite will accept the only redirect URLs under the domains you have added as a platform on the Appwrite Console. `)
448
448
  .requiredOption(`--teamId <teamId>`, `Team ID.`)
449
449
  .requiredOption(`--roles [roles...]`, `Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of 100 roles are allowed, each 32 characters long.`)
450
- .requiredOption(`--url <url>`, `URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.`)
451
450
  .option(`--email <email>`, `Email of the new team member.`)
452
451
  .option(`--userId <userId>`, `ID of the user to be added to a team.`)
453
452
  .option(`--phone <phone>`, `Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.`)
453
+ .option(`--url <url>`, `URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.`)
454
454
  .option(`--name <name>`, `Name of the new team member. Max length: 128 chars.`)
455
455
  .action(actionRunner(teamsCreateMembership))
456
456
 
@@ -1035,7 +1035,7 @@ users
1035
1035
  .command(`updateLabels`)
1036
1036
  .description(`Update the user labels by its unique ID. Labels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](/docs/permissions) for more info.`)
1037
1037
  .requiredOption(`--userId <userId>`, `User ID.`)
1038
- .requiredOption(`--labels [labels...]`, `Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.`)
1038
+ .requiredOption(`--labels [labels...]`, `Array of user labels. Replaces the previous labels. Maximum of 100 labels are allowed, each up to 36 alphanumeric characters long.`)
1039
1039
  .action(actionRunner(usersUpdateLabels))
1040
1040
 
1041
1041
  users
package/lib/parser.js CHANGED
@@ -169,6 +169,11 @@ const commandDescriptions = {
169
169
  "login": `The login command allows you to authenticate and manage a user account.`,
170
170
  "logout": `The logout command allows you to logout of your Appwrite account.`,
171
171
  "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`,
172
+ "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`,
173
+ "migrations": `The migrations command allows you to migrate data between services.`,
174
+ "project": `The project command is for overall project administration.`,
175
+ "proxy": `The proxy command allows you to configure behavior for your attached domains.`,
176
+ "vcs": `The vcs command allows you to interact with VCS providers and manage your code repositories.`,
172
177
  "main": chalk.redBright(`${logo}${description}`),
173
178
  }
174
179
 
package/lib/questions.js CHANGED
@@ -43,27 +43,57 @@ const getEntrypoint = (runtime) => {
43
43
  case 'dart':
44
44
  return 'lib/main.dart';
45
45
  case 'deno':
46
- return 'src/mod.ts';
46
+ return 'src/main.ts';
47
47
  case 'node':
48
- return 'src/index.js';
48
+ return 'src/main.js';
49
49
  case 'php':
50
50
  return 'src/index.php';
51
51
  case 'python':
52
- return 'src/index.py';
52
+ return 'src/main.py';
53
53
  case 'ruby':
54
- return 'src/index.rb';
54
+ return 'lib/main.rb';
55
55
  case 'rust':
56
56
  return 'main.rs';
57
57
  case 'swift':
58
- return 'Sources/swift-5.5/main.swift';
58
+ return 'Sources/index.swift';
59
59
  case 'cpp':
60
- return 'src/index.cc';
60
+ return 'src/main.cc';
61
61
  case 'dotnet':
62
62
  return 'src/Index.cs';
63
63
  case 'java':
64
- return 'src/Index.java';
64
+ return 'src/Main.java';
65
65
  case 'kotlin':
66
- return 'src/Index.kt';
66
+ return 'src/Main.kt';
67
+ }
68
+
69
+ return undefined;
70
+ };
71
+
72
+ const getInstallCommand = (runtime) => {
73
+ const languge = runtime.split('-')[0];
74
+
75
+ switch (languge) {
76
+ case 'dart':
77
+ return 'dart pub get';
78
+ case 'deno':
79
+ return "deno install";
80
+ case 'node':
81
+ return 'npm install';
82
+ case 'php':
83
+ return 'composer install';
84
+ case 'python':
85
+ return 'pip install -r requirements.txt';
86
+ case 'ruby':
87
+ return 'bundle install';
88
+ case 'rust':
89
+ return 'cargo install';
90
+ case 'dotnet':
91
+ return 'dotnet restore';
92
+ case 'swift':
93
+ case 'java':
94
+ case 'kotlin':
95
+ case 'cpp':
96
+ return '';
67
97
  }
68
98
 
69
99
  return undefined;
@@ -171,10 +201,15 @@ const questionsInitFunction = [
171
201
  parseOutput: false
172
202
  })
173
203
  let runtimes = response["runtimes"]
174
- let choices = runtimes.map((runtime, idx) => {
204
+ let choices = runtimes.map((runtime, idx) => {
175
205
  return {
176
206
  name: `${runtime.name} (${runtime['$id']})`,
177
- value: { id: runtime['$id'], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']) },
207
+ value: {
208
+ id: runtime['$id'],
209
+ entrypoint: getEntrypoint(runtime['$id']),
210
+ ignore: getIgnores(runtime['$id']),
211
+ commands : getInstallCommand(runtime['$id'])
212
+ },
178
213
  }
179
214
  })
180
215
  return choices;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "appwrite-cli",
3
3
  "homepage": "https://appwrite.io/support",
4
4
  "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
5
- "version": "3.0.0",
5
+ "version": "4.0.0",
6
6
  "license": "BSD-3-Clause",
7
7
  "main": "index.js",
8
8
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "windows-arm64": "pkg -t node16-win-arm64 -o build/appwrite-cli-win-arm64.exe package.json"
23
23
  },
24
24
  "dependencies": {
25
- "axios": "^0.27.2",
25
+ "axios": "1.5.0",
26
26
  "chalk": "4.1.2",
27
27
  "cli-table3": "^0.6.2",
28
28
  "commander": "^9.2.0",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "The Appwrite CLI is a command-line application that allows you to interact with Appwrite and perform server-side tasks using your terminal.",
5
5
  "homepage": "https://github.com/appwrite/sdk-for-cli",
6
6
  "license": "BSD-3-Clause",
7
7
  "architecture": {
8
8
  "64bit": {
9
- "url": "https://github.com/appwrite/sdk-for-cli/releases/download/3.0.0/appwrite-cli-win-x64.exe",
9
+ "url": "https://github.com/appwrite/sdk-for-cli/releases/download/4.0.0/appwrite-cli-win-x64.exe",
10
10
  "bin": [
11
11
  [
12
12
  "appwrite-cli-win-x64.exe",
@@ -15,7 +15,7 @@
15
15
  ]
16
16
  },
17
17
  "arm64": {
18
- "url": "https://github.com/appwrite/sdk-for-cli/releases/download/3.0.0/appwrite-cli-win-arm64.exe",
18
+ "url": "https://github.com/appwrite/sdk-for-cli/releases/download/4.0.0/appwrite-cli-win-arm64.exe",
19
19
  "bin": [
20
20
  [
21
21
  "appwrite-cli-win-arm64.exe",