appwrite-utils-cli 1.5.2 → 1.6.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.
Files changed (233) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +251 -29
  3. package/dist/adapters/AdapterFactory.d.ts +10 -3
  4. package/dist/adapters/AdapterFactory.js +213 -17
  5. package/dist/adapters/TablesDBAdapter.js +60 -17
  6. package/dist/backups/operations/bucketBackup.d.ts +19 -0
  7. package/dist/backups/operations/bucketBackup.js +197 -0
  8. package/dist/backups/operations/collectionBackup.d.ts +30 -0
  9. package/dist/backups/operations/collectionBackup.js +201 -0
  10. package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
  11. package/dist/backups/operations/comprehensiveBackup.js +238 -0
  12. package/dist/backups/schemas/bucketManifest.d.ts +93 -0
  13. package/dist/backups/schemas/bucketManifest.js +33 -0
  14. package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
  15. package/dist/backups/schemas/comprehensiveManifest.js +32 -0
  16. package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
  17. package/dist/backups/tracking/centralizedTracking.js +274 -0
  18. package/dist/cli/commands/configCommands.d.ts +8 -0
  19. package/dist/cli/commands/configCommands.js +160 -0
  20. package/dist/cli/commands/databaseCommands.d.ts +13 -0
  21. package/dist/cli/commands/databaseCommands.js +479 -0
  22. package/dist/cli/commands/functionCommands.d.ts +7 -0
  23. package/dist/cli/commands/functionCommands.js +289 -0
  24. package/dist/cli/commands/schemaCommands.d.ts +7 -0
  25. package/dist/cli/commands/schemaCommands.js +134 -0
  26. package/dist/cli/commands/transferCommands.d.ts +5 -0
  27. package/dist/cli/commands/transferCommands.js +384 -0
  28. package/dist/collections/attributes.d.ts +5 -4
  29. package/dist/collections/attributes.js +539 -246
  30. package/dist/collections/indexes.js +39 -37
  31. package/dist/collections/methods.d.ts +2 -16
  32. package/dist/collections/methods.js +90 -538
  33. package/dist/collections/transferOperations.d.ts +7 -0
  34. package/dist/collections/transferOperations.js +331 -0
  35. package/dist/collections/wipeOperations.d.ts +16 -0
  36. package/dist/collections/wipeOperations.js +328 -0
  37. package/dist/config/configMigration.d.ts +87 -0
  38. package/dist/config/configMigration.js +390 -0
  39. package/dist/config/configValidation.d.ts +66 -0
  40. package/dist/config/configValidation.js +358 -0
  41. package/dist/config/yamlConfig.d.ts +455 -1
  42. package/dist/config/yamlConfig.js +145 -52
  43. package/dist/databases/methods.js +3 -2
  44. package/dist/databases/setup.d.ts +1 -2
  45. package/dist/databases/setup.js +9 -87
  46. package/dist/examples/yamlTerminologyExample.d.ts +42 -0
  47. package/dist/examples/yamlTerminologyExample.js +269 -0
  48. package/dist/functions/deployments.js +11 -10
  49. package/dist/functions/methods.d.ts +1 -1
  50. package/dist/functions/methods.js +5 -4
  51. package/dist/init.js +9 -9
  52. package/dist/interactiveCLI.d.ts +8 -17
  53. package/dist/interactiveCLI.js +209 -1172
  54. package/dist/main.js +364 -21
  55. package/dist/migrations/afterImportActions.js +22 -30
  56. package/dist/migrations/appwriteToX.js +71 -25
  57. package/dist/migrations/dataLoader.js +35 -26
  58. package/dist/migrations/importController.js +29 -30
  59. package/dist/migrations/relationships.js +13 -12
  60. package/dist/migrations/services/ImportOrchestrator.js +16 -19
  61. package/dist/migrations/transfer.js +46 -46
  62. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
  63. package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
  64. package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
  65. package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
  66. package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
  67. package/dist/migrations/yaml/generateImportSchemas.js +736 -7
  68. package/dist/schemas/authUser.d.ts +1 -1
  69. package/dist/setupController.js +3 -2
  70. package/dist/shared/backupMetadataSchema.d.ts +94 -0
  71. package/dist/shared/backupMetadataSchema.js +38 -0
  72. package/dist/shared/backupTracking.d.ts +18 -0
  73. package/dist/shared/backupTracking.js +176 -0
  74. package/dist/shared/confirmationDialogs.js +15 -15
  75. package/dist/shared/errorUtils.d.ts +54 -0
  76. package/dist/shared/errorUtils.js +95 -0
  77. package/dist/shared/functionManager.js +20 -19
  78. package/dist/shared/indexManager.js +12 -11
  79. package/dist/shared/jsonSchemaGenerator.js +10 -26
  80. package/dist/shared/logging.d.ts +51 -0
  81. package/dist/shared/logging.js +70 -0
  82. package/dist/shared/messageFormatter.d.ts +2 -0
  83. package/dist/shared/messageFormatter.js +10 -0
  84. package/dist/shared/migrationHelpers.d.ts +6 -16
  85. package/dist/shared/migrationHelpers.js +24 -21
  86. package/dist/shared/operationLogger.d.ts +8 -1
  87. package/dist/shared/operationLogger.js +11 -24
  88. package/dist/shared/operationQueue.d.ts +28 -1
  89. package/dist/shared/operationQueue.js +268 -66
  90. package/dist/shared/operationsTable.d.ts +26 -0
  91. package/dist/shared/operationsTable.js +286 -0
  92. package/dist/shared/operationsTableSchema.d.ts +48 -0
  93. package/dist/shared/operationsTableSchema.js +35 -0
  94. package/dist/shared/relationshipExtractor.d.ts +56 -0
  95. package/dist/shared/relationshipExtractor.js +138 -0
  96. package/dist/shared/schemaGenerator.d.ts +19 -1
  97. package/dist/shared/schemaGenerator.js +56 -75
  98. package/dist/storage/backupCompression.d.ts +20 -0
  99. package/dist/storage/backupCompression.js +67 -0
  100. package/dist/storage/methods.d.ts +16 -2
  101. package/dist/storage/methods.js +98 -14
  102. package/dist/users/methods.js +9 -8
  103. package/dist/utils/configDiscovery.d.ts +78 -0
  104. package/dist/utils/configDiscovery.js +430 -0
  105. package/dist/utils/directoryUtils.d.ts +22 -0
  106. package/dist/utils/directoryUtils.js +59 -0
  107. package/dist/utils/getClientFromConfig.d.ts +17 -8
  108. package/dist/utils/getClientFromConfig.js +162 -17
  109. package/dist/utils/helperFunctions.d.ts +16 -2
  110. package/dist/utils/helperFunctions.js +19 -5
  111. package/dist/utils/loadConfigs.d.ts +34 -9
  112. package/dist/utils/loadConfigs.js +236 -316
  113. package/dist/utils/pathResolvers.d.ts +53 -0
  114. package/dist/utils/pathResolvers.js +72 -0
  115. package/dist/utils/projectConfig.d.ts +119 -0
  116. package/dist/utils/projectConfig.js +171 -0
  117. package/dist/utils/retryFailedPromises.js +4 -2
  118. package/dist/utils/sessionAuth.d.ts +48 -0
  119. package/dist/utils/sessionAuth.js +164 -0
  120. package/dist/utils/sessionPreservationExample.d.ts +1666 -0
  121. package/dist/utils/sessionPreservationExample.js +101 -0
  122. package/dist/utils/setupFiles.js +301 -41
  123. package/dist/utils/typeGuards.d.ts +35 -0
  124. package/dist/utils/typeGuards.js +57 -0
  125. package/dist/utils/versionDetection.js +145 -9
  126. package/dist/utils/yamlConverter.d.ts +53 -3
  127. package/dist/utils/yamlConverter.js +232 -13
  128. package/dist/utils/yamlLoader.d.ts +70 -0
  129. package/dist/utils/yamlLoader.js +263 -0
  130. package/dist/utilsController.d.ts +36 -3
  131. package/dist/utilsController.js +186 -56
  132. package/package.json +12 -2
  133. package/src/adapters/AdapterFactory.ts +263 -35
  134. package/src/adapters/TablesDBAdapter.ts +225 -36
  135. package/src/backups/operations/bucketBackup.ts +277 -0
  136. package/src/backups/operations/collectionBackup.ts +310 -0
  137. package/src/backups/operations/comprehensiveBackup.ts +342 -0
  138. package/src/backups/schemas/bucketManifest.ts +78 -0
  139. package/src/backups/schemas/comprehensiveManifest.ts +76 -0
  140. package/src/backups/tracking/centralizedTracking.ts +352 -0
  141. package/src/cli/commands/configCommands.ts +194 -0
  142. package/src/cli/commands/databaseCommands.ts +635 -0
  143. package/src/cli/commands/functionCommands.ts +379 -0
  144. package/src/cli/commands/schemaCommands.ts +163 -0
  145. package/src/cli/commands/transferCommands.ts +457 -0
  146. package/src/collections/attributes.ts +900 -621
  147. package/src/collections/attributes.ts.backup +1555 -0
  148. package/src/collections/indexes.ts +116 -114
  149. package/src/collections/methods.ts +295 -968
  150. package/src/collections/transferOperations.ts +516 -0
  151. package/src/collections/wipeOperations.ts +501 -0
  152. package/src/config/README.md +274 -0
  153. package/src/config/configMigration.ts +575 -0
  154. package/src/config/configValidation.ts +445 -0
  155. package/src/config/yamlConfig.ts +168 -55
  156. package/src/databases/methods.ts +3 -2
  157. package/src/databases/setup.ts +11 -138
  158. package/src/examples/yamlTerminologyExample.ts +341 -0
  159. package/src/functions/deployments.ts +14 -12
  160. package/src/functions/methods.ts +11 -11
  161. package/src/functions/templates/hono-typescript/README.md +286 -0
  162. package/src/functions/templates/hono-typescript/package.json +26 -0
  163. package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  164. package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  165. package/src/functions/templates/hono-typescript/src/app.ts +180 -0
  166. package/src/functions/templates/hono-typescript/src/context.ts +103 -0
  167. package/src/functions/templates/hono-typescript/src/index.ts +54 -0
  168. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  169. package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
  170. package/src/functions/templates/typescript-node/package.json +2 -1
  171. package/src/functions/templates/typescript-node/src/context.ts +103 -0
  172. package/src/functions/templates/typescript-node/src/index.ts +18 -12
  173. package/src/functions/templates/uv/pyproject.toml +1 -0
  174. package/src/functions/templates/uv/src/context.py +125 -0
  175. package/src/functions/templates/uv/src/index.py +35 -5
  176. package/src/init.ts +9 -11
  177. package/src/interactiveCLI.ts +274 -1563
  178. package/src/main.ts +418 -24
  179. package/src/migrations/afterImportActions.ts +71 -44
  180. package/src/migrations/appwriteToX.ts +100 -34
  181. package/src/migrations/dataLoader.ts +48 -34
  182. package/src/migrations/importController.ts +44 -39
  183. package/src/migrations/relationships.ts +28 -18
  184. package/src/migrations/services/ImportOrchestrator.ts +24 -27
  185. package/src/migrations/transfer.ts +159 -121
  186. package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
  187. package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
  188. package/src/migrations/yaml/generateImportSchemas.ts +751 -12
  189. package/src/setupController.ts +3 -2
  190. package/src/shared/backupMetadataSchema.ts +93 -0
  191. package/src/shared/backupTracking.ts +211 -0
  192. package/src/shared/confirmationDialogs.ts +19 -19
  193. package/src/shared/errorUtils.ts +110 -0
  194. package/src/shared/functionManager.ts +21 -20
  195. package/src/shared/indexManager.ts +12 -11
  196. package/src/shared/jsonSchemaGenerator.ts +38 -52
  197. package/src/shared/logging.ts +75 -0
  198. package/src/shared/messageFormatter.ts +14 -1
  199. package/src/shared/migrationHelpers.ts +45 -38
  200. package/src/shared/operationLogger.ts +11 -36
  201. package/src/shared/operationQueue.ts +322 -93
  202. package/src/shared/operationsTable.ts +338 -0
  203. package/src/shared/operationsTableSchema.ts +60 -0
  204. package/src/shared/relationshipExtractor.ts +214 -0
  205. package/src/shared/schemaGenerator.ts +179 -219
  206. package/src/storage/backupCompression.ts +88 -0
  207. package/src/storage/methods.ts +131 -34
  208. package/src/users/methods.ts +11 -9
  209. package/src/utils/configDiscovery.ts +502 -0
  210. package/src/utils/directoryUtils.ts +61 -0
  211. package/src/utils/getClientFromConfig.ts +205 -22
  212. package/src/utils/helperFunctions.ts +23 -5
  213. package/src/utils/loadConfigs.ts +313 -345
  214. package/src/utils/pathResolvers.ts +81 -0
  215. package/src/utils/projectConfig.ts +299 -0
  216. package/src/utils/retryFailedPromises.ts +4 -2
  217. package/src/utils/sessionAuth.ts +230 -0
  218. package/src/utils/setupFiles.ts +322 -54
  219. package/src/utils/typeGuards.ts +65 -0
  220. package/src/utils/versionDetection.ts +218 -64
  221. package/src/utils/yamlConverter.ts +296 -13
  222. package/src/utils/yamlLoader.ts +364 -0
  223. package/src/utilsController.ts +314 -110
  224. package/tests/README.md +497 -0
  225. package/tests/adapters/AdapterFactory.test.ts +277 -0
  226. package/tests/integration/syncOperations.test.ts +463 -0
  227. package/tests/jest.config.js +25 -0
  228. package/tests/migration/configMigration.test.ts +546 -0
  229. package/tests/setup.ts +62 -0
  230. package/tests/testUtils.ts +340 -0
  231. package/tests/utils/loadConfigs.test.ts +350 -0
  232. package/tests/validation/configValidation.test.ts +412 -0
  233. package/src/utils/schemaStrings.ts +0 -517
@@ -1,31 +1,31 @@
1
1
  import inquirer from "inquirer";
2
2
  import { UtilsController } from "./utilsController.js";
3
- import { createEmptyCollection, setupDirsFiles } from "./utils/setupFiles.js";
4
- import { fetchAllDatabases } from "./databases/methods.js";
5
3
  import { fetchAllCollections } from "./collections/methods.js";
6
4
  import { listBuckets, createBucket } from "./storage/methods.js";
7
5
  import { Databases, Storage, Client, Compression, Query, Functions, } from "node-appwrite";
8
- import { getClient } from "./utils/getClientFromConfig.js";
9
- import { ComprehensiveTransfer } from "./migrations/comprehensiveTransfer.js";
10
- import { AppwriteFunctionSchema, parseAttribute, PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
6
+ import { PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
11
7
  import { ulid } from "ulidx";
12
8
  import chalk from "chalk";
13
9
  import { DateTime } from "luxon";
14
- import { createFunctionTemplate, deleteFunction, downloadLatestFunctionDeployment, getFunction, listFunctions, listSpecifications, } from "./functions/methods.js";
15
- import { deployLocalFunction } from "./functions/deployments.js";
10
+ import { getFunction, downloadLatestFunctionDeployment, listFunctions, } from "./functions/methods.js";
16
11
  import { join } from "node:path";
17
12
  import path from "path";
18
13
  import fs from "node:fs";
19
14
  import os from "node:os";
20
- import { SchemaGenerator } from "./shared/schemaGenerator.js";
21
- import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
22
15
  import { MessageFormatter } from "./shared/messageFormatter.js";
23
- import { migrateConfig } from "./utils/configMigration.js";
24
16
  import { findAppwriteConfig } from "./utils/loadConfigs.js";
25
- import { findYamlConfig, addFunctionToYamlConfig } from "./config/yamlConfig.js";
17
+ import { findYamlConfig } from "./config/yamlConfig.js";
18
+ // Import command modules
19
+ import { configCommands } from "./cli/commands/configCommands.js";
20
+ import { databaseCommands } from "./cli/commands/databaseCommands.js";
21
+ import { functionCommands } from "./cli/commands/functionCommands.js";
22
+ import { transferCommands } from "./cli/commands/transferCommands.js";
23
+ import { schemaCommands } from "./cli/commands/schemaCommands.js";
26
24
  var CHOICES;
27
25
  (function (CHOICES) {
28
26
  CHOICES["MIGRATE_CONFIG"] = "\uD83D\uDD04 Migrate TypeScript config to YAML (.appwrite structure)";
27
+ CHOICES["VALIDATE_CONFIG"] = "\u2705 Validate configuration (collections/tables conflicts)";
28
+ CHOICES["MIGRATE_COLLECTIONS_TO_TABLES"] = "\uD83D\uDD00 Migrate collections to tables format";
29
29
  CHOICES["CREATE_COLLECTION_CONFIG"] = "\uD83D\uDCC4 Create collection config file";
30
30
  CHOICES["CREATE_FUNCTION"] = "\u26A1 Create a new function, from scratch or using a template";
31
31
  CHOICES["DEPLOY_FUNCTION"] = "\uD83D\uDE80 Deploy function(s)";
@@ -77,75 +77,80 @@ export class InteractiveCLI {
77
77
  ]);
78
78
  switch (action) {
79
79
  case CHOICES.MIGRATE_CONFIG:
80
- await this.migrateTypeScriptConfig();
80
+ await configCommands.migrateTypeScriptConfig(this);
81
+ break;
82
+ case CHOICES.VALIDATE_CONFIG:
83
+ await configCommands.validateConfiguration(this);
84
+ break;
85
+ case CHOICES.MIGRATE_COLLECTIONS_TO_TABLES:
86
+ await configCommands.migrateCollectionsToTables(this);
81
87
  break;
82
88
  case CHOICES.CREATE_COLLECTION_CONFIG:
83
- await this.createCollectionConfig();
89
+ await configCommands.createCollectionConfig(this);
84
90
  break;
85
91
  case CHOICES.CREATE_FUNCTION:
86
92
  await this.initControllerIfNeeded();
87
- await this.createFunction();
93
+ await functionCommands.createFunction(this);
88
94
  break;
89
95
  case CHOICES.DEPLOY_FUNCTION:
90
96
  await this.initControllerIfNeeded();
91
- await this.deployFunction();
97
+ await functionCommands.deployFunction(this);
92
98
  break;
93
99
  case CHOICES.DELETE_FUNCTION:
94
100
  await this.initControllerIfNeeded();
95
- await this.deleteFunction();
101
+ await functionCommands.deleteFunction(this);
96
102
  break;
97
103
  case CHOICES.SETUP_DIRS_FILES:
98
- await setupDirsFiles(false, this.currentDir);
104
+ await schemaCommands.setupDirsFiles(this, false);
99
105
  break;
100
106
  case CHOICES.SETUP_DIRS_FILES_WITH_EXAMPLE_DATA:
101
- await setupDirsFiles(true, this.currentDir);
107
+ await schemaCommands.setupDirsFiles(this, true);
102
108
  break;
103
109
  case CHOICES.SYNCHRONIZE_CONFIGURATIONS:
104
110
  await this.initControllerIfNeeded();
105
- await this.synchronizeConfigurations();
111
+ await databaseCommands.synchronizeConfigurations(this);
106
112
  break;
107
113
  case CHOICES.SYNC_DB:
108
114
  await this.initControllerIfNeeded();
109
- await this.syncDb();
115
+ await databaseCommands.syncDb(this);
110
116
  break;
111
117
  case CHOICES.TRANSFER_DATA:
112
118
  await this.initControllerIfNeeded();
113
- await this.transferData();
119
+ await transferCommands.transferData(this);
114
120
  break;
115
121
  case CHOICES.COMPREHENSIVE_TRANSFER:
116
- await this.comprehensiveTransfer();
122
+ await transferCommands.comprehensiveTransfer(this);
117
123
  break;
118
124
  case CHOICES.BACKUP_DATABASE:
119
125
  await this.initControllerIfNeeded();
120
- await this.backupDatabase();
126
+ await databaseCommands.backupDatabase(this);
121
127
  break;
122
128
  case CHOICES.WIPE_DATABASE:
123
129
  await this.initControllerIfNeeded();
124
- await this.wipeDatabase();
130
+ await databaseCommands.wipeDatabase(this);
125
131
  break;
126
132
  case CHOICES.WIPE_COLLECTIONS:
127
133
  await this.initControllerIfNeeded();
128
- await this.wipeCollections();
134
+ await databaseCommands.wipeCollections(this);
129
135
  break;
130
136
  case CHOICES.GENERATE_SCHEMAS:
131
137
  await this.initControllerIfNeeded();
132
- await this.generateSchemas();
138
+ await schemaCommands.generateSchemas(this);
133
139
  break;
134
140
  case CHOICES.GENERATE_CONSTANTS:
135
141
  await this.initControllerIfNeeded();
136
- await this.generateConstants();
142
+ await schemaCommands.generateConstants(this);
137
143
  break;
138
144
  case CHOICES.IMPORT_DATA:
139
145
  await this.initControllerIfNeeded();
140
- await this.importData();
146
+ await schemaCommands.importData(this);
141
147
  break;
142
148
  case CHOICES.RELOAD_CONFIG:
143
- await this.initControllerIfNeeded();
144
- await this.reloadConfig();
149
+ await configCommands.reloadConfigWithSessionPreservation(this);
145
150
  break;
146
151
  case CHOICES.UPDATE_FUNCTION_SPEC:
147
152
  await this.initControllerIfNeeded();
148
- await this.updateFunctionSpec();
153
+ await functionCommands.updateFunctionSpec(this);
149
154
  break;
150
155
  case CHOICES.EXIT:
151
156
  MessageFormatter.success("Goodbye!");
@@ -158,6 +163,27 @@ export class InteractiveCLI {
158
163
  this.controller = new UtilsController(this.currentDir, directConfig);
159
164
  await this.controller.init();
160
165
  }
166
+ else {
167
+ // Extract session info from existing controller before reinitializing
168
+ const sessionInfo = this.controller.getSessionInfo();
169
+ if (sessionInfo.hasSession && directConfig) {
170
+ // Create enhanced directConfig with session preservation
171
+ const enhancedDirectConfig = {
172
+ ...directConfig,
173
+ sessionCookie: this.controller.sessionCookie,
174
+ sessionMetadata: this.controller.sessionMetadata
175
+ };
176
+ // Reinitialize with session preservation
177
+ this.controller = new UtilsController(this.currentDir, enhancedDirectConfig);
178
+ await this.controller.init();
179
+ }
180
+ else if (directConfig) {
181
+ // Standard reinitialize without session
182
+ this.controller = new UtilsController(this.currentDir, directConfig);
183
+ await this.controller.init();
184
+ }
185
+ // If no directConfig provided, keep existing controller
186
+ }
161
187
  }
162
188
  async selectDatabases(databases, message, multiSelect = true) {
163
189
  await this.initControllerIfNeeded();
@@ -175,11 +201,7 @@ export class InteractiveCLI {
175
201
  acc.push(db);
176
202
  }
177
203
  return acc;
178
- }, [])
179
- .filter((db) => {
180
- const useMigrations = this.controller?.config?.useMigrations ?? true;
181
- return useMigrations || db.name.toLowerCase() !== "migrations";
182
- });
204
+ }, []);
183
205
  const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name)) &&
184
206
  allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name));
185
207
  const choices = allDatabases
@@ -192,11 +214,7 @@ export class InteractiveCLI {
192
214
  : " (Remote)"
193
215
  : ""),
194
216
  value: db,
195
- }))
196
- .filter((db) => {
197
- const useMigrations = this.controller?.config?.useMigrations ?? true;
198
- return useMigrations || db.name.toLowerCase() !== "migrations";
199
- });
217
+ }));
200
218
  const { selectedDatabases } = await inquirer.prompt([
201
219
  {
202
220
  type: multiSelect ? "checkbox" : "list",
@@ -217,7 +235,7 @@ export class InteractiveCLI {
217
235
  Query.equal("name", database.name),
218
236
  ]);
219
237
  if (dbExists.total === 0) {
220
- console.log(chalk.red(`Database "${database.name}" does not exist, using only local collection options`));
238
+ MessageFormatter.warning(`Database "${database.name}" does not exist, using only local collection/table options`, { prefix: "Database" });
221
239
  shouldFilterByDatabase = false;
222
240
  }
223
241
  else {
@@ -235,27 +253,75 @@ export class InteractiveCLI {
235
253
  ...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
236
254
  ];
237
255
  if (shouldFilterByDatabase) {
238
- // Keep local entries that don't have databaseId (common in config),
239
- // but still filter remote collections by selected database.
256
+ // Enhanced filtering for tables with optional databaseId
240
257
  allCollections = allCollections.filter((c) => {
258
+ // For remote collections, they should match the selected database
259
+ if (remoteCollections.some((rc) => rc.name === c.name)) {
260
+ return c.databaseId === database.$id;
261
+ }
262
+ // For local collections/tables:
263
+ // - Collections without databaseId are kept (backward compatibility)
264
+ // - Tables with databaseId must match the selected database
265
+ // - Tables without databaseId are kept (fallback for misconfigured tables)
241
266
  if (!c.databaseId)
242
267
  return true;
243
268
  return c.databaseId === database.$id;
244
269
  });
245
270
  }
271
+ // Filter out system tables (those starting with underscore)
272
+ allCollections = allCollections.filter((collection) => !collection.$id.startsWith('_'));
246
273
  const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
247
274
  allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
275
+ // Enhanced choice display with type indicators
248
276
  const choices = allCollections
249
- .sort((a, b) => a.name.localeCompare(b.name))
250
- .map((collection) => ({
251
- name: collection.name +
252
- (hasLocalAndRemote
253
- ? configCollections.some((c) => c.name === collection.name)
254
- ? " (Local)"
255
- : " (Remote)"
256
- : ""),
257
- value: collection,
258
- }));
277
+ .sort((a, b) => {
278
+ // Sort by type first (collections before tables), then by name
279
+ const aIsTable = a._isFromTablesDir || false;
280
+ const bIsTable = b._isFromTablesDir || false;
281
+ if (aIsTable !== bIsTable) {
282
+ return aIsTable ? 1 : -1; // Collections first, then tables
283
+ }
284
+ return a.name.localeCompare(b.name);
285
+ })
286
+ .map((collection) => {
287
+ const localCollection = configCollections.find((c) => c.name === collection.name);
288
+ const isLocal = !!localCollection;
289
+ const isTable = localCollection?._isFromTablesDir || collection._isFromTablesDir || false;
290
+ const sourceFolder = localCollection?._sourceFolder || collection._sourceFolder || 'collections';
291
+ let typeIndicator = '';
292
+ let locationIndicator = '';
293
+ // Type indicator
294
+ if (isTable) {
295
+ typeIndicator = chalk.cyan('[Table]');
296
+ }
297
+ else {
298
+ typeIndicator = chalk.green('[Collection]');
299
+ }
300
+ // Location indicator
301
+ if (hasLocalAndRemote) {
302
+ if (isLocal) {
303
+ locationIndicator = chalk.gray(`(Local/${sourceFolder})`);
304
+ }
305
+ else {
306
+ locationIndicator = chalk.gray('(Remote)');
307
+ }
308
+ }
309
+ else if (isLocal) {
310
+ locationIndicator = chalk.gray(`(${sourceFolder}/)`);
311
+ }
312
+ // Database indicator for tables with explicit databaseId
313
+ let dbIndicator = '';
314
+ if (isTable && collection.databaseId && shouldFilterByDatabase) {
315
+ const matchesCurrentDb = collection.databaseId === database.$id;
316
+ if (!matchesCurrentDb) {
317
+ dbIndicator = chalk.yellow(` [DB: ${collection.databaseId}]`);
318
+ }
319
+ }
320
+ return {
321
+ name: `${typeIndicator} ${collection.name} ${locationIndicator}${dbIndicator}`,
322
+ value: collection,
323
+ };
324
+ });
259
325
  const { selectedCollections } = await inquirer.prompt([
260
326
  {
261
327
  type: multiSelect ? "checkbox" : "list",
@@ -263,11 +329,67 @@ export class InteractiveCLI {
263
329
  message: chalk.blue(message),
264
330
  choices,
265
331
  loop: true,
266
- pageSize: 10,
332
+ pageSize: 15, // Increased page size to accommodate additional info
267
333
  },
268
334
  ]);
269
335
  return selectedCollections;
270
336
  }
337
+ /**
338
+ * Enhanced collection/table selection with better guidance for mixed scenarios
339
+ */
340
+ async selectCollectionsAndTables(database, databasesClient, message, multiSelect = true, preferLocal = false, shouldFilterByDatabase = false) {
341
+ const configCollections = this.getLocalCollections();
342
+ const collectionsCount = configCollections.filter(c => !c._isFromTablesDir).length;
343
+ const tablesCount = configCollections.filter(c => c._isFromTablesDir).length;
344
+ const totalCount = collectionsCount + tablesCount;
345
+ // Provide context about what's available
346
+ if (collectionsCount > 0 && tablesCount > 0) {
347
+ MessageFormatter.info(`\n📋 ${totalCount} total items available:`, { prefix: "Collections" });
348
+ MessageFormatter.info(` Collections: ${collectionsCount} (from collections/ folder)`, { prefix: "Collections" });
349
+ MessageFormatter.info(` Tables: ${tablesCount} (from tables/ folder)`, { prefix: "Collections" });
350
+ }
351
+ else if (collectionsCount > 0) {
352
+ MessageFormatter.info(`📁 ${collectionsCount} collections available from collections/ folder`, { prefix: "Collections" });
353
+ }
354
+ else if (tablesCount > 0) {
355
+ MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder`, { prefix: "Collections" });
356
+ }
357
+ // Ask user if they want to filter by database or show all
358
+ const { filterChoice } = await inquirer.prompt([
359
+ {
360
+ type: "list",
361
+ name: "filterChoice",
362
+ message: chalk.blue("How would you like to view collections/tables?"),
363
+ choices: [
364
+ {
365
+ name: `Show all available collections/tables (${totalCount} total) - You can push any collection to any database`,
366
+ value: "all"
367
+ },
368
+ {
369
+ name: `Filter by database "${database.name}" - Show only related collections/tables`,
370
+ value: "filter"
371
+ }
372
+ ],
373
+ default: "all"
374
+ }
375
+ ]);
376
+ // User's choice overrides the parameter
377
+ const userWantsFiltering = filterChoice === "filter";
378
+ // Show appropriate informational message
379
+ if (userWantsFiltering) {
380
+ MessageFormatter.info(`ℹ️ Showing collections/tables related to database "${database.name}"`, { prefix: "Collections" });
381
+ if (tablesCount > 0) {
382
+ const filteredTables = configCollections.filter(c => c._isFromTablesDir && (!c.databaseId || c.databaseId === database.$id)).length;
383
+ if (filteredTables !== tablesCount) {
384
+ MessageFormatter.info(` ${filteredTables}/${tablesCount} tables match this database`, { prefix: "Collections" });
385
+ }
386
+ }
387
+ }
388
+ else {
389
+ MessageFormatter.info(`ℹ️ Showing all available collections/tables - you can push any collection to any database\n`, { prefix: "Collections" });
390
+ }
391
+ return this.selectCollections(database, databasesClient, message, multiSelect, preferLocal, userWantsFiltering);
392
+ }
271
393
  getTemplateDefaults(template) {
272
394
  const defaults = {
273
395
  "typescript-node": {
@@ -276,6 +398,12 @@ export class InteractiveCLI {
276
398
  commands: "npm install && npm run build",
277
399
  specification: "s-0.5vcpu-512mb",
278
400
  },
401
+ "hono-typescript": {
402
+ runtime: "node-21.0",
403
+ entrypoint: "src/index.ts",
404
+ commands: "npm install && npm run build",
405
+ specification: "s-0.5vcpu-512mb",
406
+ },
279
407
  "uv": {
280
408
  runtime: "python-3.12",
281
409
  entrypoint: "src/index.py",
@@ -296,98 +424,6 @@ export class InteractiveCLI {
296
424
  specification: "s-0.5vcpu-512mb",
297
425
  };
298
426
  }
299
- async createFunction() {
300
- const { name } = await inquirer.prompt([
301
- {
302
- type: "input",
303
- name: "name",
304
- message: "Function name:",
305
- validate: (input) => input.length > 0,
306
- },
307
- ]);
308
- const { template } = await inquirer.prompt([
309
- {
310
- type: "list",
311
- name: "template",
312
- message: "Select a template:",
313
- choices: [
314
- { name: "TypeScript Node.js", value: "typescript-node" },
315
- { name: "Python with UV", value: "uv" },
316
- { name: "Count Documents in Collection", value: "count-docs-in-collection" },
317
- { name: "None (Empty Function)", value: "none" },
318
- ],
319
- },
320
- ]);
321
- // Get template defaults
322
- const templateDefaults = this.getTemplateDefaults(template);
323
- const { runtime } = await inquirer.prompt([
324
- {
325
- type: "list",
326
- name: "runtime",
327
- message: "Select runtime:",
328
- choices: Object.values(RuntimeSchema.Values),
329
- default: templateDefaults.runtime,
330
- },
331
- ]);
332
- const specifications = await listSpecifications(this.controller.appwriteServer);
333
- const { specification } = await inquirer.prompt([
334
- {
335
- type: "list",
336
- name: "specification",
337
- message: "Select specification:",
338
- choices: [
339
- { name: "None", value: undefined },
340
- ...specifications.specifications.map((s) => ({
341
- name: s.slug,
342
- value: s.slug,
343
- })),
344
- ],
345
- default: templateDefaults.specification,
346
- },
347
- ]);
348
- const functionConfig = {
349
- $id: ulid(),
350
- name,
351
- runtime,
352
- events: [],
353
- execute: ["any"],
354
- enabled: true,
355
- logging: true,
356
- entrypoint: templateDefaults.entrypoint,
357
- commands: templateDefaults.commands,
358
- specification: specification || templateDefaults.specification,
359
- scopes: [],
360
- timeout: 15,
361
- schedule: "",
362
- installationId: "",
363
- providerRepositoryId: "",
364
- providerBranch: "",
365
- providerSilentMode: false,
366
- providerRootDirectory: "",
367
- templateRepository: "",
368
- templateOwner: "",
369
- templateRootDirectory: "",
370
- };
371
- if (template !== "none") {
372
- await createFunctionTemplate(template, name, "./functions");
373
- }
374
- // Add to in-memory config
375
- if (!this.controller.config.functions) {
376
- this.controller.config.functions = [];
377
- }
378
- this.controller.config.functions.push(functionConfig);
379
- // If using YAML config, also add to YAML file
380
- const yamlConfigPath = findYamlConfig(this.currentDir);
381
- if (yamlConfigPath) {
382
- try {
383
- await addFunctionToYamlConfig(yamlConfigPath, functionConfig);
384
- }
385
- catch (error) {
386
- MessageFormatter.warning(`Function created but failed to update YAML config: ${error instanceof Error ? error.message : error}`, { prefix: "Functions" });
387
- }
388
- }
389
- MessageFormatter.success("Function created successfully!", { prefix: "Functions" });
390
- }
391
427
  async findFunctionInSubdirectories(basePaths, functionName) {
392
428
  // Common locations to check first
393
429
  const commonPaths = basePaths.flatMap((basePath) => [
@@ -408,7 +444,7 @@ export class InteractiveCLI {
408
444
  try {
409
445
  const stats = await fs.promises.stat(path);
410
446
  if (stats.isDirectory()) {
411
- console.log(chalk.green(`Found function at common location: ${path}`));
447
+ MessageFormatter.success(`Found function at common location: ${path}`, { prefix: "Functions" });
412
448
  return path;
413
449
  }
414
450
  }
@@ -417,7 +453,7 @@ export class InteractiveCLI {
417
453
  }
418
454
  }
419
455
  // If not found in common locations, do recursive search
420
- console.log(chalk.yellow("Function not found in common locations, searching subdirectories..."));
456
+ MessageFormatter.info("Function not found in common locations, searching subdirectories...", { prefix: "Functions" });
421
457
  const queue = [...basePaths];
422
458
  const searched = new Set();
423
459
  while (queue.length > 0) {
@@ -444,7 +480,7 @@ export class InteractiveCLI {
444
480
  // Check if any variation of the entry name matches any variation of the function name
445
481
  const hasMatch = [...functionNameVariations].some((fnVar) => [...entryNameVariations].includes(fnVar));
446
482
  if (hasMatch) {
447
- console.log(chalk.green(`Found function at: ${fullPath}`));
483
+ MessageFormatter.success(`Found function at: ${fullPath}`, { prefix: "Functions" });
448
484
  return fullPath;
449
485
  }
450
486
  queue.push(fullPath);
@@ -452,150 +488,11 @@ export class InteractiveCLI {
452
488
  }
453
489
  }
454
490
  catch (error) {
455
- console.log(chalk.yellow(`Error reading directory ${currentPath}:`, error));
491
+ MessageFormatter.warning(`Error reading directory ${currentPath}: ${error}`, { prefix: "Functions" });
456
492
  }
457
493
  }
458
494
  return null;
459
495
  }
460
- async deployFunction() {
461
- await this.initControllerIfNeeded();
462
- if (!this.controller?.config) {
463
- console.log(chalk.red("Failed to initialize controller or load config"));
464
- return;
465
- }
466
- const functions = await this.selectFunctions("Select function(s) to deploy:", true, true);
467
- if (!functions?.length) {
468
- console.log(chalk.red("No function selected"));
469
- return;
470
- }
471
- for (const functionConfig of functions) {
472
- if (!functionConfig) {
473
- console.log(chalk.red("Invalid function configuration"));
474
- return;
475
- }
476
- // Ensure functions array exists
477
- if (!this.controller.config.functions) {
478
- this.controller.config.functions = [];
479
- }
480
- const functionNameLower = functionConfig.name
481
- .toLowerCase()
482
- .replace(/\s+/g, "-");
483
- // Debug logging
484
- console.log(chalk.blue(`🔍 Function deployment debug:`));
485
- console.log(chalk.gray(` Function name: ${functionConfig.name}`));
486
- console.log(chalk.gray(` Function ID: ${functionConfig.$id}`));
487
- console.log(chalk.gray(` Config dirPath: ${functionConfig.dirPath || 'undefined'}`));
488
- if (functionConfig.dirPath) {
489
- const expandedPath = functionConfig.dirPath.startsWith('~/')
490
- ? functionConfig.dirPath.replace('~', os.homedir())
491
- : functionConfig.dirPath;
492
- console.log(chalk.gray(` Expanded dirPath: ${expandedPath}`));
493
- }
494
- console.log(chalk.gray(` Appwrite folder: ${this.controller.getAppwriteFolderPath()}`));
495
- console.log(chalk.gray(` Current working dir: ${process.cwd()}`));
496
- // Helper function to expand tilde in paths
497
- const expandTildePath = (path) => {
498
- if (path.startsWith('~/')) {
499
- return path.replace('~', os.homedir());
500
- }
501
- return path;
502
- };
503
- // Check locations in priority order:
504
- const priorityLocations = [
505
- // 1. Config dirPath if specified (with tilde expansion)
506
- functionConfig.dirPath ? expandTildePath(functionConfig.dirPath) : undefined,
507
- // 2. Appwrite config folder/functions/name
508
- join(this.controller.getAppwriteFolderPath(), "functions", functionNameLower),
509
- // 3. Current working directory/functions/name
510
- join(process.cwd(), "functions", functionNameLower),
511
- // 4. Current working directory/name
512
- join(process.cwd(), functionNameLower),
513
- ].filter((val) => val !== undefined); // Remove undefined entries (in case dirPath is undefined)
514
- console.log(chalk.blue(`🔍 Priority locations to check:`));
515
- priorityLocations.forEach((loc, i) => {
516
- console.log(chalk.gray(` ${i + 1}. ${loc}`));
517
- });
518
- let functionPath = null;
519
- // Check each priority location
520
- for (const location of priorityLocations) {
521
- console.log(chalk.gray(` Checking: ${location} - ${fs.existsSync(location) ? 'EXISTS' : 'NOT FOUND'}`));
522
- if (fs.existsSync(location)) {
523
- console.log(chalk.green(`✅ Found function at: ${location}`));
524
- functionPath = location;
525
- break;
526
- }
527
- }
528
- // If not found in priority locations, do a broader search
529
- if (!functionPath) {
530
- console.log(chalk.yellow(`Function not found in primary locations, searching subdirectories...`));
531
- // Search in both appwrite config directory and current working directory
532
- functionPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], functionNameLower);
533
- }
534
- if (!functionPath) {
535
- const { shouldDownload } = await inquirer.prompt([
536
- {
537
- type: "confirm",
538
- name: "shouldDownload",
539
- message: "Function not found locally. Would you like to download the latest deployment?",
540
- default: false,
541
- },
542
- ]);
543
- if (shouldDownload) {
544
- try {
545
- console.log(chalk.blue("Downloading latest deployment..."));
546
- const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(this.controller.appwriteServer, functionConfig.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
547
- console.log(chalk.green(`✨ Function downloaded to ${downloadedPath}`));
548
- functionPath = downloadedPath;
549
- functionConfig.dirPath = downloadedPath;
550
- const existingIndex = this.controller.config.functions.findIndex((f) => f?.$id === remoteFunction.$id);
551
- if (existingIndex >= 0) {
552
- this.controller.config.functions[existingIndex].dirPath =
553
- downloadedPath;
554
- }
555
- await this.controller.reloadConfig();
556
- }
557
- catch (error) {
558
- console.error(chalk.red("Failed to download function deployment:"), error);
559
- return;
560
- }
561
- }
562
- else {
563
- console.log(chalk.red(`Function ${functionConfig.name} not found locally. Cannot deploy.`));
564
- return;
565
- }
566
- }
567
- if (!this.controller.appwriteServer) {
568
- console.log(chalk.red("Appwrite server not initialized"));
569
- return;
570
- }
571
- try {
572
- await deployLocalFunction(this.controller.appwriteServer, functionConfig.name, {
573
- ...functionConfig,
574
- dirPath: functionPath,
575
- }, functionPath);
576
- MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
577
- }
578
- catch (error) {
579
- console.error(chalk.red("Failed to deploy function:"), error);
580
- }
581
- }
582
- }
583
- async deleteFunction() {
584
- const functions = await this.selectFunctions("Select functions to delete:", true, false);
585
- if (!functions.length) {
586
- console.log(chalk.red("No functions selected"));
587
- return;
588
- }
589
- for (const func of functions) {
590
- try {
591
- await deleteFunction(this.controller.appwriteServer, func.$id);
592
- console.log(chalk.green(`✨ Function ${func.name} deleted successfully!`));
593
- }
594
- catch (error) {
595
- console.error(chalk.red(`Failed to delete function ${func.name}:`), error);
596
- }
597
- }
598
- }
599
496
  async selectFunctions(message, multiple = true, includeRemote = false) {
600
497
  const remoteFunctions = includeRemote
601
498
  ? await listFunctions(this.controller.appwriteServer, [
@@ -673,18 +570,6 @@ export class InteractiveCLI {
673
570
  ]);
674
571
  return selectedBuckets;
675
572
  }
676
- async createCollectionConfig() {
677
- const { collectionName } = await inquirer.prompt([
678
- {
679
- type: "input",
680
- name: "collectionName",
681
- message: chalk.blue("Enter the name of the collection:"),
682
- validate: (input) => input.trim() !== "" || "Collection name cannot be empty.",
683
- },
684
- ]);
685
- console.log(chalk.green(`Creating collection config file for '${collectionName}'...`));
686
- createEmptyCollection(collectionName);
687
- }
688
573
  async configureBuckets(config, databases) {
689
574
  const { storage } = this.controller;
690
575
  if (!storage) {
@@ -866,548 +751,6 @@ export class InteractiveCLI {
866
751
  antivirus: bucketAntivirus,
867
752
  }, bucketId.length > 0 ? bucketId : ulid());
868
753
  }
869
- async syncDb() {
870
- console.log(chalk.blue("Pushing local configuration to Appwrite..."));
871
- const databases = await this.selectDatabases(this.getLocalDatabases(), chalk.blue("Select local databases to push:"), true);
872
- if (!databases.length) {
873
- console.log(chalk.yellow("No databases selected. Skipping database sync."));
874
- return;
875
- }
876
- const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true, // prefer local
877
- true // filter by selected database
878
- );
879
- const { syncFunctions } = await inquirer.prompt([
880
- {
881
- type: "confirm",
882
- name: "syncFunctions",
883
- message: "Do you want to push local functions to remote?",
884
- default: false,
885
- },
886
- ]);
887
- try {
888
- // First sync databases and collections
889
- await this.controller.syncDb(databases, collections);
890
- console.log(chalk.green("Database and collections pushed successfully"));
891
- // Then handle functions if requested
892
- if (syncFunctions && this.controller.config?.functions?.length) {
893
- const functions = await this.selectFunctions(chalk.blue("Select local functions to push:"), true, true // prefer local
894
- );
895
- for (const func of functions) {
896
- try {
897
- await this.controller.deployFunction(func.name);
898
- console.log(chalk.green(`Function ${func.name} deployed successfully`));
899
- }
900
- catch (error) {
901
- console.error(chalk.red(`Failed to deploy function ${func.name}:`), error);
902
- }
903
- }
904
- }
905
- console.log(chalk.green("Local configuration push completed successfully!"));
906
- }
907
- catch (error) {
908
- console.error(chalk.red("Failed to push local configuration:"), error);
909
- throw error;
910
- }
911
- }
912
- async synchronizeConfigurations() {
913
- console.log(chalk.blue("Synchronizing configurations..."));
914
- await this.controller.init();
915
- // Sync databases, collections, and buckets
916
- const { syncDatabases } = await inquirer.prompt([
917
- {
918
- type: "confirm",
919
- name: "syncDatabases",
920
- message: "Do you want to synchronize databases, collections, and their buckets?",
921
- default: true,
922
- },
923
- ]);
924
- if (syncDatabases) {
925
- const remoteDatabases = await fetchAllDatabases(this.controller.database);
926
- // Use the controller's synchronizeConfigurations method which handles collections properly
927
- console.log(chalk.blue("Pulling collections and generating collection files..."));
928
- await this.controller.synchronizeConfigurations(remoteDatabases);
929
- // Also configure buckets for any new databases
930
- const localDatabases = this.controller.config?.databases || [];
931
- const updatedConfig = await this.configureBuckets({
932
- ...this.controller.config,
933
- databases: [
934
- ...localDatabases,
935
- ...remoteDatabases.filter((rd) => !localDatabases.some((ld) => ld.name === rd.name)),
936
- ],
937
- });
938
- this.controller.config = updatedConfig;
939
- }
940
- // Then sync functions
941
- const { syncFunctions } = await inquirer.prompt([
942
- {
943
- type: "confirm",
944
- name: "syncFunctions",
945
- message: "Do you want to synchronize functions?",
946
- default: true,
947
- },
948
- ]);
949
- if (syncFunctions) {
950
- const remoteFunctions = await this.controller.listAllFunctions();
951
- const localFunctions = this.controller.config?.functions || [];
952
- const allFunctions = [
953
- ...remoteFunctions,
954
- ...localFunctions.filter((f) => !remoteFunctions.some((rf) => rf.$id === f.$id)),
955
- ];
956
- for (const func of allFunctions) {
957
- const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
958
- const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
959
- if (hasLocal && hasRemote) {
960
- // First try to find the function locally
961
- let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
962
- if (!fs.existsSync(functionPath)) {
963
- console.log(chalk.yellow(`Function not found in primary location, searching subdirectories...`));
964
- const foundPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], func.name);
965
- if (foundPath) {
966
- console.log(chalk.green(`Found function at: ${foundPath}`));
967
- functionPath = foundPath;
968
- }
969
- }
970
- const { preference } = await inquirer.prompt([
971
- {
972
- type: "list",
973
- name: "preference",
974
- message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
975
- choices: [
976
- ...(functionPath
977
- ? [
978
- {
979
- name: "Keep local version (deploy to remote)",
980
- value: "local",
981
- },
982
- ]
983
- : []),
984
- { name: "Use remote version (download)", value: "remote" },
985
- { name: "Update config only", value: "config" },
986
- { name: "Skip this function", value: "skip" },
987
- ],
988
- },
989
- ]);
990
- if (preference === "local" && functionPath) {
991
- await this.controller.deployFunction(func.name);
992
- }
993
- else if (preference === "remote") {
994
- await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
995
- }
996
- else if (preference === "config") {
997
- const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
998
- const newFunction = {
999
- $id: remoteFunction.$id,
1000
- name: remoteFunction.name,
1001
- runtime: remoteFunction.runtime,
1002
- execute: remoteFunction.execute || [],
1003
- events: remoteFunction.events || [],
1004
- schedule: remoteFunction.schedule || "",
1005
- timeout: remoteFunction.timeout || 15,
1006
- enabled: remoteFunction.enabled !== false,
1007
- logging: remoteFunction.logging !== false,
1008
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
1009
- commands: remoteFunction.commands || "npm install",
1010
- scopes: (remoteFunction.scopes || []),
1011
- installationId: remoteFunction.installationId,
1012
- providerRepositoryId: remoteFunction.providerRepositoryId,
1013
- providerBranch: remoteFunction.providerBranch,
1014
- providerSilentMode: remoteFunction.providerSilentMode,
1015
- providerRootDirectory: remoteFunction.providerRootDirectory,
1016
- specification: remoteFunction.specification,
1017
- };
1018
- const existingIndex = this.controller.config.functions.findIndex((f) => f.$id === remoteFunction.$id);
1019
- if (existingIndex >= 0) {
1020
- this.controller.config.functions[existingIndex] = newFunction;
1021
- }
1022
- else {
1023
- this.controller.config.functions.push(newFunction);
1024
- }
1025
- console.log(chalk.green(`Updated config for function: ${func.name}`));
1026
- }
1027
- }
1028
- else if (hasLocal) {
1029
- // Similar check for local-only functions
1030
- let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
1031
- if (!fs.existsSync(functionPath)) {
1032
- const foundPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], func.name);
1033
- if (foundPath) {
1034
- functionPath = foundPath;
1035
- }
1036
- }
1037
- const { action } = await inquirer.prompt([
1038
- {
1039
- type: "list",
1040
- name: "action",
1041
- message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
1042
- choices: [
1043
- ...(functionPath
1044
- ? [
1045
- {
1046
- name: "Deploy to remote",
1047
- value: "deploy",
1048
- },
1049
- ]
1050
- : []),
1051
- { name: "Skip this function", value: "skip" },
1052
- ],
1053
- },
1054
- ]);
1055
- if (action === "deploy" && functionPath) {
1056
- await this.controller.deployFunction(func.name);
1057
- }
1058
- }
1059
- else if (hasRemote) {
1060
- const { action } = await inquirer.prompt([
1061
- {
1062
- type: "list",
1063
- name: "action",
1064
- message: `Function "${func.name}" exists only remotely. What would you like to do?`,
1065
- choices: [
1066
- { name: "Update config only", value: "config" },
1067
- { name: "Download locally", value: "download" },
1068
- { name: "Skip this function", value: "skip" },
1069
- ],
1070
- },
1071
- ]);
1072
- if (action === "download") {
1073
- await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
1074
- }
1075
- else if (action === "config") {
1076
- const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
1077
- const newFunction = {
1078
- $id: remoteFunction.$id,
1079
- name: remoteFunction.name,
1080
- runtime: remoteFunction.runtime,
1081
- execute: remoteFunction.execute || [],
1082
- events: remoteFunction.events || [],
1083
- schedule: remoteFunction.schedule || "",
1084
- timeout: remoteFunction.timeout || 15,
1085
- enabled: remoteFunction.enabled !== false,
1086
- logging: remoteFunction.logging !== false,
1087
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
1088
- commands: remoteFunction.commands || "npm install",
1089
- scopes: (remoteFunction.scopes || []),
1090
- installationId: remoteFunction.installationId,
1091
- providerRepositoryId: remoteFunction.providerRepositoryId,
1092
- providerBranch: remoteFunction.providerBranch,
1093
- providerSilentMode: remoteFunction.providerSilentMode,
1094
- providerRootDirectory: remoteFunction.providerRootDirectory,
1095
- specification: remoteFunction.specification,
1096
- };
1097
- this.controller.config.functions =
1098
- this.controller.config.functions || [];
1099
- this.controller.config.functions.push(newFunction);
1100
- console.log(chalk.green(`Added config for remote function: ${func.name}`));
1101
- }
1102
- }
1103
- }
1104
- // Schema generation and collection file writing is handled by controller.synchronizeConfigurations()
1105
- }
1106
- console.log(chalk.green("✨ Configurations synchronized successfully!"));
1107
- }
1108
- async backupDatabase() {
1109
- if (!this.controller.database) {
1110
- throw new Error("Database is not initialized, is the config file correct & created?");
1111
- }
1112
- const databases = await fetchAllDatabases(this.controller.database);
1113
- const selectedDatabases = await this.selectDatabases(databases, "Select databases to backup:");
1114
- for (const db of selectedDatabases) {
1115
- console.log(chalk.yellow(`Backing up database: ${db.name}`));
1116
- await this.controller.backupDatabase(db);
1117
- }
1118
- MessageFormatter.success("Database backup completed", { prefix: "Backup" });
1119
- }
1120
- async wipeDatabase() {
1121
- if (!this.controller.database || !this.controller.storage) {
1122
- throw new Error("Database or Storage is not initialized, is the config file correct & created?");
1123
- }
1124
- const databases = await fetchAllDatabases(this.controller.database);
1125
- const storage = await listBuckets(this.controller.storage);
1126
- const selectedDatabases = await this.selectDatabases(databases, "Select databases to wipe:");
1127
- const { selectedStorage } = await inquirer.prompt([
1128
- {
1129
- type: "checkbox",
1130
- name: "selectedStorage",
1131
- message: "Select storage buckets to wipe:",
1132
- choices: storage.buckets.map((s) => ({ name: s.name, value: s.$id })),
1133
- },
1134
- ]);
1135
- const { wipeUsers } = await inquirer.prompt([
1136
- {
1137
- type: "confirm",
1138
- name: "wipeUsers",
1139
- message: "Do you want to wipe users as well?",
1140
- default: false,
1141
- },
1142
- ]);
1143
- const databaseNames = selectedDatabases.map(db => db.name);
1144
- const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
1145
- includeStorage: selectedStorage.length > 0,
1146
- includeUsers: wipeUsers
1147
- });
1148
- if (confirmed) {
1149
- MessageFormatter.info("Starting wipe operation...", { prefix: "Wipe" });
1150
- for (const db of selectedDatabases) {
1151
- await this.controller.wipeDatabase(db);
1152
- }
1153
- for (const bucketId of selectedStorage) {
1154
- await this.controller.wipeDocumentStorage(bucketId);
1155
- }
1156
- if (wipeUsers) {
1157
- await this.controller.wipeUsers();
1158
- }
1159
- MessageFormatter.success("Wipe operation completed", { prefix: "Wipe" });
1160
- }
1161
- else {
1162
- MessageFormatter.info("Wipe operation cancelled", { prefix: "Wipe" });
1163
- }
1164
- }
1165
- async wipeCollections() {
1166
- if (!this.controller.database) {
1167
- throw new Error("Database is not initialized, is the config file correct & created?");
1168
- }
1169
- const databases = await fetchAllDatabases(this.controller.database);
1170
- const selectedDatabases = await this.selectDatabases(databases, "Select the database(s) containing the collections to wipe:", true);
1171
- for (const database of selectedDatabases) {
1172
- const collections = await this.selectCollections(database, this.controller.database, `Select collections to wipe from ${database.name}:`, true, undefined, true);
1173
- const collectionNames = collections.map(c => c.name);
1174
- const confirmed = await ConfirmationDialogs.confirmCollectionWipe(database.name, collectionNames);
1175
- if (confirmed) {
1176
- MessageFormatter.info(`Wiping selected collections from ${database.name}...`, { prefix: "Wipe" });
1177
- for (const collection of collections) {
1178
- await this.controller.wipeCollection(database, collection);
1179
- MessageFormatter.success(`Collection ${collection.name} wiped successfully`, { prefix: "Wipe" });
1180
- }
1181
- }
1182
- else {
1183
- MessageFormatter.info(`Wipe operation cancelled for ${database.name}`, { prefix: "Wipe" });
1184
- }
1185
- }
1186
- MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
1187
- }
1188
- async generateSchemas() {
1189
- console.log(chalk.yellow("Generating schemas..."));
1190
- // Prompt user for schema type preference
1191
- const { schemaType } = await inquirer.prompt([
1192
- {
1193
- type: "list",
1194
- name: "schemaType",
1195
- message: "What type of schemas would you like to generate?",
1196
- choices: [
1197
- { name: "TypeScript (Zod) schemas", value: "zod" },
1198
- { name: "JSON schemas", value: "json" },
1199
- { name: "Both TypeScript and JSON schemas", value: "both" },
1200
- ],
1201
- default: "both",
1202
- },
1203
- ]);
1204
- // Get the config folder path (where the config file is located)
1205
- const configFolderPath = this.controller.getAppwriteFolderPath();
1206
- if (!configFolderPath) {
1207
- MessageFormatter.error("Failed to get config folder path", undefined, { prefix: "Schemas" });
1208
- return;
1209
- }
1210
- // Create SchemaGenerator with the correct base path and generate schemas
1211
- const schemaGenerator = new SchemaGenerator(this.controller.config, configFolderPath);
1212
- schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
1213
- MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
1214
- }
1215
- async generateConstants() {
1216
- console.log(chalk.yellow("Generating cross-language constants..."));
1217
- if (!this.controller?.config) {
1218
- MessageFormatter.error("No configuration found", undefined, { prefix: "Constants" });
1219
- return;
1220
- }
1221
- // Prompt for languages
1222
- const { languages } = await inquirer.prompt([
1223
- {
1224
- type: "checkbox",
1225
- name: "languages",
1226
- message: "Select languages for constants generation:",
1227
- choices: [
1228
- { name: "TypeScript", value: "typescript", checked: true },
1229
- { name: "JavaScript", value: "javascript" },
1230
- { name: "Python", value: "python" },
1231
- { name: "PHP", value: "php" },
1232
- { name: "Dart", value: "dart" },
1233
- { name: "JSON", value: "json" },
1234
- { name: "Environment Variables", value: "env" },
1235
- ],
1236
- validate: (input) => {
1237
- if (input.length === 0) {
1238
- return "Please select at least one language";
1239
- }
1240
- return true;
1241
- },
1242
- },
1243
- ]);
1244
- // Determine default output directory based on config location
1245
- const configPath = this.controller.getAppwriteFolderPath();
1246
- const defaultOutputDir = configPath
1247
- ? path.join(configPath, "constants")
1248
- : path.join(process.cwd(), "constants");
1249
- // Prompt for output directory
1250
- const { outputDir } = await inquirer.prompt([
1251
- {
1252
- type: "input",
1253
- name: "outputDir",
1254
- message: "Output directory for constants files:",
1255
- default: defaultOutputDir,
1256
- validate: (input) => {
1257
- if (!input.trim()) {
1258
- return "Output directory cannot be empty";
1259
- }
1260
- return true;
1261
- },
1262
- },
1263
- ]);
1264
- try {
1265
- const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
1266
- const generator = new ConstantsGenerator(this.controller.config);
1267
- MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
1268
- await generator.generateFiles(languages, outputDir);
1269
- MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
1270
- }
1271
- catch (error) {
1272
- MessageFormatter.error("Failed to generate constants", error instanceof Error ? error : new Error(String(error)), { prefix: "Constants" });
1273
- }
1274
- }
1275
- async importData() {
1276
- console.log(chalk.yellow("Importing data..."));
1277
- const { doBackup } = await inquirer.prompt([
1278
- {
1279
- type: "confirm",
1280
- name: "doBackup",
1281
- message: "Do you want to perform a backup before importing?",
1282
- default: true,
1283
- },
1284
- ]);
1285
- const databases = await this.selectDatabases(await fetchAllDatabases(this.controller.database), "Select databases to import data into:", true);
1286
- const collections = await this.selectCollections(databases[0], this.controller.database, "Select collections to import data into (leave empty for all):", true);
1287
- const { shouldWriteFile } = await inquirer.prompt([
1288
- {
1289
- type: "confirm",
1290
- name: "shouldWriteFile",
1291
- message: "Do you want to write the imported data to a file?",
1292
- default: false,
1293
- },
1294
- ]);
1295
- const options = {
1296
- databases,
1297
- collections: collections.map((c) => c.name),
1298
- doBackup,
1299
- importData: true,
1300
- shouldWriteFile,
1301
- };
1302
- try {
1303
- await this.controller.importData(options);
1304
- console.log(chalk.green("Data import completed successfully."));
1305
- }
1306
- catch (error) {
1307
- console.error(chalk.red("Error importing data:"), error);
1308
- }
1309
- }
1310
- async transferData() {
1311
- if (!this.controller.database) {
1312
- throw new Error("Database is not initialized, is the config file correct & created?");
1313
- }
1314
- const { isRemote } = await inquirer.prompt([
1315
- {
1316
- type: "confirm",
1317
- name: "isRemote",
1318
- message: "Is this a remote transfer?",
1319
- default: false,
1320
- },
1321
- ]);
1322
- let sourceClient = this.controller.database;
1323
- let targetClient;
1324
- let sourceDatabases;
1325
- let targetDatabases;
1326
- let remoteOptions;
1327
- if (isRemote) {
1328
- remoteOptions = await inquirer.prompt([
1329
- {
1330
- type: "input",
1331
- name: "transferEndpoint",
1332
- message: "Enter the remote endpoint:",
1333
- },
1334
- {
1335
- type: "input",
1336
- name: "transferProject",
1337
- message: "Enter the remote project ID:",
1338
- },
1339
- {
1340
- type: "input",
1341
- name: "transferKey",
1342
- message: "Enter the remote API key:",
1343
- },
1344
- ]);
1345
- const remoteClient = getClient(remoteOptions.transferEndpoint, remoteOptions.transferProject, remoteOptions.transferKey);
1346
- targetClient = new Databases(remoteClient);
1347
- sourceDatabases = await fetchAllDatabases(sourceClient);
1348
- targetDatabases = await fetchAllDatabases(targetClient);
1349
- }
1350
- else {
1351
- targetClient = sourceClient;
1352
- const allDatabases = await fetchAllDatabases(sourceClient);
1353
- sourceDatabases = targetDatabases = allDatabases;
1354
- }
1355
- const fromDbs = await this.selectDatabases(sourceDatabases, "Select the source database:", false);
1356
- const fromDb = fromDbs[0];
1357
- if (!fromDb) {
1358
- throw new Error("No source database selected");
1359
- }
1360
- const availableDbs = targetDatabases.filter((db) => db.$id !== fromDb.$id);
1361
- const targetDbs = await this.selectDatabases(availableDbs, "Select the target database:", false);
1362
- const targetDb = targetDbs[0];
1363
- if (!targetDb) {
1364
- throw new Error("No target database selected");
1365
- }
1366
- const selectedCollections = await this.selectCollections(fromDb, sourceClient, "Select collections to transfer:", true, false // don't prefer local for transfers
1367
- );
1368
- const { transferStorage } = await inquirer.prompt([
1369
- {
1370
- type: "confirm",
1371
- name: "transferStorage",
1372
- message: "Do you want to transfer storage as well?",
1373
- default: false,
1374
- },
1375
- ]);
1376
- let sourceBucket, targetBucket;
1377
- if (transferStorage) {
1378
- const sourceStorage = new Storage(this.controller.appwriteServer);
1379
- const targetStorage = isRemote
1380
- ? new Storage(getClient(remoteOptions.transferEndpoint, remoteOptions.transferProject, remoteOptions.transferKey))
1381
- : sourceStorage;
1382
- const sourceBuckets = await listBuckets(sourceStorage);
1383
- const targetBuckets = isRemote
1384
- ? await listBuckets(targetStorage)
1385
- : sourceBuckets;
1386
- const sourceBucketPicked = await this.selectBuckets(sourceBuckets.buckets, "Select the source bucket:", false);
1387
- const targetBucketPicked = await this.selectBuckets(targetBuckets.buckets, "Select the target bucket:", false);
1388
- sourceBucket = sourceBucketPicked[0];
1389
- targetBucket = targetBucketPicked[0];
1390
- }
1391
- let transferOptions = {
1392
- fromDb,
1393
- targetDb,
1394
- isRemote,
1395
- collections: selectedCollections.length > 0
1396
- ? selectedCollections.map((c) => c.$id)
1397
- : undefined,
1398
- sourceBucket,
1399
- targetBucket,
1400
- };
1401
- if (isRemote && remoteOptions) {
1402
- transferOptions = {
1403
- ...transferOptions,
1404
- ...remoteOptions,
1405
- };
1406
- }
1407
- console.log(chalk.yellow("Transferring data..."));
1408
- await this.controller.transferData(transferOptions);
1409
- console.log(chalk.green("Data transfer completed."));
1410
- }
1411
754
  getLocalCollections() {
1412
755
  const configCollections = this.controller.config?.collections || [];
1413
756
  // @ts-expect-error - appwrite invalid types
@@ -1422,6 +765,8 @@ export class InteractiveCLI {
1422
765
  indexes: c.indexes || [],
1423
766
  $permissions: PermissionToAppwritePermission(c.$permissions) || [],
1424
767
  databaseId: c.databaseId,
768
+ _isFromTablesDir: c._isFromTablesDir || false,
769
+ _sourceFolder: c._isFromTablesDir ? 'tables' : 'collections',
1425
770
  }));
1426
771
  }
1427
772
  getLocalDatabases() {
@@ -1434,58 +779,29 @@ export class InteractiveCLI {
1434
779
  enabled: true,
1435
780
  }));
1436
781
  }
1437
- async reloadConfig() {
1438
- MessageFormatter.progress("Reloading configuration files...", { prefix: "Config" });
1439
- try {
1440
- await this.controller.reloadConfig();
1441
- MessageFormatter.success("Configuration files reloaded successfully", { prefix: "Config" });
782
+ /**
783
+ * Extract session information from current controller for preservation
784
+ */
785
+ extractSessionFromController() {
786
+ if (!this.controller?.config) {
787
+ return undefined;
1442
788
  }
1443
- catch (error) {
1444
- MessageFormatter.error("Failed to reload configuration files", error instanceof Error ? error : new Error(String(error)), { prefix: "Config" });
789
+ const sessionInfo = this.controller.getSessionInfo();
790
+ const config = this.controller.config;
791
+ if (!config.appwriteEndpoint || !config.appwriteProject) {
792
+ return undefined;
1445
793
  }
1446
- }
1447
- async updateFunctionSpec() {
1448
- const remoteFunctions = await listFunctions(this.controller.appwriteServer, [Query.limit(1000)]);
1449
- const localFunctions = this.getLocalFunctions();
1450
- const allFunctions = [
1451
- ...remoteFunctions.functions,
1452
- ...localFunctions.filter((f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)),
1453
- ];
1454
- const functionsToUpdate = await inquirer.prompt([
1455
- {
1456
- type: "checkbox",
1457
- name: "functionId",
1458
- message: "Select functions to update:",
1459
- choices: allFunctions.map((f) => ({
1460
- name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name)
1461
- ? " (Local)"
1462
- : " (Remote)"}`,
1463
- value: f.$id,
1464
- })),
1465
- loop: true,
1466
- },
1467
- ]);
1468
- const specifications = await listSpecifications(this.controller.appwriteServer);
1469
- const { specification } = await inquirer.prompt([
1470
- {
1471
- type: "list",
1472
- name: "specification",
1473
- message: "Select new specification:",
1474
- choices: specifications.specifications.map((s) => ({
1475
- name: `${s.slug}`,
1476
- value: s.slug,
1477
- })),
1478
- },
1479
- ]);
1480
- try {
1481
- for (const functionId of functionsToUpdate.functionId) {
1482
- await this.controller.updateFunctionSpecifications(functionId, specification);
1483
- console.log(chalk.green(`Successfully updated function specification to ${specification}`));
1484
- }
1485
- }
1486
- catch (error) {
1487
- console.error(chalk.red("Error updating function specification:"), error);
794
+ const result = {
795
+ appwriteEndpoint: config.appwriteEndpoint,
796
+ appwriteProject: config.appwriteProject,
797
+ appwriteKey: config.appwriteKey
798
+ };
799
+ // Add session data if available
800
+ if (sessionInfo.hasSession) {
801
+ result.sessionCookie = this.controller.sessionCookie;
802
+ result.sessionMetadata = this.controller.sessionMetadata;
1488
803
  }
804
+ return result;
1489
805
  }
1490
806
  async detectConfigurationType() {
1491
807
  try {
@@ -1530,283 +846,4 @@ export class InteractiveCLI {
1530
846
  return allChoices.filter(choice => choice !== CHOICES.MIGRATE_CONFIG);
1531
847
  }
1532
848
  }
1533
- async migrateTypeScriptConfig() {
1534
- try {
1535
- MessageFormatter.info("Starting TypeScript to YAML configuration migration...", { prefix: "Migration" });
1536
- // Perform the migration
1537
- await migrateConfig(this.currentDir);
1538
- // Reset the detection flag
1539
- this.isUsingTypeScriptConfig = false;
1540
- // Reset the controller to pick up the new config
1541
- this.controller = undefined;
1542
- MessageFormatter.success("Migration completed successfully!", { prefix: "Migration" });
1543
- MessageFormatter.info("Your configuration has been migrated to the .appwrite directory structure", { prefix: "Migration" });
1544
- MessageFormatter.info("You can now use YAML configuration for easier management", { prefix: "Migration" });
1545
- }
1546
- catch (error) {
1547
- MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
1548
- }
1549
- }
1550
- async comprehensiveTransfer() {
1551
- MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
1552
- try {
1553
- // Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
1554
- await this.initControllerIfNeeded();
1555
- // Check if user has an appwrite config for easier setup
1556
- const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
1557
- this.controller?.config?.appwriteProject &&
1558
- this.controller?.config?.appwriteKey;
1559
- let sourceConfig;
1560
- let targetConfig;
1561
- if (hasAppwriteConfig) {
1562
- // Offer to use existing config for source
1563
- const { useConfigForSource } = await inquirer.prompt([
1564
- {
1565
- type: "confirm",
1566
- name: "useConfigForSource",
1567
- message: "Use your current appwriteConfig as the source?",
1568
- default: true,
1569
- },
1570
- ]);
1571
- if (useConfigForSource) {
1572
- sourceConfig = {
1573
- sourceEndpoint: this.controller.config.appwriteEndpoint,
1574
- sourceProject: this.controller.config.appwriteProject,
1575
- sourceKey: this.controller.config.appwriteKey,
1576
- };
1577
- MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
1578
- }
1579
- else {
1580
- // Get source configuration manually
1581
- sourceConfig = await inquirer.prompt([
1582
- {
1583
- type: "input",
1584
- name: "sourceEndpoint",
1585
- message: "Enter the source Appwrite endpoint:",
1586
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1587
- },
1588
- {
1589
- type: "input",
1590
- name: "sourceProject",
1591
- message: "Enter the source project ID:",
1592
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1593
- },
1594
- {
1595
- type: "password",
1596
- name: "sourceKey",
1597
- message: "Enter the source API key:",
1598
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1599
- },
1600
- ]);
1601
- }
1602
- // Offer to use existing config for target
1603
- const { useConfigForTarget } = await inquirer.prompt([
1604
- {
1605
- type: "confirm",
1606
- name: "useConfigForTarget",
1607
- message: "Use your current appwriteConfig as the target?",
1608
- default: false,
1609
- },
1610
- ]);
1611
- if (useConfigForTarget) {
1612
- targetConfig = {
1613
- targetEndpoint: this.controller.config.appwriteEndpoint,
1614
- targetProject: this.controller.config.appwriteProject,
1615
- targetKey: this.controller.config.appwriteKey,
1616
- };
1617
- MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
1618
- }
1619
- else {
1620
- // Get target configuration manually
1621
- targetConfig = await inquirer.prompt([
1622
- {
1623
- type: "input",
1624
- name: "targetEndpoint",
1625
- message: "Enter the target Appwrite endpoint:",
1626
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1627
- },
1628
- {
1629
- type: "input",
1630
- name: "targetProject",
1631
- message: "Enter the target project ID:",
1632
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1633
- },
1634
- {
1635
- type: "password",
1636
- name: "targetKey",
1637
- message: "Enter the target API key:",
1638
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1639
- },
1640
- ]);
1641
- }
1642
- }
1643
- else {
1644
- // No appwrite config found, get both configurations manually
1645
- MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
1646
- // Get source configuration
1647
- sourceConfig = await inquirer.prompt([
1648
- {
1649
- type: "input",
1650
- name: "sourceEndpoint",
1651
- message: "Enter the source Appwrite endpoint:",
1652
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1653
- },
1654
- {
1655
- type: "input",
1656
- name: "sourceProject",
1657
- message: "Enter the source project ID:",
1658
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1659
- },
1660
- {
1661
- type: "password",
1662
- name: "sourceKey",
1663
- message: "Enter the source API key:",
1664
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1665
- },
1666
- ]);
1667
- // Get target configuration
1668
- targetConfig = await inquirer.prompt([
1669
- {
1670
- type: "input",
1671
- name: "targetEndpoint",
1672
- message: "Enter the target Appwrite endpoint:",
1673
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1674
- },
1675
- {
1676
- type: "input",
1677
- name: "targetProject",
1678
- message: "Enter the target project ID:",
1679
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1680
- },
1681
- {
1682
- type: "password",
1683
- name: "targetKey",
1684
- message: "Enter the target API key:",
1685
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
1686
- },
1687
- ]);
1688
- }
1689
- // Get transfer options
1690
- const transferOptions = await inquirer.prompt([
1691
- {
1692
- type: "checkbox",
1693
- name: "transferTypes",
1694
- message: "Select what to transfer:",
1695
- choices: [
1696
- { name: "👥 Users", value: "users", checked: true },
1697
- { name: "👥 Teams", value: "teams", checked: true },
1698
- { name: "🗄️ Databases", value: "databases", checked: true },
1699
- { name: "📦 Storage Buckets", value: "buckets", checked: true },
1700
- { name: "⚡ Functions", value: "functions", checked: true },
1701
- ],
1702
- validate: (input) => input.length > 0 || "Select at least one transfer type",
1703
- },
1704
- {
1705
- type: "list",
1706
- name: "concurrencyLimit",
1707
- message: "Select concurrency limit:",
1708
- choices: [
1709
- { name: "5 (Conservative) - Users: 2, Files: 1", value: 5 },
1710
- { name: "10 (Balanced) - Users: 5, Files: 2", value: 10 },
1711
- { name: "15 - Users: 7, Files: 3", value: 15 },
1712
- { name: "20 - Users: 10, Files: 5", value: 20 },
1713
- { name: "25 - Users: 12, Files: 6", value: 25 },
1714
- { name: "30 - Users: 15, Files: 7", value: 30 },
1715
- { name: "35 - Users: 17, Files: 8", value: 35 },
1716
- { name: "40 - Users: 20, Files: 10", value: 40 },
1717
- { name: "45 - Users: 22, Files: 11", value: 45 },
1718
- { name: "50 - Users: 25, Files: 12", value: 50 },
1719
- { name: "55 - Users: 27, Files: 13", value: 55 },
1720
- { name: "60 - Users: 30, Files: 15", value: 60 },
1721
- { name: "65 - Users: 32, Files: 16", value: 65 },
1722
- { name: "70 - Users: 35, Files: 17", value: 70 },
1723
- { name: "75 - Users: 37, Files: 18", value: 75 },
1724
- { name: "80 - Users: 40, Files: 20", value: 80 },
1725
- { name: "85 - Users: 42, Files: 21", value: 85 },
1726
- { name: "90 - Users: 45, Files: 22", value: 90 },
1727
- { name: "95 - Users: 47, Files: 23", value: 95 },
1728
- { name: "100 (Aggressive) - Users: 50, Files: 25", value: 100 },
1729
- ],
1730
- default: 10,
1731
- },
1732
- {
1733
- type: "confirm",
1734
- name: "dryRun",
1735
- message: "Run in dry-run mode (no actual changes)?",
1736
- default: false,
1737
- },
1738
- ]);
1739
- // Confirmation
1740
- const { confirmed } = await inquirer.prompt([
1741
- {
1742
- type: "confirm",
1743
- name: "confirmed",
1744
- message: `Are you sure you want to ${transferOptions.dryRun ? "dry-run" : "perform"} comprehensive transfer from ${sourceConfig.sourceEndpoint} to ${targetConfig.targetEndpoint}?`,
1745
- default: false,
1746
- },
1747
- ]);
1748
- if (!confirmed) {
1749
- MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
1750
- return;
1751
- }
1752
- // Password preservation information
1753
- if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
1754
- MessageFormatter.info("User Password Transfer Information:", { prefix: "Transfer" });
1755
- MessageFormatter.info("✅ Users with hashed passwords (Argon2, Bcrypt, Scrypt, MD5, SHA, PHPass) will preserve their passwords", { prefix: "Transfer" });
1756
- MessageFormatter.info("⚠️ Users without hash information will receive temporary passwords and need to reset", { prefix: "Transfer" });
1757
- MessageFormatter.info("🔒 All user data (preferences, labels, verification status) will be preserved", { prefix: "Transfer" });
1758
- const { continueWithUsers } = await inquirer.prompt([
1759
- {
1760
- type: "confirm",
1761
- name: "continueWithUsers",
1762
- message: "Continue with user transfer?",
1763
- default: true,
1764
- },
1765
- ]);
1766
- if (!continueWithUsers) {
1767
- // Remove users from transfer types
1768
- transferOptions.transferTypes = transferOptions.transferTypes.filter((type) => type !== "users");
1769
- if (transferOptions.transferTypes.length === 0) {
1770
- MessageFormatter.info("No transfer types selected, cancelling", { prefix: "Transfer" });
1771
- return;
1772
- }
1773
- }
1774
- }
1775
- // Execute comprehensive transfer
1776
- const comprehensiveTransferOptions = {
1777
- sourceEndpoint: sourceConfig.sourceEndpoint,
1778
- sourceProject: sourceConfig.sourceProject,
1779
- sourceKey: sourceConfig.sourceKey,
1780
- targetEndpoint: targetConfig.targetEndpoint,
1781
- targetProject: targetConfig.targetProject,
1782
- targetKey: targetConfig.targetKey,
1783
- transferUsers: transferOptions.transferTypes.includes("users"),
1784
- transferTeams: transferOptions.transferTypes.includes("teams"),
1785
- transferDatabases: transferOptions.transferTypes.includes("databases"),
1786
- transferBuckets: transferOptions.transferTypes.includes("buckets"),
1787
- transferFunctions: transferOptions.transferTypes.includes("functions"),
1788
- concurrencyLimit: transferOptions.concurrencyLimit,
1789
- dryRun: transferOptions.dryRun,
1790
- };
1791
- const transfer = new ComprehensiveTransfer(comprehensiveTransferOptions);
1792
- const results = await transfer.execute();
1793
- // Display results
1794
- if (transferOptions.dryRun) {
1795
- MessageFormatter.success("Dry run completed successfully!", { prefix: "Transfer" });
1796
- }
1797
- else {
1798
- MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
1799
- if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
1800
- MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
1801
- MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
1802
- }
1803
- if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
1804
- MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
1805
- }
1806
- }
1807
- }
1808
- catch (error) {
1809
- MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1810
- }
1811
- }
1812
849
  }