appwrite-utils-cli 0.9.993 → 0.9.995

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.
Files changed (46) hide show
  1. package/README.md +1 -2
  2. package/dist/collections/methods.js +6 -4
  3. package/dist/functions/deployments.d.ts +4 -0
  4. package/dist/functions/deployments.js +114 -0
  5. package/dist/functions/methods.d.ts +13 -8
  6. package/dist/functions/methods.js +74 -20
  7. package/dist/functions/templates/count-docs-in-collection/src/main.d.ts +21 -0
  8. package/dist/functions/templates/count-docs-in-collection/src/main.js +114 -0
  9. package/dist/functions/templates/count-docs-in-collection/src/request.d.ts +15 -0
  10. package/dist/functions/templates/count-docs-in-collection/src/request.js +6 -0
  11. package/dist/functions/templates/typescript-node/src/index.d.ts +11 -0
  12. package/dist/functions/templates/typescript-node/src/index.js +11 -0
  13. package/dist/interactiveCLI.d.ts +6 -0
  14. package/dist/interactiveCLI.js +437 -32
  15. package/dist/main.js +0 -0
  16. package/dist/migrations/appwriteToX.js +32 -1
  17. package/dist/migrations/schemaStrings.d.ts +1 -0
  18. package/dist/migrations/schemaStrings.js +74 -2
  19. package/dist/migrations/transfer.js +112 -123
  20. package/dist/utils/loadConfigs.d.ts +1 -0
  21. package/dist/utils/loadConfigs.js +19 -0
  22. package/dist/utils/schemaStrings.js +27 -0
  23. package/dist/utilsController.d.ts +5 -1
  24. package/dist/utilsController.js +54 -2
  25. package/package.json +57 -55
  26. package/src/collections/methods.ts +29 -10
  27. package/src/functions/deployments.ts +190 -0
  28. package/src/functions/methods.ts +295 -235
  29. package/src/functions/templates/count-docs-in-collection/README.md +54 -0
  30. package/src/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  31. package/src/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  32. package/src/functions/templates/poetry/README.md +30 -0
  33. package/src/functions/templates/poetry/pyproject.toml +16 -0
  34. package/src/functions/templates/poetry/src/__init__.py +0 -0
  35. package/src/functions/templates/poetry/src/index.py +16 -0
  36. package/src/functions/templates/typescript-node/README.md +32 -0
  37. package/src/functions/templates/typescript-node/src/index.ts +23 -0
  38. package/src/interactiveCLI.ts +606 -47
  39. package/src/migrations/appwriteToX.ts +44 -1
  40. package/src/migrations/schemaStrings.ts +83 -2
  41. package/src/migrations/transfer.ts +280 -207
  42. package/src/setupController.ts +41 -41
  43. package/src/utils/loadConfigs.ts +24 -0
  44. package/src/utils/schemaStrings.ts +27 -0
  45. package/src/utilsController.ts +63 -3
  46. package/tsconfig.json +8 -1
@@ -11,26 +11,47 @@ import {
11
11
  type Models,
12
12
  Compression,
13
13
  Query,
14
+ Functions,
14
15
  } from "node-appwrite";
15
16
  import { getClient } from "./utils/getClientFromConfig.js";
16
17
  import type { TransferOptions } from "./migrations/transfer.js";
17
18
  import {
19
+ AppwriteFunctionSchema,
18
20
  parseAttribute,
19
21
  PermissionToAppwritePermission,
22
+ RuntimeSchema,
23
+ permissionSchema,
20
24
  type AppwriteConfig,
25
+ type AppwriteFunction,
21
26
  type ConfigDatabases,
27
+ type Runtime,
28
+ type Specification,
29
+ type FunctionScope,
22
30
  } from "appwrite-utils";
23
31
  import { ulid } from "ulidx";
24
32
  import chalk from "chalk";
25
33
  import { DateTime } from "luxon";
26
- import { listFunctions, listSpecifications } from "./functions/methods.js";
34
+ import {
35
+ createFunctionTemplate,
36
+ deleteFunction,
37
+ downloadLatestFunctionDeployment,
38
+ listFunctions,
39
+ listSpecifications,
40
+ } from "./functions/methods.js";
41
+ import { deployLocalFunction } from "./functions/deployments.js";
42
+ import { join } from "node:path";
43
+ import fs from "node:fs";
44
+ import { SchemaGenerator } from "./migrations/schemaStrings.js";
27
45
 
28
46
  enum CHOICES {
29
47
  CREATE_COLLECTION_CONFIG = "Create collection config file",
48
+ CREATE_FUNCTION = "Create a new function, from scratch or using a template",
49
+ DEPLOY_FUNCTION = "Deploy function",
50
+ DELETE_FUNCTION = "Delete function",
30
51
  SETUP_DIRS_FILES = "Setup directories and files",
31
52
  SETUP_DIRS_FILES_WITH_EXAMPLE_DATA = "Setup directories and files with example data",
32
53
  SYNC_DB = "Push local config to Appwrite",
33
- SYNCHRONIZE_CONFIGURATIONS = "Synchronize configurations",
54
+ SYNCHRONIZE_CONFIGURATIONS = "Synchronize configurations - Pull from Appwrite and write to local config",
34
55
  TRANSFER_DATA = "Transfer data",
35
56
  BACKUP_DATABASE = "Backup database",
36
57
  WIPE_DATABASE = "Wipe database",
@@ -72,6 +93,18 @@ export class InteractiveCLI {
72
93
  await this.initControllerIfNeeded();
73
94
  await this.createCollectionConfig();
74
95
  break;
96
+ case CHOICES.CREATE_FUNCTION:
97
+ await this.initControllerIfNeeded();
98
+ await this.createFunction();
99
+ break;
100
+ case CHOICES.DEPLOY_FUNCTION:
101
+ await this.initControllerIfNeeded();
102
+ await this.deployFunction();
103
+ break;
104
+ case CHOICES.DELETE_FUNCTION:
105
+ await this.initControllerIfNeeded();
106
+ await this.deleteFunction();
107
+ break;
75
108
  case CHOICES.SETUP_DIRS_FILES:
76
109
  await setupDirsFiles(false, this.currentDir);
77
110
  break;
@@ -270,6 +303,389 @@ export class InteractiveCLI {
270
303
  return selectedCollections;
271
304
  }
272
305
 
306
+ private async createFunction(): Promise<void> {
307
+ const { name } = await inquirer.prompt([
308
+ {
309
+ type: "input",
310
+ name: "name",
311
+ message: "Function name:",
312
+ validate: (input) => input.length > 0,
313
+ },
314
+ ]);
315
+
316
+ const { template } = await inquirer.prompt([
317
+ {
318
+ type: "list",
319
+ name: "template",
320
+ message: "Select a template:",
321
+ choices: [
322
+ "typescript-node",
323
+ "poetry",
324
+ "count-docs-in-collection",
325
+ "none",
326
+ ],
327
+ },
328
+ ]);
329
+
330
+ const { runtime } = await inquirer.prompt([
331
+ {
332
+ type: "list",
333
+ name: "runtime",
334
+ message: "Select runtime:",
335
+ choices: Object.values(RuntimeSchema.Values),
336
+ },
337
+ ]);
338
+
339
+ const specifications = await listSpecifications(
340
+ this.controller!.appwriteServer!
341
+ );
342
+ const { specification } = await inquirer.prompt([
343
+ {
344
+ type: "list",
345
+ name: "specification",
346
+ message: "Select specification:",
347
+ choices: [
348
+ { name: "None", value: undefined },
349
+ ...specifications.specifications.map((s) => ({
350
+ name: s.slug,
351
+ value: s.slug,
352
+ })),
353
+ ],
354
+ },
355
+ ]);
356
+
357
+ const functionConfig: AppwriteFunction = {
358
+ $id: ulid(),
359
+ name,
360
+ runtime,
361
+ events: [],
362
+ execute: ["any"],
363
+ enabled: true,
364
+ logging: true,
365
+ entrypoint: template === "none" ? "src/index.ts" : undefined,
366
+ specification,
367
+ predeployCommands: template.includes("typescript")
368
+ ? ["npm install", "npm run build"]
369
+ : undefined,
370
+ deployDir: template.includes("typescript") ? "dist" : undefined,
371
+ };
372
+
373
+ if (template !== "none") {
374
+ await createFunctionTemplate(
375
+ template as "typescript-node" | "poetry" | "count-docs-in-collection",
376
+ name,
377
+ "./functions"
378
+ );
379
+ }
380
+
381
+ // Add to config
382
+ if (!this.controller!.config!.functions) {
383
+ this.controller!.config!.functions = [];
384
+ }
385
+ this.controller!.config!.functions.push(functionConfig);
386
+
387
+ console.log(chalk.green("✨ Function created successfully!"));
388
+ }
389
+
390
+ private async findFunctionInSubdirectories(
391
+ basePath: string,
392
+ functionName: string
393
+ ): Promise<string | null> {
394
+ const queue = [basePath];
395
+
396
+ while (queue.length > 0) {
397
+ const currentPath = queue.shift()!;
398
+
399
+ try {
400
+ const entries = await fs.promises.readdir(currentPath, {
401
+ withFileTypes: true,
402
+ });
403
+
404
+ // Check if function exists in current directory
405
+ const functionPath = join(currentPath, functionName);
406
+ if (fs.existsSync(functionPath)) {
407
+ return functionPath;
408
+ }
409
+
410
+ // Add subdirectories to queue
411
+ for (const entry of entries) {
412
+ if (entry.isDirectory()) {
413
+ queue.push(join(currentPath, entry.name));
414
+ }
415
+ }
416
+ } catch (error) {
417
+ console.log(
418
+ chalk.yellow(`Skipping inaccessible directory: ${currentPath}`)
419
+ );
420
+ }
421
+ }
422
+
423
+ return null;
424
+ }
425
+
426
+ private async deployFunction(): Promise<void> {
427
+ await this.initControllerIfNeeded();
428
+ if (!this.controller?.config) {
429
+ console.log(chalk.red("Failed to initialize controller or load config"));
430
+ return;
431
+ }
432
+
433
+ const functions = await this.selectFunctions(
434
+ "Select function to deploy:",
435
+ false,
436
+ true
437
+ );
438
+
439
+ if (!functions?.length) {
440
+ console.log(chalk.red("No function selected"));
441
+ return;
442
+ }
443
+
444
+ const functionConfig = functions[0];
445
+ if (!functionConfig) {
446
+ console.log(chalk.red("Invalid function configuration"));
447
+ return;
448
+ }
449
+
450
+ let functionPath = join(
451
+ this.controller.getAppwriteFolderPath(),
452
+ "functions",
453
+ functionConfig.name
454
+ );
455
+
456
+ if (!fs.existsSync(functionPath)) {
457
+ console.log(
458
+ chalk.yellow(
459
+ `Function not found in primary location, searching subdirectories...`
460
+ )
461
+ );
462
+ const foundPath = await this.findFunctionInSubdirectories(
463
+ this.controller.getAppwriteFolderPath(),
464
+ functionConfig.name
465
+ );
466
+
467
+ if (foundPath) {
468
+ console.log(chalk.green(`Found function at: ${foundPath}`));
469
+ functionPath = foundPath;
470
+ functionConfig.dirPath = foundPath;
471
+ } else {
472
+ console.log(
473
+ chalk.yellow(
474
+ `Function ${functionConfig.name} not found locally in any subdirectory`
475
+ )
476
+ );
477
+
478
+ const { shouldDownload } = await inquirer.prompt([
479
+ {
480
+ type: "confirm",
481
+ name: "shouldDownload",
482
+ message: "Would you like to download the latest deployment?",
483
+ default: true,
484
+ },
485
+ ]);
486
+
487
+ if (shouldDownload) {
488
+ try {
489
+ console.log(chalk.blue("Downloading latest deployment..."));
490
+ const { path: downloadedPath, function: remoteFunction } =
491
+ await downloadLatestFunctionDeployment(
492
+ this.controller.appwriteServer!,
493
+ functionConfig.$id,
494
+ join(this.controller.getAppwriteFolderPath(), "functions")
495
+ );
496
+ console.log(
497
+ chalk.green(`✨ Function downloaded to ${downloadedPath}`)
498
+ );
499
+
500
+ // Update the config and functions array safely
501
+ this.controller.config.functions =
502
+ this.controller.config.functions || [];
503
+
504
+ const newFunction = {
505
+ $id: remoteFunction.$id,
506
+ name: remoteFunction.name,
507
+ runtime: remoteFunction.runtime as Runtime,
508
+ execute: remoteFunction.execute || [],
509
+ events: remoteFunction.events || [],
510
+ schedule: remoteFunction.schedule || "",
511
+ timeout: remoteFunction.timeout || 15,
512
+ enabled: remoteFunction.enabled !== false,
513
+ logging: remoteFunction.logging !== false,
514
+ entrypoint: remoteFunction.entrypoint || "src/index.ts",
515
+ commands: remoteFunction.commands || "npm install",
516
+ dirPath: downloadedPath,
517
+ scopes: (remoteFunction.scopes || []) as FunctionScope[],
518
+ installationId: remoteFunction.installationId,
519
+ providerRepositoryId: remoteFunction.providerRepositoryId,
520
+ providerBranch: remoteFunction.providerBranch,
521
+ providerSilentMode: remoteFunction.providerSilentMode,
522
+ providerRootDirectory: remoteFunction.providerRootDirectory,
523
+ specification: remoteFunction.specification as Specification,
524
+ };
525
+
526
+ const existingIndex = this.controller.config.functions.findIndex(
527
+ (f) => f?.$id === remoteFunction.$id
528
+ );
529
+
530
+ if (existingIndex >= 0) {
531
+ this.controller.config.functions[existingIndex] = newFunction;
532
+ } else {
533
+ this.controller.config.functions.push(newFunction);
534
+ }
535
+
536
+ const schemaGenerator = new SchemaGenerator(
537
+ this.controller.config,
538
+ this.controller.getAppwriteFolderPath()
539
+ );
540
+ schemaGenerator.updateConfig(this.controller.config);
541
+ console.log(
542
+ chalk.green("✨ Updated appwriteConfig.ts with new function")
543
+ );
544
+
545
+ await this.controller.reloadConfig();
546
+ functionConfig.dirPath = downloadedPath;
547
+ } catch (error) {
548
+ console.error(
549
+ chalk.red("Failed to download function deployment:"),
550
+ error
551
+ );
552
+ return;
553
+ }
554
+ } else {
555
+ console.log(chalk.yellow("Deployment cancelled"));
556
+ return;
557
+ }
558
+ }
559
+ }
560
+
561
+ if (!this.controller.appwriteServer) {
562
+ console.log(chalk.red("Appwrite server not initialized"));
563
+ return;
564
+ }
565
+
566
+ await deployLocalFunction(
567
+ this.controller.appwriteServer,
568
+ functionConfig.name,
569
+ functionConfig
570
+ );
571
+ }
572
+
573
+ private async deleteFunction(): Promise<void> {
574
+ const functions = await this.selectFunctions(
575
+ "Select functions to delete:",
576
+ true,
577
+ false
578
+ );
579
+
580
+ if (!functions.length) {
581
+ console.log(chalk.red("No functions selected"));
582
+ return;
583
+ }
584
+
585
+ for (const func of functions) {
586
+ try {
587
+ await deleteFunction(this.controller!.appwriteServer!, func.$id);
588
+ console.log(
589
+ chalk.green(`✨ Function ${func.name} deleted successfully!`)
590
+ );
591
+ } catch (error) {
592
+ console.error(
593
+ chalk.red(`Failed to delete function ${func.name}:`),
594
+ error
595
+ );
596
+ }
597
+ }
598
+ }
599
+
600
+ private async selectFunctions(
601
+ message: string,
602
+ multiSelect = true,
603
+ preferLocal = false
604
+ ): Promise<AppwriteFunction[]> {
605
+ await this.initControllerIfNeeded();
606
+
607
+ const configFunctions = this.getLocalFunctions();
608
+ let remoteFunctions: Models.Function[] = [];
609
+
610
+ try {
611
+ const functions = await this.controller!.listAllFunctions();
612
+ remoteFunctions = functions;
613
+ } catch (error) {
614
+ console.log(
615
+ chalk.yellow(
616
+ `Note: Remote functions not available, using only local functions`
617
+ )
618
+ );
619
+ }
620
+
621
+ // Combine functions based on whether we're deploying or not
622
+ const allFunctions = preferLocal
623
+ ? [
624
+ ...configFunctions,
625
+ ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
626
+ ]
627
+ : [
628
+ ...remoteFunctions.map((f) => AppwriteFunctionSchema.parse(f)),
629
+ ...configFunctions.filter(
630
+ (f) => !remoteFunctions.some((rf) => rf.name === f.name)
631
+ ),
632
+ ];
633
+
634
+ if (allFunctions.length === 0) {
635
+ console.log(chalk.red("No functions available"));
636
+ return [];
637
+ }
638
+
639
+ const choices = allFunctions
640
+ .sort((a, b) => a.name.localeCompare(b.name))
641
+ .map((func) => ({
642
+ name: func.name,
643
+ value: func,
644
+ }));
645
+
646
+ const { selectedFunctions } = await inquirer.prompt([
647
+ {
648
+ type: multiSelect ? "checkbox" : "list",
649
+ name: "selectedFunctions",
650
+ message: chalk.blue(message),
651
+ choices,
652
+ loop: true,
653
+ pageSize: 10,
654
+ },
655
+ ]);
656
+
657
+ // For single selection, ensure we return an array
658
+ if (!multiSelect) {
659
+ return selectedFunctions ? [selectedFunctions] : [];
660
+ }
661
+
662
+ return selectedFunctions || [];
663
+ }
664
+
665
+ private getLocalFunctions(): AppwriteFunction[] {
666
+ const configFunctions = this.controller!.config?.functions || [];
667
+ return configFunctions.map((f) => ({
668
+ $id: f.$id || ulid(),
669
+ $createdAt: DateTime.now().toISO(),
670
+ $updatedAt: DateTime.now().toISO(),
671
+ name: f.name,
672
+ runtime: f.runtime,
673
+ execute: f.execute || ["any"],
674
+ events: f.events || [],
675
+ schedule: f.schedule || "",
676
+ timeout: f.timeout || 15,
677
+ enabled: f.enabled !== false,
678
+ logging: f.logging !== false,
679
+ entrypoint: f.entrypoint || "src/index.ts",
680
+ commands: f.commands || "npm install",
681
+ path: f.dirPath || `functions/${f.name}`,
682
+ ...(f.specification ? { specification: f.specification } : {}),
683
+ ...(f.predeployCommands
684
+ ? { predeployCommands: f.predeployCommands }
685
+ : {}),
686
+ ...(f.deployDir ? { deployDir: f.deployDir } : {}),
687
+ }));
688
+ }
273
689
  private async selectBuckets(
274
690
  buckets: Models.Bucket[],
275
691
  message: string,
@@ -407,15 +823,16 @@ export class InteractiveCLI {
407
823
  ]);
408
824
 
409
825
  if (action === "assign") {
410
- const [selectedBucket] = await this.selectBuckets(
826
+ const selectedBuckets = await this.selectBuckets(
411
827
  allBuckets.buckets.filter(
412
828
  (b) => !globalBuckets.some((gb) => gb.$id === b.$id)
413
829
  ),
414
830
  `Select a bucket for the database "${database.name}":`,
415
- false
831
+ false // multiSelect = false
416
832
  );
417
833
 
418
- if (selectedBucket) {
834
+ if (selectedBuckets.length > 0) {
835
+ const selectedBucket = selectedBuckets[0];
419
836
  database.bucket = {
420
837
  $id: selectedBucket.$id,
421
838
  name: selectedBucket.name,
@@ -425,6 +842,9 @@ export class InteractiveCLI {
425
842
  compression: selectedBucket.compression as Compression,
426
843
  encryption: selectedBucket.encryption,
427
844
  antivirus: selectedBucket.antivirus,
845
+ permissions: selectedBucket.$permissions.map((p) =>
846
+ permissionSchema.parse(p)
847
+ ),
428
848
  };
429
849
  }
430
850
  } else if (action === "create") {
@@ -545,6 +965,7 @@ export class InteractiveCLI {
545
965
 
546
966
  private async syncDb(): Promise<void> {
547
967
  console.log(chalk.yellow("Syncing database..."));
968
+ const functionsClient = new Functions(this.controller!.appwriteServer!);
548
969
  const databases = await this.selectDatabases(
549
970
  await fetchAllDatabases(this.controller!.database!),
550
971
  chalk.blue("Select databases to synchronize:"),
@@ -557,35 +978,151 @@ export class InteractiveCLI {
557
978
  true,
558
979
  true // prefer local
559
980
  );
560
- await this.controller!.syncDb(databases, collections);
981
+ const answer = await inquirer.prompt([
982
+ {
983
+ type: "confirm",
984
+ name: "syncFunctions",
985
+ message: "Do you want to synchronize functions?",
986
+ default: false,
987
+ },
988
+ ]);
989
+ if (answer.syncFunctions) {
990
+ const functions = await this.selectFunctions(
991
+ chalk.blue("Select functions to synchronize:"),
992
+ true,
993
+ true // prefer local
994
+ );
995
+ await this.controller!.syncDb(databases, collections);
996
+ for (const func of functions) {
997
+ await deployLocalFunction(
998
+ this.controller!.appwriteServer!,
999
+ func.dirPath || `functions/${func.name}`,
1000
+ func
1001
+ );
1002
+ }
1003
+ }
561
1004
  console.log(chalk.green("Database sync completed."));
562
1005
  }
563
1006
 
564
1007
  private async synchronizeConfigurations(): Promise<void> {
565
- if (!this.controller!.database) {
566
- throw new Error(
567
- "Database is not initialized. Is the config file correct and created?"
1008
+ console.log(chalk.blue("Synchronizing configurations..."));
1009
+ await this.controller!.init();
1010
+ // Sync databases and buckets first
1011
+ const { syncDatabases } = await inquirer.prompt([
1012
+ {
1013
+ type: "confirm",
1014
+ name: "syncDatabases",
1015
+ message: "Do you want to synchronize databases and their buckets?",
1016
+ default: true,
1017
+ },
1018
+ ]);
1019
+
1020
+ if (syncDatabases) {
1021
+ const remoteDatabases = await fetchAllDatabases(
1022
+ this.controller!.database!
568
1023
  );
1024
+ const localDatabases = this.controller!.config?.databases || [];
1025
+
1026
+ // Update config with remote databases that don't exist locally
1027
+ const updatedConfig = await this.configureBuckets({
1028
+ ...this.controller!.config!,
1029
+ databases: [
1030
+ ...localDatabases,
1031
+ ...remoteDatabases.filter(
1032
+ (rd) => !localDatabases.some((ld) => ld.name === rd.name)
1033
+ ),
1034
+ ],
1035
+ });
1036
+
1037
+ this.controller!.config = updatedConfig;
569
1038
  }
570
- const databases = await fetchAllDatabases(this.controller!.database);
571
1039
 
572
- const selectedDatabases = await this.selectDatabases(
573
- databases,
574
- "Select databases to synchronize:"
575
- );
1040
+ // Then sync functions
1041
+ const { syncFunctions } = await inquirer.prompt([
1042
+ {
1043
+ type: "confirm",
1044
+ name: "syncFunctions",
1045
+ message: "Do you want to synchronize functions?",
1046
+ default: true,
1047
+ },
1048
+ ]);
576
1049
 
577
- console.log(chalk.yellow("Configuring storage buckets..."));
578
- const updatedConfig = await this.configureBuckets(
579
- this.controller!.config!,
580
- selectedDatabases
581
- );
1050
+ if (syncFunctions) {
1051
+ const remoteFunctions = await this.controller!.listAllFunctions();
1052
+ const localFunctions = this.controller!.config?.functions || [];
582
1053
 
583
- console.log(chalk.yellow("Synchronizing configurations..."));
584
- await this.controller!.synchronizeConfigurations(
585
- selectedDatabases,
586
- updatedConfig
587
- );
588
- console.log(chalk.green("Configuration synchronization completed."));
1054
+ const allFunctions = [
1055
+ ...remoteFunctions,
1056
+ ...localFunctions.filter(
1057
+ (f) => !remoteFunctions.some((rf) => rf.$id === f.$id)
1058
+ ),
1059
+ ];
1060
+
1061
+ for (const func of allFunctions) {
1062
+ const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
1063
+ const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
1064
+
1065
+ if (hasLocal && hasRemote) {
1066
+ const { preference } = await inquirer.prompt([
1067
+ {
1068
+ type: "list",
1069
+ name: "preference",
1070
+ message: `Function "${func.name}" exists both locally and remotely. What would you like to do?`,
1071
+ choices: [
1072
+ {
1073
+ name: "Keep local version (deploy to remote)",
1074
+ value: "local",
1075
+ },
1076
+ { name: "Use remote version (download)", value: "remote" },
1077
+ { name: "Skip this function", value: "skip" },
1078
+ ],
1079
+ },
1080
+ ]);
1081
+
1082
+ if (preference === "local") {
1083
+ await this.controller!.deployFunction(func.name);
1084
+ } else if (preference === "remote") {
1085
+ await downloadLatestFunctionDeployment(
1086
+ this.controller!.appwriteServer!,
1087
+ func.$id,
1088
+ join(this.controller!.getAppwriteFolderPath(), "functions")
1089
+ );
1090
+ }
1091
+ } else if (hasLocal) {
1092
+ const { deploy } = await inquirer.prompt([
1093
+ {
1094
+ type: "confirm",
1095
+ name: "deploy",
1096
+ message: `Function "${func.name}" exists only locally. Deploy to remote?`,
1097
+ default: true,
1098
+ },
1099
+ ]);
1100
+
1101
+ if (deploy) {
1102
+ await this.controller!.deployFunction(func.name);
1103
+ }
1104
+ } else if (hasRemote) {
1105
+ const { download } = await inquirer.prompt([
1106
+ {
1107
+ type: "confirm",
1108
+ name: "download",
1109
+ message: `Function "${func.name}" exists only remotely. Download locally?`,
1110
+ default: true,
1111
+ },
1112
+ ]);
1113
+
1114
+ if (download) {
1115
+ await downloadLatestFunctionDeployment(
1116
+ this.controller!.appwriteServer!,
1117
+ func.$id,
1118
+ join(this.controller!.getAppwriteFolderPath(), "functions")
1119
+ );
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ console.log(chalk.green("✨ Configurations synchronized successfully!"));
589
1126
  }
590
1127
 
591
1128
  private async backupDatabase(): Promise<void> {
@@ -970,43 +1507,65 @@ export class InteractiveCLI {
970
1507
  }
971
1508
 
972
1509
  private async updateFunctionSpec(): Promise<void> {
973
- const functions = await listFunctions(this.controller!.appwriteServer!, [
974
- Query.limit(1000),
975
- ]);
976
-
1510
+ const remoteFunctions = await listFunctions(
1511
+ this.controller!.appwriteServer!,
1512
+ [Query.limit(1000)]
1513
+ );
1514
+ const localFunctions = this.getLocalFunctions();
1515
+
1516
+ const allFunctions = [
1517
+ ...remoteFunctions.functions,
1518
+ ...localFunctions.filter(
1519
+ (f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)
1520
+ ),
1521
+ ];
1522
+
977
1523
  const functionsToUpdate = await inquirer.prompt([
978
1524
  {
979
- type: 'checkbox',
980
- name: 'functionId',
981
- message: 'Select functions to update:',
982
- choices: functions.functions.map(f => ({
983
- name: `${f.name} (${f.$id})`,
984
- value: f.$id
1525
+ type: "checkbox",
1526
+ name: "functionId",
1527
+ message: "Select functions to update:",
1528
+ choices: allFunctions.map((f) => ({
1529
+ name: `${f.name} (${f.$id})${
1530
+ localFunctions.some((lf) => lf.name === f.name)
1531
+ ? " (Local)"
1532
+ : " (Remote)"
1533
+ }`,
1534
+ value: f.$id,
985
1535
  })),
986
1536
  loop: true,
987
- }
1537
+ },
988
1538
  ]);
989
1539
 
990
- const specifications = await listSpecifications(this.controller!.appwriteServer!);
1540
+ const specifications = await listSpecifications(
1541
+ this.controller!.appwriteServer!
1542
+ );
991
1543
  const { specification } = await inquirer.prompt([
992
1544
  {
993
- type: 'list',
994
- name: 'specification',
995
- message: 'Select new specification:',
1545
+ type: "list",
1546
+ name: "specification",
1547
+ message: "Select new specification:",
996
1548
  choices: specifications.specifications.map((s) => ({
997
1549
  name: `${s.slug}`,
998
- value: s.slug
1550
+ value: s.slug,
999
1551
  })),
1000
- }
1552
+ },
1001
1553
  ]);
1002
-
1003
- try {
1554
+
1555
+ try {
1004
1556
  for (const functionId of functionsToUpdate.functionId) {
1005
- await this.controller!.updateFunctionSpecifications(functionId, specification);
1006
- console.log(chalk.green(`Successfully updated function specification to ${specification}`));
1557
+ await this.controller!.updateFunctionSpecifications(
1558
+ functionId,
1559
+ specification
1560
+ );
1561
+ console.log(
1562
+ chalk.green(
1563
+ `Successfully updated function specification to ${specification}`
1564
+ )
1565
+ );
1007
1566
  }
1008
1567
  } catch (error) {
1009
- console.error(chalk.red('Error updating function specification:'), error);
1568
+ console.error(chalk.red("Error updating function specification:"), error);
1010
1569
  }
1011
1570
  }
1012
1571
  }