appwrite-utils-cli 0.9.999 → 0.10.1

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
@@ -147,6 +147,9 @@ This updated CLI ensures that developers have robust tools at their fingertips t
147
147
 
148
148
  ## Changelog
149
149
 
150
+ - 0.10.01: Fixed `predeployCommands` to work
151
+ - 0.10.001: Updated `deployFunction` to not updateConfig if it's already present
152
+ - 0.10.0: Fixed `synchronize configurations` for functions, now you do not need to deploy the function first
150
153
  - 0.9.999: Fixed Functions, looks for `./functions` in addition to `appwriteConfigFolder/functions`
151
154
  - 0.9.998: Fixed transfer finally, added `--targetDbId` and `--sourceDbId` as aliases
152
155
  - 0.9.994: Added function deployment management, in BETA, and fixed document transfer between databases
@@ -3,6 +3,7 @@ import { InputFile } from "node-appwrite/file";
3
3
  import { create as createTarball } from "tar";
4
4
  import { join } from "node:path";
5
5
  import fs from "node:fs";
6
+ import { platform } from "node:os";
6
7
  import {} from "appwrite-utils";
7
8
  import chalk from "chalk";
8
9
  import cliProgress from "cli-progress";
@@ -46,20 +47,20 @@ export const deployFunction = async (client, functionId, codePath, activate = tr
46
47
  const fileObject = InputFile.fromBuffer(new Uint8Array(fileBuffer), `function-${functionId}.tar.gz`);
47
48
  try {
48
49
  console.log(chalk.blue("🚀 Creating deployment..."));
50
+ progressBar.start(1, 0);
49
51
  const functionResponse = await functions.createDeployment(functionId, fileObject, activate, entrypoint, commands, (progress) => {
50
52
  const chunks = progress.chunksUploaded;
51
53
  const total = progress.chunksTotal;
52
- if (chunks && total) {
53
- if (chunks === 0) {
54
+ if (chunks !== undefined && total) {
55
+ if (chunks === 0 && total !== 0) {
54
56
  progressBar.start(total, 0);
55
57
  }
56
- else if (chunks === total) {
57
- progressBar.update(total);
58
- progressBar.stop();
59
- console.log(chalk.green("✅ Upload complete!"));
60
- }
61
58
  else {
62
59
  progressBar.update(chunks);
60
+ if (chunks === total) {
61
+ progressBar.stop();
62
+ console.log(chalk.green("✅ Upload complete!"));
63
+ }
63
64
  }
64
65
  }
65
66
  });
@@ -85,22 +86,29 @@ export const deployLocalFunction = async (client, functionName, functionConfig,
85
86
  functionConfig.dirPath ||
86
87
  findFunctionDirectory(process.cwd(), functionName) ||
87
88
  join(process.cwd(), "functions", functionName.toLowerCase().replace(/\s+/g, "-"));
89
+ if (!fs.existsSync(resolvedPath)) {
90
+ throw new Error(`Function directory not found at ${resolvedPath}`);
91
+ }
88
92
  if (functionConfig.predeployCommands?.length) {
89
93
  console.log(chalk.blue("Executing predeploy commands..."));
94
+ const isWindows = platform() === "win32";
90
95
  for (const command of functionConfig.predeployCommands) {
91
96
  try {
92
97
  console.log(chalk.gray(`Executing: ${command}`));
93
98
  execSync(command, {
94
99
  cwd: resolvedPath,
95
100
  stdio: "inherit",
101
+ shell: isWindows ? "cmd.exe" : "/bin/sh",
102
+ windowsHide: true,
96
103
  });
97
104
  }
98
105
  catch (error) {
99
- console.error(chalk.red(`Failed to execute predeploy command: ${command}`));
100
- throw error;
106
+ console.error(chalk.red(`Failed to execute predeploy command: ${command}`), error);
107
+ throw new Error(``);
101
108
  }
102
109
  }
103
110
  }
111
+ // Only create function if it doesn't exist
104
112
  if (!functionExists) {
105
113
  await createFunction(client, functionConfig.$id, functionConfig.name, functionConfig.runtime, functionConfig.execute, functionConfig.events, functionConfig.schedule, functionConfig.timeout, functionConfig.enabled, functionConfig.logging, functionConfig.entrypoint, functionConfig.commands);
106
114
  }
@@ -10,7 +10,7 @@ import { AppwriteFunctionSchema, parseAttribute, PermissionToAppwritePermission,
10
10
  import { ulid } from "ulidx";
11
11
  import chalk from "chalk";
12
12
  import { DateTime } from "luxon";
13
- import { createFunctionTemplate, deleteFunction, downloadLatestFunctionDeployment, listFunctions, listSpecifications, } from "./functions/methods.js";
13
+ import { createFunctionTemplate, deleteFunction, downloadLatestFunctionDeployment, getFunction, listFunctions, listSpecifications, } from "./functions/methods.js";
14
14
  import { deployLocalFunction } from "./functions/deployments.js";
15
15
  import { join } from "node:path";
16
16
  import fs from "node:fs";
@@ -298,7 +298,17 @@ export class InteractiveCLI {
298
298
  join(process.cwd(), functionName), // ./functionName
299
299
  join(basePath, "functions", functionName), // appwriteFolder/functions/functionName
300
300
  join(basePath, functionName), // appwriteFolder/functionName
301
+ join(basePath, functionName.toLowerCase()), // appwriteFolder/functionName.toLowerCase()
302
+ join(basePath, functionName.toLowerCase().replace(/\s+/g, "")), // appwriteFolder/functionName.toLowerCase().replace(/\s+/g, "")
303
+ join(process.cwd(), functionName.toLowerCase()), // ./functionName.toLowerCase()
301
304
  ];
305
+ // Create different variations of the function name for comparison
306
+ const functionNameVariations = new Set([
307
+ functionName.toLowerCase(),
308
+ functionName.toLowerCase().replace(/\s+/g, ""),
309
+ functionName.toLowerCase().replace(/[^a-z0-9]/g, ""),
310
+ functionName.toLowerCase().replace(/[-_\s]+/g, ""),
311
+ ]);
302
312
  // Check common locations first
303
313
  for (const path of commonPaths) {
304
314
  try {
@@ -331,7 +341,15 @@ export class InteractiveCLI {
331
341
  if (entry.isDirectory() &&
332
342
  !entry.name.startsWith(".") &&
333
343
  entry.name !== "node_modules") {
334
- if (entry.name === functionName) {
344
+ const entryNameVariations = new Set([
345
+ entry.name.toLowerCase(),
346
+ entry.name.toLowerCase().replace(/\s+/g, ""),
347
+ entry.name.toLowerCase().replace(/[^a-z0-9]/g, ""),
348
+ entry.name.toLowerCase().replace(/[-_\s]+/g, ""),
349
+ ]);
350
+ // Check if any variation of the entry name matches any variation of the function name
351
+ const hasMatch = [...functionNameVariations].some((fnVar) => [...entryNameVariations].includes(fnVar));
352
+ if (hasMatch) {
335
353
  console.log(chalk.green(`Found function at: ${fullPath}`));
336
354
  return fullPath;
337
355
  }
@@ -361,23 +379,26 @@ export class InteractiveCLI {
361
379
  console.log(chalk.red("Invalid function configuration"));
362
380
  return;
363
381
  }
382
+ // Ensure functions array exists
383
+ if (!this.controller.config.functions) {
384
+ this.controller.config.functions = [];
385
+ }
364
386
  let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", functionConfig.name);
365
387
  if (!fs.existsSync(functionPath)) {
366
388
  console.log(chalk.yellow(`Function not found in primary location, searching subdirectories...`));
367
- const foundPath = await this.findFunctionInSubdirectories(this.controller.getAppwriteFolderPath(), functionConfig.name);
389
+ const foundPath = await this.findFunctionInSubdirectories(this.controller.getAppwriteFolderPath(), functionConfig.name.toLowerCase());
368
390
  if (foundPath) {
369
391
  console.log(chalk.green(`Found function at: ${foundPath}`));
370
392
  functionPath = foundPath;
371
393
  functionConfig.dirPath = foundPath;
372
394
  }
373
395
  else {
374
- console.log(chalk.yellow(`Function ${functionConfig.name} not found locally in any subdirectory`));
375
396
  const { shouldDownload } = await inquirer.prompt([
376
397
  {
377
398
  type: "confirm",
378
399
  name: "shouldDownload",
379
- message: "Would you like to download the latest deployment?",
380
- default: true,
400
+ message: "Function not found locally. Would you like to download the latest deployment?",
401
+ default: false,
381
402
  },
382
403
  ]);
383
404
  if (shouldDownload) {
@@ -385,42 +406,15 @@ export class InteractiveCLI {
385
406
  console.log(chalk.blue("Downloading latest deployment..."));
386
407
  const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(this.controller.appwriteServer, functionConfig.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
387
408
  console.log(chalk.green(`✨ Function downloaded to ${downloadedPath}`));
388
- // Update the config and functions array safely
389
- this.controller.config.functions =
390
- this.controller.config.functions || [];
391
- const newFunction = {
392
- $id: remoteFunction.$id,
393
- name: remoteFunction.name,
394
- runtime: remoteFunction.runtime,
395
- execute: remoteFunction.execute || [],
396
- events: remoteFunction.events || [],
397
- schedule: remoteFunction.schedule || "",
398
- timeout: remoteFunction.timeout || 15,
399
- enabled: remoteFunction.enabled !== false,
400
- logging: remoteFunction.logging !== false,
401
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
402
- commands: remoteFunction.commands || "npm install",
403
- dirPath: downloadedPath,
404
- scopes: (remoteFunction.scopes || []),
405
- installationId: remoteFunction.installationId,
406
- providerRepositoryId: remoteFunction.providerRepositoryId,
407
- providerBranch: remoteFunction.providerBranch,
408
- providerSilentMode: remoteFunction.providerSilentMode,
409
- providerRootDirectory: remoteFunction.providerRootDirectory,
410
- specification: remoteFunction.specification,
411
- };
412
409
  const existingIndex = this.controller.config.functions.findIndex((f) => f?.$id === remoteFunction.$id);
413
410
  if (existingIndex >= 0) {
414
- this.controller.config.functions[existingIndex] = newFunction;
415
- }
416
- else {
417
- this.controller.config.functions.push(newFunction);
411
+ // Only update the dirPath if function exists
412
+ this.controller.config.functions[existingIndex].dirPath =
413
+ downloadedPath;
418
414
  }
419
- const schemaGenerator = new SchemaGenerator(this.controller.config, this.controller.getAppwriteFolderPath());
420
- schemaGenerator.updateConfig(this.controller.config);
421
- console.log(chalk.green("✨ Updated appwriteConfig.ts with new function"));
422
415
  await this.controller.reloadConfig();
423
416
  functionConfig.dirPath = downloadedPath;
417
+ functionPath = downloadedPath;
424
418
  }
425
419
  catch (error) {
426
420
  console.error(chalk.red("Failed to download function deployment:"), error);
@@ -428,7 +422,7 @@ export class InteractiveCLI {
428
422
  }
429
423
  }
430
424
  else {
431
- console.log(chalk.yellow("Deployment cancelled"));
425
+ console.log(chalk.red(`Function ${functionConfig.name} not found locally. Cannot deploy.`));
432
426
  return;
433
427
  }
434
428
  }
@@ -437,7 +431,13 @@ export class InteractiveCLI {
437
431
  console.log(chalk.red("Appwrite server not initialized"));
438
432
  return;
439
433
  }
440
- await deployLocalFunction(this.controller.appwriteServer, functionConfig.name, functionConfig);
434
+ try {
435
+ await deployLocalFunction(this.controller.appwriteServer, functionConfig.name, functionConfig);
436
+ console.log(chalk.green("✨ Function deployed successfully!"));
437
+ }
438
+ catch (error) {
439
+ console.error(chalk.red("Failed to deploy function:"), error);
440
+ }
441
441
  }
442
442
  async deleteFunction() {
443
443
  const functions = await this.selectFunctions("Select functions to delete:", true, false);
@@ -455,52 +455,33 @@ export class InteractiveCLI {
455
455
  }
456
456
  }
457
457
  }
458
- async selectFunctions(message, multiSelect = true, preferLocal = false) {
459
- await this.initControllerIfNeeded();
460
- const configFunctions = this.getLocalFunctions();
461
- let remoteFunctions = [];
462
- try {
463
- const functions = await this.controller.listAllFunctions();
464
- remoteFunctions = functions;
465
- }
466
- catch (error) {
467
- console.log(chalk.yellow(`Note: Remote functions not available, using only local functions`));
468
- }
469
- // Combine functions based on whether we're deploying or not
470
- const allFunctions = preferLocal
471
- ? [
472
- ...configFunctions,
473
- ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
474
- ]
475
- : [
476
- ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
477
- ...configFunctions.filter((f) => !remoteFunctions.some((rf) => rf.name === f.name)),
478
- ];
479
- if (allFunctions.length === 0) {
480
- console.log(chalk.red("No functions available"));
481
- return [];
482
- }
483
- const choices = allFunctions
484
- .sort((a, b) => a.name.localeCompare(b.name))
485
- .map((func) => ({
486
- name: func.name,
487
- value: func,
488
- }));
458
+ async selectFunctions(message, multiple = true, includeRemote = false) {
459
+ const remoteFunctions = includeRemote
460
+ ? await listFunctions(this.controller.appwriteServer, [
461
+ Query.limit(1000),
462
+ ])
463
+ : { functions: [] };
464
+ const localFunctions = this.getLocalFunctions();
465
+ // Combine functions, preferring local ones
466
+ const allFunctions = [
467
+ ...localFunctions,
468
+ ...remoteFunctions.functions.filter((rf) => !localFunctions.some((lf) => lf.name === rf.name)),
469
+ ];
489
470
  const { selectedFunctions } = await inquirer.prompt([
490
471
  {
491
- type: multiSelect ? "checkbox" : "list",
472
+ type: multiple ? "checkbox" : "list",
492
473
  name: "selectedFunctions",
493
- message: chalk.blue(message),
494
- choices,
474
+ message,
475
+ choices: allFunctions.map((f) => ({
476
+ name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name)
477
+ ? " (Local)"
478
+ : " (Remote)"}`,
479
+ value: f,
480
+ })),
495
481
  loop: true,
496
- pageSize: 10,
497
482
  },
498
483
  ]);
499
- // For single selection, ensure we return an array
500
- if (!multiSelect) {
501
- return selectedFunctions ? [selectedFunctions] : [];
502
- }
503
- return selectedFunctions || [];
484
+ return multiple ? selectedFunctions : [selectedFunctions];
504
485
  }
505
486
  getLocalFunctions() {
506
487
  const configFunctions = this.controller.config?.functions || [];
@@ -805,55 +786,153 @@ export class InteractiveCLI {
805
786
  const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
806
787
  const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
807
788
  if (hasLocal && hasRemote) {
789
+ // First try to find the function locally
790
+ let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
791
+ if (!fs.existsSync(functionPath)) {
792
+ console.log(chalk.yellow(`Function not found in primary location, searching subdirectories...`));
793
+ const foundPath = await this.findFunctionInSubdirectories(this.controller.getAppwriteFolderPath(), func.name);
794
+ if (foundPath) {
795
+ console.log(chalk.green(`Found function at: ${foundPath}`));
796
+ functionPath = foundPath;
797
+ }
798
+ }
808
799
  const { preference } = await inquirer.prompt([
809
800
  {
810
801
  type: "list",
811
802
  name: "preference",
812
- message: `Function "${func.name}" exists both locally and remotely. What would you like to do?`,
803
+ message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
813
804
  choices: [
814
- {
815
- name: "Keep local version (deploy to remote)",
816
- value: "local",
817
- },
805
+ ...(functionPath
806
+ ? [
807
+ {
808
+ name: "Keep local version (deploy to remote)",
809
+ value: "local",
810
+ },
811
+ ]
812
+ : []),
818
813
  { name: "Use remote version (download)", value: "remote" },
814
+ { name: "Update config only", value: "config" },
819
815
  { name: "Skip this function", value: "skip" },
820
816
  ],
821
817
  },
822
818
  ]);
823
- if (preference === "local") {
819
+ if (preference === "local" && functionPath) {
824
820
  await this.controller.deployFunction(func.name);
825
821
  }
826
822
  else if (preference === "remote") {
827
823
  await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
828
824
  }
825
+ else if (preference === "config") {
826
+ const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
827
+ const newFunction = {
828
+ $id: remoteFunction.$id,
829
+ name: remoteFunction.name,
830
+ runtime: remoteFunction.runtime,
831
+ execute: remoteFunction.execute || [],
832
+ events: remoteFunction.events || [],
833
+ schedule: remoteFunction.schedule || "",
834
+ timeout: remoteFunction.timeout || 15,
835
+ enabled: remoteFunction.enabled !== false,
836
+ logging: remoteFunction.logging !== false,
837
+ entrypoint: remoteFunction.entrypoint || "src/index.ts",
838
+ commands: remoteFunction.commands || "npm install",
839
+ scopes: (remoteFunction.scopes || []),
840
+ installationId: remoteFunction.installationId,
841
+ providerRepositoryId: remoteFunction.providerRepositoryId,
842
+ providerBranch: remoteFunction.providerBranch,
843
+ providerSilentMode: remoteFunction.providerSilentMode,
844
+ providerRootDirectory: remoteFunction.providerRootDirectory,
845
+ specification: remoteFunction.specification,
846
+ };
847
+ const existingIndex = this.controller.config.functions.findIndex((f) => f.$id === remoteFunction.$id);
848
+ if (existingIndex >= 0) {
849
+ this.controller.config.functions[existingIndex] = newFunction;
850
+ }
851
+ else {
852
+ this.controller.config.functions.push(newFunction);
853
+ }
854
+ console.log(chalk.green(`Updated config for function: ${func.name}`));
855
+ }
829
856
  }
830
857
  else if (hasLocal) {
831
- const { deploy } = await inquirer.prompt([
858
+ // Similar check for local-only functions
859
+ let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
860
+ if (!fs.existsSync(functionPath)) {
861
+ const foundPath = await this.findFunctionInSubdirectories(this.controller.getAppwriteFolderPath(), func.name);
862
+ if (foundPath) {
863
+ functionPath = foundPath;
864
+ }
865
+ }
866
+ const { action } = await inquirer.prompt([
832
867
  {
833
- type: "confirm",
834
- name: "deploy",
835
- message: `Function "${func.name}" exists only locally. Deploy to remote?`,
836
- default: true,
868
+ type: "list",
869
+ name: "action",
870
+ message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
871
+ choices: [
872
+ ...(functionPath
873
+ ? [
874
+ {
875
+ name: "Deploy to remote",
876
+ value: "deploy",
877
+ },
878
+ ]
879
+ : []),
880
+ { name: "Skip this function", value: "skip" },
881
+ ],
837
882
  },
838
883
  ]);
839
- if (deploy) {
884
+ if (action === "deploy" && functionPath) {
840
885
  await this.controller.deployFunction(func.name);
841
886
  }
842
887
  }
843
888
  else if (hasRemote) {
844
- const { download } = await inquirer.prompt([
889
+ const { action } = await inquirer.prompt([
845
890
  {
846
- type: "confirm",
847
- name: "download",
848
- message: `Function "${func.name}" exists only remotely. Download locally?`,
849
- default: true,
891
+ type: "list",
892
+ name: "action",
893
+ message: `Function "${func.name}" exists only remotely. What would you like to do?`,
894
+ choices: [
895
+ { name: "Update config only", value: "config" },
896
+ { name: "Download locally", value: "download" },
897
+ { name: "Skip this function", value: "skip" },
898
+ ],
850
899
  },
851
900
  ]);
852
- if (download) {
901
+ if (action === "download") {
853
902
  await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
854
903
  }
904
+ else if (action === "config") {
905
+ const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
906
+ const newFunction = {
907
+ $id: remoteFunction.$id,
908
+ name: remoteFunction.name,
909
+ runtime: remoteFunction.runtime,
910
+ execute: remoteFunction.execute || [],
911
+ events: remoteFunction.events || [],
912
+ schedule: remoteFunction.schedule || "",
913
+ timeout: remoteFunction.timeout || 15,
914
+ enabled: remoteFunction.enabled !== false,
915
+ logging: remoteFunction.logging !== false,
916
+ entrypoint: remoteFunction.entrypoint || "src/index.ts",
917
+ commands: remoteFunction.commands || "npm install",
918
+ scopes: (remoteFunction.scopes || []),
919
+ installationId: remoteFunction.installationId,
920
+ providerRepositoryId: remoteFunction.providerRepositoryId,
921
+ providerBranch: remoteFunction.providerBranch,
922
+ providerSilentMode: remoteFunction.providerSilentMode,
923
+ providerRootDirectory: remoteFunction.providerRootDirectory,
924
+ specification: remoteFunction.specification,
925
+ };
926
+ this.controller.config.functions =
927
+ this.controller.config.functions || [];
928
+ this.controller.config.functions.push(newFunction);
929
+ console.log(chalk.green(`Added config for remote function: ${func.name}`));
930
+ }
855
931
  }
856
932
  }
933
+ // Update schemas after all changes
934
+ const schemaGenerator = new SchemaGenerator(this.controller.config, this.controller.getAppwriteFolderPath());
935
+ schemaGenerator.updateConfig(this.controller.config);
857
936
  }
858
937
  console.log(chalk.green("✨ Configurations synchronized successfully!"));
859
938
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "0.9.999",
4
+ "version": "0.10.01",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -3,6 +3,7 @@ import { InputFile } from "node-appwrite/file";
3
3
  import { create as createTarball } from "tar";
4
4
  import { join } from "node:path";
5
5
  import fs from "node:fs";
6
+ import { platform } from "node:os";
6
7
  import { type AppwriteFunction, type Specification } from "appwrite-utils";
7
8
  import chalk from "chalk";
8
9
  import cliProgress from "cli-progress";
@@ -76,6 +77,7 @@ export const deployFunction = async (
76
77
 
77
78
  try {
78
79
  console.log(chalk.blue("🚀 Creating deployment..."));
80
+ progressBar.start(1, 0);
79
81
  const functionResponse = await functions.createDeployment(
80
82
  functionId,
81
83
  fileObject,
@@ -85,15 +87,15 @@ export const deployFunction = async (
85
87
  (progress) => {
86
88
  const chunks = progress.chunksUploaded;
87
89
  const total = progress.chunksTotal;
88
- if (chunks && total) {
89
- if (chunks === 0) {
90
+ if (chunks !== undefined && total) {
91
+ if (chunks === 0 && total !== 0) {
90
92
  progressBar.start(total, 0);
91
- } else if (chunks === total) {
92
- progressBar.update(total);
93
- progressBar.stop();
94
- console.log(chalk.green("✅ Upload complete!"));
95
93
  } else {
96
94
  progressBar.update(chunks);
95
+ if (chunks === total) {
96
+ progressBar.stop();
97
+ console.log(chalk.green("✅ Upload complete!"));
98
+ }
97
99
  }
98
100
  }
99
101
  }
@@ -132,24 +134,34 @@ export const deployLocalFunction = async (
132
134
  functionName.toLowerCase().replace(/\s+/g, "-")
133
135
  );
134
136
 
137
+ if (!fs.existsSync(resolvedPath)) {
138
+ throw new Error(`Function directory not found at ${resolvedPath}`);
139
+ }
140
+
135
141
  if (functionConfig.predeployCommands?.length) {
136
142
  console.log(chalk.blue("Executing predeploy commands..."));
143
+ const isWindows = platform() === "win32";
144
+
137
145
  for (const command of functionConfig.predeployCommands) {
138
146
  try {
139
147
  console.log(chalk.gray(`Executing: ${command}`));
140
148
  execSync(command, {
141
149
  cwd: resolvedPath,
142
150
  stdio: "inherit",
151
+ shell: isWindows ? "cmd.exe" : "/bin/sh",
152
+ windowsHide: true,
143
153
  });
144
154
  } catch (error) {
145
155
  console.error(
146
- chalk.red(`Failed to execute predeploy command: ${command}`)
156
+ chalk.red(`Failed to execute predeploy command: ${command}`),
157
+ error
147
158
  );
148
- throw error;
159
+ throw new Error(``);
149
160
  }
150
161
  }
151
162
  }
152
163
 
164
+ // Only create function if it doesn't exist
153
165
  if (!functionExists) {
154
166
  await createFunction(
155
167
  client,
@@ -35,6 +35,7 @@ import {
35
35
  createFunctionTemplate,
36
36
  deleteFunction,
37
37
  downloadLatestFunctionDeployment,
38
+ getFunction,
38
39
  listFunctions,
39
40
  listSpecifications,
40
41
  } from "./functions/methods.js";
@@ -397,8 +398,19 @@ export class InteractiveCLI {
397
398
  join(process.cwd(), functionName), // ./functionName
398
399
  join(basePath, "functions", functionName), // appwriteFolder/functions/functionName
399
400
  join(basePath, functionName), // appwriteFolder/functionName
401
+ join(basePath, functionName.toLowerCase()), // appwriteFolder/functionName.toLowerCase()
402
+ join(basePath, functionName.toLowerCase().replace(/\s+/g, "")), // appwriteFolder/functionName.toLowerCase().replace(/\s+/g, "")
403
+ join(process.cwd(), functionName.toLowerCase()), // ./functionName.toLowerCase()
400
404
  ];
401
405
 
406
+ // Create different variations of the function name for comparison
407
+ const functionNameVariations = new Set([
408
+ functionName.toLowerCase(),
409
+ functionName.toLowerCase().replace(/\s+/g, ""),
410
+ functionName.toLowerCase().replace(/[^a-z0-9]/g, ""),
411
+ functionName.toLowerCase().replace(/[-_\s]+/g, ""),
412
+ ]);
413
+
402
414
  // Check common locations first
403
415
  for (const path of commonPaths) {
404
416
  try {
@@ -444,7 +456,19 @@ export class InteractiveCLI {
444
456
  !entry.name.startsWith(".") &&
445
457
  entry.name !== "node_modules"
446
458
  ) {
447
- if (entry.name === functionName) {
459
+ const entryNameVariations = new Set([
460
+ entry.name.toLowerCase(),
461
+ entry.name.toLowerCase().replace(/\s+/g, ""),
462
+ entry.name.toLowerCase().replace(/[^a-z0-9]/g, ""),
463
+ entry.name.toLowerCase().replace(/[-_\s]+/g, ""),
464
+ ]);
465
+
466
+ // Check if any variation of the entry name matches any variation of the function name
467
+ const hasMatch = [...functionNameVariations].some((fnVar) =>
468
+ [...entryNameVariations].includes(fnVar)
469
+ );
470
+
471
+ if (hasMatch) {
448
472
  console.log(chalk.green(`Found function at: ${fullPath}`));
449
473
  return fullPath;
450
474
  }
@@ -486,6 +510,11 @@ export class InteractiveCLI {
486
510
  return;
487
511
  }
488
512
 
513
+ // Ensure functions array exists
514
+ if (!this.controller.config.functions) {
515
+ this.controller.config.functions = [];
516
+ }
517
+
489
518
  let functionPath = join(
490
519
  this.controller.getAppwriteFolderPath(),
491
520
  "functions",
@@ -500,7 +529,7 @@ export class InteractiveCLI {
500
529
  );
501
530
  const foundPath = await this.findFunctionInSubdirectories(
502
531
  this.controller.getAppwriteFolderPath(),
503
- functionConfig.name
532
+ functionConfig.name.toLowerCase()
504
533
  );
505
534
 
506
535
  if (foundPath) {
@@ -508,18 +537,13 @@ export class InteractiveCLI {
508
537
  functionPath = foundPath;
509
538
  functionConfig.dirPath = foundPath;
510
539
  } else {
511
- console.log(
512
- chalk.yellow(
513
- `Function ${functionConfig.name} not found locally in any subdirectory`
514
- )
515
- );
516
-
517
540
  const { shouldDownload } = await inquirer.prompt([
518
541
  {
519
542
  type: "confirm",
520
543
  name: "shouldDownload",
521
- message: "Would you like to download the latest deployment?",
522
- default: true,
544
+ message:
545
+ "Function not found locally. Would you like to download the latest deployment?",
546
+ default: false,
523
547
  },
524
548
  ]);
525
549
 
@@ -536,53 +560,19 @@ export class InteractiveCLI {
536
560
  chalk.green(`✨ Function downloaded to ${downloadedPath}`)
537
561
  );
538
562
 
539
- // Update the config and functions array safely
540
- this.controller.config.functions =
541
- this.controller.config.functions || [];
542
-
543
- const newFunction = {
544
- $id: remoteFunction.$id,
545
- name: remoteFunction.name,
546
- runtime: remoteFunction.runtime as Runtime,
547
- execute: remoteFunction.execute || [],
548
- events: remoteFunction.events || [],
549
- schedule: remoteFunction.schedule || "",
550
- timeout: remoteFunction.timeout || 15,
551
- enabled: remoteFunction.enabled !== false,
552
- logging: remoteFunction.logging !== false,
553
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
554
- commands: remoteFunction.commands || "npm install",
555
- dirPath: downloadedPath,
556
- scopes: (remoteFunction.scopes || []) as FunctionScope[],
557
- installationId: remoteFunction.installationId,
558
- providerRepositoryId: remoteFunction.providerRepositoryId,
559
- providerBranch: remoteFunction.providerBranch,
560
- providerSilentMode: remoteFunction.providerSilentMode,
561
- providerRootDirectory: remoteFunction.providerRootDirectory,
562
- specification: remoteFunction.specification as Specification,
563
- };
564
-
565
563
  const existingIndex = this.controller.config.functions.findIndex(
566
564
  (f) => f?.$id === remoteFunction.$id
567
565
  );
568
566
 
569
567
  if (existingIndex >= 0) {
570
- this.controller.config.functions[existingIndex] = newFunction;
571
- } else {
572
- this.controller.config.functions.push(newFunction);
568
+ // Only update the dirPath if function exists
569
+ this.controller.config.functions[existingIndex].dirPath =
570
+ downloadedPath;
573
571
  }
574
572
 
575
- const schemaGenerator = new SchemaGenerator(
576
- this.controller.config,
577
- this.controller.getAppwriteFolderPath()
578
- );
579
- schemaGenerator.updateConfig(this.controller.config);
580
- console.log(
581
- chalk.green("✨ Updated appwriteConfig.ts with new function")
582
- );
583
-
584
573
  await this.controller.reloadConfig();
585
574
  functionConfig.dirPath = downloadedPath;
575
+ functionPath = downloadedPath;
586
576
  } catch (error) {
587
577
  console.error(
588
578
  chalk.red("Failed to download function deployment:"),
@@ -591,7 +581,11 @@ export class InteractiveCLI {
591
581
  return;
592
582
  }
593
583
  } else {
594
- console.log(chalk.yellow("Deployment cancelled"));
584
+ console.log(
585
+ chalk.red(
586
+ `Function ${functionConfig.name} not found locally. Cannot deploy.`
587
+ )
588
+ );
595
589
  return;
596
590
  }
597
591
  }
@@ -602,11 +596,16 @@ export class InteractiveCLI {
602
596
  return;
603
597
  }
604
598
 
605
- await deployLocalFunction(
606
- this.controller.appwriteServer,
607
- functionConfig.name,
608
- functionConfig
609
- );
599
+ try {
600
+ await deployLocalFunction(
601
+ this.controller.appwriteServer,
602
+ functionConfig.name,
603
+ functionConfig
604
+ );
605
+ console.log(chalk.green("✨ Function deployed successfully!"));
606
+ } catch (error) {
607
+ console.error(chalk.red("Failed to deploy function:"), error);
608
+ }
610
609
  }
611
610
 
612
611
  private async deleteFunction(): Promise<void> {
@@ -638,67 +637,42 @@ export class InteractiveCLI {
638
637
 
639
638
  private async selectFunctions(
640
639
  message: string,
641
- multiSelect = true,
642
- preferLocal = false
640
+ multiple: boolean = true,
641
+ includeRemote: boolean = false
643
642
  ): Promise<AppwriteFunction[]> {
644
- await this.initControllerIfNeeded();
645
-
646
- const configFunctions = this.getLocalFunctions();
647
- let remoteFunctions: Models.Function[] = [];
648
-
649
- try {
650
- const functions = await this.controller!.listAllFunctions();
651
- remoteFunctions = functions;
652
- } catch (error) {
653
- console.log(
654
- chalk.yellow(
655
- `Note: Remote functions not available, using only local functions`
656
- )
657
- );
658
- }
659
-
660
- // Combine functions based on whether we're deploying or not
661
- const allFunctions = preferLocal
662
- ? [
663
- ...configFunctions,
664
- ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
665
- ]
666
- : [
667
- ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
668
- ...configFunctions.filter(
669
- (f) => !remoteFunctions.some((rf) => rf.name === f.name)
670
- ),
671
- ];
672
-
673
- if (allFunctions.length === 0) {
674
- console.log(chalk.red("No functions available"));
675
- return [];
676
- }
643
+ const remoteFunctions = includeRemote
644
+ ? await listFunctions(this.controller!.appwriteServer!, [
645
+ Query.limit(1000),
646
+ ])
647
+ : { functions: [] };
648
+ const localFunctions = this.getLocalFunctions();
677
649
 
678
- const choices = allFunctions
679
- .sort((a, b) => a.name.localeCompare(b.name))
680
- .map((func) => ({
681
- name: func.name,
682
- value: func,
683
- }));
650
+ // Combine functions, preferring local ones
651
+ const allFunctions = [
652
+ ...localFunctions,
653
+ ...remoteFunctions.functions.filter(
654
+ (rf) => !localFunctions.some((lf) => lf.name === rf.name)
655
+ ),
656
+ ];
684
657
 
685
658
  const { selectedFunctions } = await inquirer.prompt([
686
659
  {
687
- type: multiSelect ? "checkbox" : "list",
660
+ type: multiple ? "checkbox" : "list",
688
661
  name: "selectedFunctions",
689
- message: chalk.blue(message),
690
- choices,
662
+ message,
663
+ choices: allFunctions.map((f) => ({
664
+ name: `${f.name} (${f.$id})${
665
+ localFunctions.some((lf) => lf.name === f.name)
666
+ ? " (Local)"
667
+ : " (Remote)"
668
+ }`,
669
+ value: f,
670
+ })),
691
671
  loop: true,
692
- pageSize: 10,
693
672
  },
694
673
  ]);
695
674
 
696
- // For single selection, ensure we return an array
697
- if (!multiSelect) {
698
- return selectedFunctions ? [selectedFunctions] : [];
699
- }
700
-
701
- return selectedFunctions || [];
675
+ return multiple ? selectedFunctions : [selectedFunctions];
702
676
  }
703
677
 
704
678
  private getLocalFunctions(): AppwriteFunction[] {
@@ -1102,23 +1076,54 @@ export class InteractiveCLI {
1102
1076
  const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
1103
1077
 
1104
1078
  if (hasLocal && hasRemote) {
1079
+ // First try to find the function locally
1080
+ let functionPath = join(
1081
+ this.controller!.getAppwriteFolderPath(),
1082
+ "functions",
1083
+ func.name
1084
+ );
1085
+
1086
+ if (!fs.existsSync(functionPath)) {
1087
+ console.log(
1088
+ chalk.yellow(
1089
+ `Function not found in primary location, searching subdirectories...`
1090
+ )
1091
+ );
1092
+ const foundPath = await this.findFunctionInSubdirectories(
1093
+ this.controller!.getAppwriteFolderPath(),
1094
+ func.name
1095
+ );
1096
+
1097
+ if (foundPath) {
1098
+ console.log(chalk.green(`Found function at: ${foundPath}`));
1099
+ functionPath = foundPath;
1100
+ }
1101
+ }
1102
+
1105
1103
  const { preference } = await inquirer.prompt([
1106
1104
  {
1107
1105
  type: "list",
1108
1106
  name: "preference",
1109
- message: `Function "${func.name}" exists both locally and remotely. What would you like to do?`,
1107
+ message: `Function "${func.name}" ${
1108
+ functionPath ? "found at " + functionPath : "not found locally"
1109
+ }. What would you like to do?`,
1110
1110
  choices: [
1111
- {
1112
- name: "Keep local version (deploy to remote)",
1113
- value: "local",
1114
- },
1111
+ ...(functionPath
1112
+ ? [
1113
+ {
1114
+ name: "Keep local version (deploy to remote)",
1115
+ value: "local",
1116
+ },
1117
+ ]
1118
+ : []),
1115
1119
  { name: "Use remote version (download)", value: "remote" },
1120
+ { name: "Update config only", value: "config" },
1116
1121
  { name: "Skip this function", value: "skip" },
1117
1122
  ],
1118
1123
  },
1119
1124
  ]);
1120
1125
 
1121
- if (preference === "local") {
1126
+ if (preference === "local" && functionPath) {
1122
1127
  await this.controller!.deployFunction(func.name);
1123
1128
  } else if (preference === "remote") {
1124
1129
  await downloadLatestFunctionDeployment(
@@ -1126,39 +1131,152 @@ export class InteractiveCLI {
1126
1131
  func.$id,
1127
1132
  join(this.controller!.getAppwriteFolderPath(), "functions")
1128
1133
  );
1134
+ } else if (preference === "config") {
1135
+ const remoteFunction = await getFunction(
1136
+ this.controller!.appwriteServer!,
1137
+ func.$id
1138
+ );
1139
+
1140
+ const newFunction = {
1141
+ $id: remoteFunction.$id,
1142
+ name: remoteFunction.name,
1143
+ runtime: remoteFunction.runtime as Runtime,
1144
+ execute: remoteFunction.execute || [],
1145
+ events: remoteFunction.events || [],
1146
+ schedule: remoteFunction.schedule || "",
1147
+ timeout: remoteFunction.timeout || 15,
1148
+ enabled: remoteFunction.enabled !== false,
1149
+ logging: remoteFunction.logging !== false,
1150
+ entrypoint: remoteFunction.entrypoint || "src/index.ts",
1151
+ commands: remoteFunction.commands || "npm install",
1152
+ scopes: (remoteFunction.scopes || []) as FunctionScope[],
1153
+ installationId: remoteFunction.installationId,
1154
+ providerRepositoryId: remoteFunction.providerRepositoryId,
1155
+ providerBranch: remoteFunction.providerBranch,
1156
+ providerSilentMode: remoteFunction.providerSilentMode,
1157
+ providerRootDirectory: remoteFunction.providerRootDirectory,
1158
+ specification: remoteFunction.specification as Specification,
1159
+ };
1160
+
1161
+ const existingIndex = this.controller!.config!.functions!.findIndex(
1162
+ (f) => f.$id === remoteFunction.$id
1163
+ );
1164
+
1165
+ if (existingIndex >= 0) {
1166
+ this.controller!.config!.functions![existingIndex] = newFunction;
1167
+ } else {
1168
+ this.controller!.config!.functions!.push(newFunction);
1169
+ }
1170
+ console.log(
1171
+ chalk.green(`Updated config for function: ${func.name}`)
1172
+ );
1129
1173
  }
1130
1174
  } else if (hasLocal) {
1131
- const { deploy } = await inquirer.prompt([
1175
+ // Similar check for local-only functions
1176
+ let functionPath = join(
1177
+ this.controller!.getAppwriteFolderPath(),
1178
+ "functions",
1179
+ func.name
1180
+ );
1181
+
1182
+ if (!fs.existsSync(functionPath)) {
1183
+ const foundPath = await this.findFunctionInSubdirectories(
1184
+ this.controller!.getAppwriteFolderPath(),
1185
+ func.name
1186
+ );
1187
+
1188
+ if (foundPath) {
1189
+ functionPath = foundPath;
1190
+ }
1191
+ }
1192
+
1193
+ const { action } = await inquirer.prompt([
1132
1194
  {
1133
- type: "confirm",
1134
- name: "deploy",
1135
- message: `Function "${func.name}" exists only locally. Deploy to remote?`,
1136
- default: true,
1195
+ type: "list",
1196
+ name: "action",
1197
+ message: `Function "${func.name}" ${
1198
+ functionPath ? "found at " + functionPath : "not found locally"
1199
+ }. What would you like to do?`,
1200
+ choices: [
1201
+ ...(functionPath
1202
+ ? [
1203
+ {
1204
+ name: "Deploy to remote",
1205
+ value: "deploy",
1206
+ },
1207
+ ]
1208
+ : []),
1209
+ { name: "Skip this function", value: "skip" },
1210
+ ],
1137
1211
  },
1138
1212
  ]);
1139
1213
 
1140
- if (deploy) {
1214
+ if (action === "deploy" && functionPath) {
1141
1215
  await this.controller!.deployFunction(func.name);
1142
1216
  }
1143
1217
  } else if (hasRemote) {
1144
- const { download } = await inquirer.prompt([
1218
+ const { action } = await inquirer.prompt([
1145
1219
  {
1146
- type: "confirm",
1147
- name: "download",
1148
- message: `Function "${func.name}" exists only remotely. Download locally?`,
1149
- default: true,
1220
+ type: "list",
1221
+ name: "action",
1222
+ message: `Function "${func.name}" exists only remotely. What would you like to do?`,
1223
+ choices: [
1224
+ { name: "Update config only", value: "config" },
1225
+ { name: "Download locally", value: "download" },
1226
+ { name: "Skip this function", value: "skip" },
1227
+ ],
1150
1228
  },
1151
1229
  ]);
1152
1230
 
1153
- if (download) {
1231
+ if (action === "download") {
1154
1232
  await downloadLatestFunctionDeployment(
1155
1233
  this.controller!.appwriteServer!,
1156
1234
  func.$id,
1157
1235
  join(this.controller!.getAppwriteFolderPath(), "functions")
1158
1236
  );
1237
+ } else if (action === "config") {
1238
+ const remoteFunction = await getFunction(
1239
+ this.controller!.appwriteServer!,
1240
+ func.$id
1241
+ );
1242
+
1243
+ const newFunction = {
1244
+ $id: remoteFunction.$id,
1245
+ name: remoteFunction.name,
1246
+ runtime: remoteFunction.runtime as Runtime,
1247
+ execute: remoteFunction.execute || [],
1248
+ events: remoteFunction.events || [],
1249
+ schedule: remoteFunction.schedule || "",
1250
+ timeout: remoteFunction.timeout || 15,
1251
+ enabled: remoteFunction.enabled !== false,
1252
+ logging: remoteFunction.logging !== false,
1253
+ entrypoint: remoteFunction.entrypoint || "src/index.ts",
1254
+ commands: remoteFunction.commands || "npm install",
1255
+ scopes: (remoteFunction.scopes || []) as FunctionScope[],
1256
+ installationId: remoteFunction.installationId,
1257
+ providerRepositoryId: remoteFunction.providerRepositoryId,
1258
+ providerBranch: remoteFunction.providerBranch,
1259
+ providerSilentMode: remoteFunction.providerSilentMode,
1260
+ providerRootDirectory: remoteFunction.providerRootDirectory,
1261
+ specification: remoteFunction.specification as Specification,
1262
+ };
1263
+
1264
+ this.controller!.config!.functions =
1265
+ this.controller!.config!.functions || [];
1266
+ this.controller!.config!.functions.push(newFunction);
1267
+ console.log(
1268
+ chalk.green(`Added config for remote function: ${func.name}`)
1269
+ );
1159
1270
  }
1160
1271
  }
1161
1272
  }
1273
+
1274
+ // Update schemas after all changes
1275
+ const schemaGenerator = new SchemaGenerator(
1276
+ this.controller!.config!,
1277
+ this.controller!.getAppwriteFolderPath()
1278
+ );
1279
+ schemaGenerator.updateConfig(this.controller!.config!);
1162
1280
  }
1163
1281
 
1164
1282
  console.log(chalk.green("✨ Configurations synchronized successfully!"));