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,7 +1,5 @@
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 {
@@ -13,12 +11,7 @@ import {
13
11
  Query,
14
12
  Functions,
15
13
  } from "node-appwrite";
16
- import { getClient } from "./utils/getClientFromConfig.js";
17
- import type { TransferOptions } from "./migrations/transfer.js";
18
- import { ComprehensiveTransfer, type ComprehensiveTransferOptions } from "./migrations/comprehensiveTransfer.js";
19
14
  import {
20
- AppwriteFunctionSchema,
21
- parseAttribute,
22
15
  PermissionToAppwritePermission,
23
16
  RuntimeSchema,
24
17
  permissionSchema,
@@ -33,27 +26,29 @@ import { ulid } from "ulidx";
33
26
  import chalk from "chalk";
34
27
  import { DateTime } from "luxon";
35
28
  import {
36
- createFunctionTemplate,
37
- deleteFunction,
38
- downloadLatestFunctionDeployment,
39
29
  getFunction,
30
+ downloadLatestFunctionDeployment,
40
31
  listFunctions,
41
- listSpecifications,
42
32
  } from "./functions/methods.js";
43
- import { deployLocalFunction } from "./functions/deployments.js";
44
33
  import { join } from "node:path";
45
34
  import path from "path";
46
35
  import fs from "node:fs";
47
36
  import os from "node:os";
48
- import { SchemaGenerator } from "./shared/schemaGenerator.js";
49
- import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
50
37
  import { MessageFormatter } from "./shared/messageFormatter.js";
51
- import { migrateConfig } from "./utils/configMigration.js";
52
38
  import { findAppwriteConfig } from "./utils/loadConfigs.js";
53
- import { findYamlConfig, addFunctionToYamlConfig } from "./config/yamlConfig.js";
39
+ import { findYamlConfig } from "./config/yamlConfig.js";
40
+
41
+ // Import command modules
42
+ import { configCommands } from "./cli/commands/configCommands.js";
43
+ import { databaseCommands } from "./cli/commands/databaseCommands.js";
44
+ import { functionCommands } from "./cli/commands/functionCommands.js";
45
+ import { transferCommands } from "./cli/commands/transferCommands.js";
46
+ import { schemaCommands } from "./cli/commands/schemaCommands.js";
54
47
 
55
48
  enum CHOICES {
56
49
  MIGRATE_CONFIG = "🔄 Migrate TypeScript config to YAML (.appwrite structure)",
50
+ VALIDATE_CONFIG = "✅ Validate configuration (collections/tables conflicts)",
51
+ MIGRATE_COLLECTIONS_TO_TABLES = "🔀 Migrate collections to tables format",
57
52
  CREATE_COLLECTION_CONFIG = "📄 Create collection config file",
58
53
  CREATE_FUNCTION = "⚡ Create a new function, from scratch or using a template",
59
54
  DEPLOY_FUNCTION = "🚀 Deploy function(s)",
@@ -113,75 +108,80 @@ export class InteractiveCLI {
113
108
 
114
109
  switch (action) {
115
110
  case CHOICES.MIGRATE_CONFIG:
116
- await this.migrateTypeScriptConfig();
111
+ await configCommands.migrateTypeScriptConfig(this);
112
+ break;
113
+ case CHOICES.VALIDATE_CONFIG:
114
+ await configCommands.validateConfiguration(this);
115
+ break;
116
+ case CHOICES.MIGRATE_COLLECTIONS_TO_TABLES:
117
+ await configCommands.migrateCollectionsToTables(this);
117
118
  break;
118
119
  case CHOICES.CREATE_COLLECTION_CONFIG:
119
- await this.createCollectionConfig();
120
+ await configCommands.createCollectionConfig(this);
120
121
  break;
121
122
  case CHOICES.CREATE_FUNCTION:
122
123
  await this.initControllerIfNeeded();
123
- await this.createFunction();
124
+ await functionCommands.createFunction(this);
124
125
  break;
125
126
  case CHOICES.DEPLOY_FUNCTION:
126
127
  await this.initControllerIfNeeded();
127
- await this.deployFunction();
128
+ await functionCommands.deployFunction(this);
128
129
  break;
129
130
  case CHOICES.DELETE_FUNCTION:
130
131
  await this.initControllerIfNeeded();
131
- await this.deleteFunction();
132
+ await functionCommands.deleteFunction(this);
132
133
  break;
133
134
  case CHOICES.SETUP_DIRS_FILES:
134
- await setupDirsFiles(false, this.currentDir);
135
+ await schemaCommands.setupDirsFiles(this, false);
135
136
  break;
136
137
  case CHOICES.SETUP_DIRS_FILES_WITH_EXAMPLE_DATA:
137
- await setupDirsFiles(true, this.currentDir);
138
+ await schemaCommands.setupDirsFiles(this, true);
138
139
  break;
139
140
  case CHOICES.SYNCHRONIZE_CONFIGURATIONS:
140
141
  await this.initControllerIfNeeded();
141
- await this.synchronizeConfigurations();
142
+ await databaseCommands.synchronizeConfigurations(this);
142
143
  break;
143
144
  case CHOICES.SYNC_DB:
144
145
  await this.initControllerIfNeeded();
145
- await this.syncDb();
146
+ await databaseCommands.syncDb(this);
146
147
  break;
147
148
  case CHOICES.TRANSFER_DATA:
148
149
  await this.initControllerIfNeeded();
149
- await this.transferData();
150
+ await transferCommands.transferData(this);
150
151
  break;
151
152
  case CHOICES.COMPREHENSIVE_TRANSFER:
152
- await this.comprehensiveTransfer();
153
+ await transferCommands.comprehensiveTransfer(this);
153
154
  break;
154
155
  case CHOICES.BACKUP_DATABASE:
155
156
  await this.initControllerIfNeeded();
156
- await this.backupDatabase();
157
+ await databaseCommands.backupDatabase(this);
157
158
  break;
158
159
  case CHOICES.WIPE_DATABASE:
159
160
  await this.initControllerIfNeeded();
160
- await this.wipeDatabase();
161
+ await databaseCommands.wipeDatabase(this);
161
162
  break;
162
163
  case CHOICES.WIPE_COLLECTIONS:
163
164
  await this.initControllerIfNeeded();
164
- await this.wipeCollections();
165
+ await databaseCommands.wipeCollections(this);
165
166
  break;
166
167
  case CHOICES.GENERATE_SCHEMAS:
167
168
  await this.initControllerIfNeeded();
168
- await this.generateSchemas();
169
+ await schemaCommands.generateSchemas(this);
169
170
  break;
170
171
  case CHOICES.GENERATE_CONSTANTS:
171
172
  await this.initControllerIfNeeded();
172
- await this.generateConstants();
173
+ await schemaCommands.generateConstants(this);
173
174
  break;
174
175
  case CHOICES.IMPORT_DATA:
175
176
  await this.initControllerIfNeeded();
176
- await this.importData();
177
+ await schemaCommands.importData(this);
177
178
  break;
178
179
  case CHOICES.RELOAD_CONFIG:
179
- await this.initControllerIfNeeded();
180
- await this.reloadConfig();
180
+ await configCommands.reloadConfigWithSessionPreservation(this);
181
181
  break;
182
182
  case CHOICES.UPDATE_FUNCTION_SPEC:
183
183
  await this.initControllerIfNeeded();
184
- await this.updateFunctionSpec();
184
+ await functionCommands.updateFunctionSpec(this);
185
185
  break;
186
186
  case CHOICES.EXIT:
187
187
  MessageFormatter.success("Goodbye!");
@@ -198,6 +198,26 @@ export class InteractiveCLI {
198
198
  if (!this.controller) {
199
199
  this.controller = new UtilsController(this.currentDir, directConfig);
200
200
  await this.controller.init();
201
+ } else {
202
+ // Extract session info from existing controller before reinitializing
203
+ const sessionInfo = this.controller.getSessionInfo();
204
+ if (sessionInfo.hasSession && directConfig) {
205
+ // Create enhanced directConfig with session preservation
206
+ const enhancedDirectConfig = {
207
+ ...directConfig,
208
+ sessionCookie: (this.controller as any).sessionCookie,
209
+ sessionMetadata: (this.controller as any).sessionMetadata
210
+ };
211
+
212
+ // Reinitialize with session preservation
213
+ this.controller = new UtilsController(this.currentDir, enhancedDirectConfig);
214
+ await this.controller.init();
215
+ } else if (directConfig) {
216
+ // Standard reinitialize without session
217
+ this.controller = new UtilsController(this.currentDir, directConfig);
218
+ await this.controller.init();
219
+ }
220
+ // If no directConfig provided, keep existing controller
201
221
  }
202
222
  }
203
223
 
@@ -220,11 +240,7 @@ export class InteractiveCLI {
220
240
  acc.push(db);
221
241
  }
222
242
  return acc;
223
- }, [] as Models.Database[])
224
- .filter((db) => {
225
- const useMigrations = this.controller?.config?.useMigrations ?? true;
226
- return useMigrations || db.name.toLowerCase() !== "migrations";
227
- });
243
+ }, [] as Models.Database[]);
228
244
 
229
245
  const hasLocalAndRemote =
230
246
  allDatabases.some((db) =>
@@ -245,11 +261,7 @@ export class InteractiveCLI {
245
261
  : " (Remote)"
246
262
  : ""),
247
263
  value: db,
248
- }))
249
- .filter((db) => {
250
- const useMigrations = this.controller?.config?.useMigrations ?? true;
251
- return useMigrations || db.name.toLowerCase() !== "migrations";
252
- });
264
+ }));
253
265
 
254
266
  const { selectedDatabases } = await inquirer.prompt([
255
267
  {
@@ -282,10 +294,9 @@ export class InteractiveCLI {
282
294
  Query.equal("name", database.name),
283
295
  ]);
284
296
  if (dbExists.total === 0) {
285
- console.log(
286
- chalk.red(
287
- `Database "${database.name}" does not exist, using only local collection options`
288
- )
297
+ MessageFormatter.warning(
298
+ `Database "${database.name}" does not exist, using only local collection/table options`,
299
+ { prefix: "Database" }
289
300
  );
290
301
  shouldFilterByDatabase = false;
291
302
  } else {
@@ -312,14 +323,27 @@ export class InteractiveCLI {
312
323
  ),
313
324
  ];
314
325
 
315
- if (shouldFilterByDatabase) {
316
- // Keep local entries that don't have databaseId (common in config),
317
- // but still filter remote collections by selected database.
318
- allCollections = allCollections.filter((c) => {
319
- if (!c.databaseId) return true;
320
- return c.databaseId === database.$id;
321
- });
322
- }
326
+ if (shouldFilterByDatabase) {
327
+ // Enhanced filtering for tables with optional databaseId
328
+ allCollections = allCollections.filter((c: any) => {
329
+ // For remote collections, they should match the selected database
330
+ if (remoteCollections.some((rc) => rc.name === c.name)) {
331
+ return c.databaseId === database.$id;
332
+ }
333
+
334
+ // For local collections/tables:
335
+ // - Collections without databaseId are kept (backward compatibility)
336
+ // - Tables with databaseId must match the selected database
337
+ // - Tables without databaseId are kept (fallback for misconfigured tables)
338
+ if (!c.databaseId) return true;
339
+ return c.databaseId === database.$id;
340
+ });
341
+ }
342
+
343
+ // Filter out system tables (those starting with underscore)
344
+ allCollections = allCollections.filter(
345
+ (collection) => !collection.$id.startsWith('_')
346
+ );
323
347
 
324
348
  const hasLocalAndRemote =
325
349
  allCollections.some((coll) =>
@@ -329,18 +353,60 @@ export class InteractiveCLI {
329
353
  (coll) => !configCollections.some((c) => c.name === coll.name)
330
354
  );
331
355
 
356
+ // Enhanced choice display with type indicators
332
357
  const choices = allCollections
333
- .sort((a, b) => a.name.localeCompare(b.name))
334
- .map((collection) => ({
335
- name:
336
- collection.name +
337
- (hasLocalAndRemote
338
- ? configCollections.some((c) => c.name === collection.name)
339
- ? " (Local)"
340
- : " (Remote)"
341
- : ""),
342
- value: collection,
343
- }));
358
+ .sort((a, b) => {
359
+ // Sort by type first (collections before tables), then by name
360
+ const aIsTable = (a as any)._isFromTablesDir || false;
361
+ const bIsTable = (b as any)._isFromTablesDir || false;
362
+
363
+ if (aIsTable !== bIsTable) {
364
+ return aIsTable ? 1 : -1; // Collections first, then tables
365
+ }
366
+
367
+ return a.name.localeCompare(b.name);
368
+ })
369
+ .map((collection) => {
370
+ const localCollection = configCollections.find((c) => c.name === collection.name);
371
+ const isLocal = !!localCollection;
372
+ const isTable = localCollection?._isFromTablesDir || (collection as any)._isFromTablesDir || false;
373
+ const sourceFolder = localCollection?._sourceFolder || (collection as any)._sourceFolder || 'collections';
374
+
375
+ let typeIndicator = '';
376
+ let locationIndicator = '';
377
+
378
+ // Type indicator
379
+ if (isTable) {
380
+ typeIndicator = chalk.cyan('[Table]');
381
+ } else {
382
+ typeIndicator = chalk.green('[Collection]');
383
+ }
384
+
385
+ // Location indicator
386
+ if (hasLocalAndRemote) {
387
+ if (isLocal) {
388
+ locationIndicator = chalk.gray(`(Local/${sourceFolder})`);
389
+ } else {
390
+ locationIndicator = chalk.gray('(Remote)');
391
+ }
392
+ } else if (isLocal) {
393
+ locationIndicator = chalk.gray(`(${sourceFolder}/)`);
394
+ }
395
+
396
+ // Database indicator for tables with explicit databaseId
397
+ let dbIndicator = '';
398
+ if (isTable && collection.databaseId && shouldFilterByDatabase) {
399
+ const matchesCurrentDb = collection.databaseId === database.$id;
400
+ if (!matchesCurrentDb) {
401
+ dbIndicator = chalk.yellow(` [DB: ${collection.databaseId}]`);
402
+ }
403
+ }
404
+
405
+ return {
406
+ name: `${typeIndicator} ${collection.name} ${locationIndicator}${dbIndicator}`,
407
+ value: collection,
408
+ };
409
+ });
344
410
 
345
411
  const { selectedCollections } = await inquirer.prompt([
346
412
  {
@@ -349,13 +415,81 @@ export class InteractiveCLI {
349
415
  message: chalk.blue(message),
350
416
  choices,
351
417
  loop: true,
352
- pageSize: 10,
418
+ pageSize: 15, // Increased page size to accommodate additional info
353
419
  },
354
420
  ]);
355
421
 
356
422
  return selectedCollections;
357
423
  }
358
424
 
425
+ /**
426
+ * Enhanced collection/table selection with better guidance for mixed scenarios
427
+ */
428
+ private async selectCollectionsAndTables(
429
+ database: Models.Database,
430
+ databasesClient: Databases,
431
+ message: string,
432
+ multiSelect = true,
433
+ preferLocal = false,
434
+ shouldFilterByDatabase = false
435
+ ): Promise<Models.Collection[]> {
436
+ const configCollections = this.getLocalCollections();
437
+ const collectionsCount = configCollections.filter(c => !c._isFromTablesDir).length;
438
+ const tablesCount = configCollections.filter(c => c._isFromTablesDir).length;
439
+ const totalCount = collectionsCount + tablesCount;
440
+
441
+ // Provide context about what's available
442
+ if (collectionsCount > 0 && tablesCount > 0) {
443
+ MessageFormatter.info(`\n📋 ${totalCount} total items available:`, { prefix: "Collections" });
444
+ MessageFormatter.info(` Collections: ${collectionsCount} (from collections/ folder)`, { prefix: "Collections" });
445
+ MessageFormatter.info(` Tables: ${tablesCount} (from tables/ folder)`, { prefix: "Collections" });
446
+ } else if (collectionsCount > 0) {
447
+ MessageFormatter.info(`📁 ${collectionsCount} collections available from collections/ folder`, { prefix: "Collections" });
448
+ } else if (tablesCount > 0) {
449
+ MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder`, { prefix: "Collections" });
450
+ }
451
+
452
+ // Ask user if they want to filter by database or show all
453
+ const { filterChoice } = await inquirer.prompt([
454
+ {
455
+ type: "list",
456
+ name: "filterChoice",
457
+ message: chalk.blue("How would you like to view collections/tables?"),
458
+ choices: [
459
+ {
460
+ name: `Show all available collections/tables (${totalCount} total) - You can push any collection to any database`,
461
+ value: "all"
462
+ },
463
+ {
464
+ name: `Filter by database "${database.name}" - Show only related collections/tables`,
465
+ value: "filter"
466
+ }
467
+ ],
468
+ default: "all"
469
+ }
470
+ ]);
471
+
472
+ // User's choice overrides the parameter
473
+ const userWantsFiltering = filterChoice === "filter";
474
+
475
+ // Show appropriate informational message
476
+ if (userWantsFiltering) {
477
+ MessageFormatter.info(`ℹ️ Showing collections/tables related to database "${database.name}"`, { prefix: "Collections" });
478
+ if (tablesCount > 0) {
479
+ const filteredTables = configCollections.filter(c =>
480
+ c._isFromTablesDir && (!c.databaseId || c.databaseId === database.$id)
481
+ ).length;
482
+ if (filteredTables !== tablesCount) {
483
+ MessageFormatter.info(` ${filteredTables}/${tablesCount} tables match this database`, { prefix: "Collections" });
484
+ }
485
+ }
486
+ } else {
487
+ MessageFormatter.info(`ℹ️ Showing all available collections/tables - you can push any collection to any database\n`, { prefix: "Collections" });
488
+ }
489
+
490
+ return this.selectCollections(database, databasesClient, message, multiSelect, preferLocal, userWantsFiltering);
491
+ }
492
+
359
493
  private getTemplateDefaults(template: string) {
360
494
  const defaults = {
361
495
  "typescript-node": {
@@ -364,6 +498,12 @@ export class InteractiveCLI {
364
498
  commands: "npm install && npm run build",
365
499
  specification: "s-0.5vcpu-512mb" as Specification,
366
500
  },
501
+ "hono-typescript": {
502
+ runtime: "node-21.0" as Runtime,
503
+ entrypoint: "src/index.ts",
504
+ commands: "npm install && npm run build",
505
+ specification: "s-0.5vcpu-512mb" as Specification,
506
+ },
367
507
  "uv": {
368
508
  runtime: "python-3.12" as Runtime,
369
509
  entrypoint: "src/index.py",
@@ -386,115 +526,6 @@ export class InteractiveCLI {
386
526
  };
387
527
  }
388
528
 
389
- private async createFunction(): Promise<void> {
390
- const { name } = await inquirer.prompt([
391
- {
392
- type: "input",
393
- name: "name",
394
- message: "Function name:",
395
- validate: (input) => input.length > 0,
396
- },
397
- ]);
398
-
399
- const { template } = await inquirer.prompt([
400
- {
401
- type: "list",
402
- name: "template",
403
- message: "Select a template:",
404
- choices: [
405
- { name: "TypeScript Node.js", value: "typescript-node" },
406
- { name: "Python with UV", value: "uv" },
407
- { name: "Count Documents in Collection", value: "count-docs-in-collection" },
408
- { name: "None (Empty Function)", value: "none" },
409
- ],
410
- },
411
- ]);
412
-
413
- // Get template defaults
414
- const templateDefaults = this.getTemplateDefaults(template);
415
-
416
- const { runtime } = await inquirer.prompt([
417
- {
418
- type: "list",
419
- name: "runtime",
420
- message: "Select runtime:",
421
- choices: Object.values(RuntimeSchema.Values),
422
- default: templateDefaults.runtime,
423
- },
424
- ]);
425
-
426
- const specifications = await listSpecifications(
427
- this.controller!.appwriteServer!
428
- );
429
- const { specification } = await inquirer.prompt([
430
- {
431
- type: "list",
432
- name: "specification",
433
- message: "Select specification:",
434
- choices: [
435
- { name: "None", value: undefined },
436
- ...specifications.specifications.map((s) => ({
437
- name: s.slug,
438
- value: s.slug,
439
- })),
440
- ],
441
- default: templateDefaults.specification,
442
- },
443
- ]);
444
-
445
- const functionConfig: AppwriteFunction = {
446
- $id: ulid(),
447
- name,
448
- runtime,
449
- events: [],
450
- execute: ["any"],
451
- enabled: true,
452
- logging: true,
453
- entrypoint: templateDefaults.entrypoint,
454
- commands: templateDefaults.commands,
455
- specification: specification || templateDefaults.specification,
456
- scopes: [],
457
- timeout: 15,
458
- schedule: "",
459
- installationId: "",
460
- providerRepositoryId: "",
461
- providerBranch: "",
462
- providerSilentMode: false,
463
- providerRootDirectory: "",
464
- templateRepository: "",
465
- templateOwner: "",
466
- templateRootDirectory: "",
467
- };
468
-
469
- if (template !== "none") {
470
- await createFunctionTemplate(
471
- template as "typescript-node" | "uv" | "count-docs-in-collection",
472
- name,
473
- "./functions"
474
- );
475
- }
476
-
477
- // Add to in-memory config
478
- if (!this.controller!.config!.functions) {
479
- this.controller!.config!.functions = [];
480
- }
481
- this.controller!.config!.functions.push(functionConfig);
482
-
483
- // If using YAML config, also add to YAML file
484
- const yamlConfigPath = findYamlConfig(this.currentDir);
485
- if (yamlConfigPath) {
486
- try {
487
- await addFunctionToYamlConfig(yamlConfigPath, functionConfig);
488
- } catch (error) {
489
- MessageFormatter.warning(
490
- `Function created but failed to update YAML config: ${error instanceof Error ? error.message : error}`,
491
- { prefix: "Functions" }
492
- );
493
- }
494
- }
495
-
496
- MessageFormatter.success("Function created successfully!", { prefix: "Functions" });
497
- }
498
529
 
499
530
  private async findFunctionInSubdirectories(
500
531
  basePaths: string[],
@@ -521,9 +552,7 @@ export class InteractiveCLI {
521
552
  try {
522
553
  const stats = await fs.promises.stat(path);
523
554
  if (stats.isDirectory()) {
524
- console.log(
525
- chalk.green(`Found function at common location: ${path}`)
526
- );
555
+ MessageFormatter.success(`Found function at common location: ${path}`, { prefix: "Functions" });
527
556
  return path;
528
557
  }
529
558
  } catch (error) {
@@ -532,11 +561,7 @@ export class InteractiveCLI {
532
561
  }
533
562
 
534
563
  // If not found in common locations, do recursive search
535
- console.log(
536
- chalk.yellow(
537
- "Function not found in common locations, searching subdirectories..."
538
- )
539
- );
564
+ MessageFormatter.info("Function not found in common locations, searching subdirectories...", { prefix: "Functions" });
540
565
 
541
566
  const queue = [...basePaths];
542
567
  const searched = new Set<string>();
@@ -574,7 +599,7 @@ export class InteractiveCLI {
574
599
  );
575
600
 
576
601
  if (hasMatch) {
577
- console.log(chalk.green(`Found function at: ${fullPath}`));
602
+ MessageFormatter.success(`Found function at: ${fullPath}`, { prefix: "Functions" });
578
603
  return fullPath;
579
604
  }
580
605
 
@@ -582,220 +607,13 @@ export class InteractiveCLI {
582
607
  }
583
608
  }
584
609
  } catch (error) {
585
- console.log(
586
- chalk.yellow(`Error reading directory ${currentPath}:`, error)
587
- );
610
+ MessageFormatter.warning(`Error reading directory ${currentPath}: ${error}`, { prefix: "Functions" });
588
611
  }
589
612
  }
590
613
 
591
614
  return null;
592
615
  }
593
616
 
594
- private async deployFunction(): Promise<void> {
595
- await this.initControllerIfNeeded();
596
- if (!this.controller?.config) {
597
- console.log(chalk.red("Failed to initialize controller or load config"));
598
- return;
599
- }
600
-
601
- const functions = await this.selectFunctions(
602
- "Select function(s) to deploy:",
603
- true,
604
- true
605
- );
606
-
607
- if (!functions?.length) {
608
- console.log(chalk.red("No function selected"));
609
- return;
610
- }
611
-
612
- for (const functionConfig of functions) {
613
- if (!functionConfig) {
614
- console.log(chalk.red("Invalid function configuration"));
615
- return;
616
- }
617
-
618
- // Ensure functions array exists
619
- if (!this.controller.config.functions) {
620
- this.controller.config.functions = [];
621
- }
622
-
623
- const functionNameLower = functionConfig.name
624
- .toLowerCase()
625
- .replace(/\s+/g, "-");
626
-
627
- // Debug logging
628
- console.log(chalk.blue(`🔍 Function deployment debug:`));
629
- console.log(chalk.gray(` Function name: ${functionConfig.name}`));
630
- console.log(chalk.gray(` Function ID: ${functionConfig.$id}`));
631
- console.log(chalk.gray(` Config dirPath: ${functionConfig.dirPath || 'undefined'}`));
632
- if (functionConfig.dirPath) {
633
- const expandedPath = functionConfig.dirPath.startsWith('~/')
634
- ? functionConfig.dirPath.replace('~', os.homedir())
635
- : functionConfig.dirPath;
636
- console.log(chalk.gray(` Expanded dirPath: ${expandedPath}`));
637
- }
638
- console.log(chalk.gray(` Appwrite folder: ${this.controller.getAppwriteFolderPath()}`));
639
- console.log(chalk.gray(` Current working dir: ${process.cwd()}`));
640
-
641
- // Helper function to expand tilde in paths
642
- const expandTildePath = (path: string): string => {
643
- if (path.startsWith('~/')) {
644
- return path.replace('~', os.homedir());
645
- }
646
- return path;
647
- };
648
-
649
- // Check locations in priority order:
650
- const priorityLocations = [
651
- // 1. Config dirPath if specified (with tilde expansion)
652
- functionConfig.dirPath ? expandTildePath(functionConfig.dirPath) : undefined,
653
- // 2. Appwrite config folder/functions/name
654
- join(
655
- this.controller.getAppwriteFolderPath()!,
656
- "functions",
657
- functionNameLower
658
- ),
659
- // 3. Current working directory/functions/name
660
- join(process.cwd(), "functions", functionNameLower),
661
- // 4. Current working directory/name
662
- join(process.cwd(), functionNameLower),
663
- ].filter((val): val is string => val !== undefined); // Remove undefined entries (in case dirPath is undefined)
664
-
665
- console.log(chalk.blue(`🔍 Priority locations to check:`));
666
- priorityLocations.forEach((loc, i) => {
667
- console.log(chalk.gray(` ${i + 1}. ${loc}`));
668
- });
669
-
670
- let functionPath: string | null = null;
671
-
672
- // Check each priority location
673
- for (const location of priorityLocations) {
674
- console.log(chalk.gray(` Checking: ${location} - ${fs.existsSync(location) ? 'EXISTS' : 'NOT FOUND'}`));
675
- if (fs.existsSync(location)) {
676
- console.log(chalk.green(`✅ Found function at: ${location}`));
677
- functionPath = location;
678
- break;
679
- }
680
- }
681
-
682
- // If not found in priority locations, do a broader search
683
- if (!functionPath) {
684
- console.log(
685
- chalk.yellow(
686
- `Function not found in primary locations, searching subdirectories...`
687
- )
688
- );
689
-
690
- // Search in both appwrite config directory and current working directory
691
- functionPath = await this.findFunctionInSubdirectories(
692
- [this.controller.getAppwriteFolderPath()!, process.cwd()],
693
- functionNameLower
694
- );
695
- }
696
-
697
- if (!functionPath) {
698
- const { shouldDownload } = await inquirer.prompt([
699
- {
700
- type: "confirm",
701
- name: "shouldDownload",
702
- message:
703
- "Function not found locally. Would you like to download the latest deployment?",
704
- default: false,
705
- },
706
- ]);
707
-
708
- if (shouldDownload) {
709
- try {
710
- console.log(chalk.blue("Downloading latest deployment..."));
711
- const { path: downloadedPath, function: remoteFunction } =
712
- await downloadLatestFunctionDeployment(
713
- this.controller.appwriteServer!,
714
- functionConfig.$id,
715
- join(this.controller.getAppwriteFolderPath()!, "functions")
716
- );
717
- console.log(
718
- chalk.green(`✨ Function downloaded to ${downloadedPath}`)
719
- );
720
-
721
- functionPath = downloadedPath;
722
- functionConfig.dirPath = downloadedPath;
723
-
724
- const existingIndex = this.controller.config.functions.findIndex(
725
- (f) => f?.$id === remoteFunction.$id
726
- );
727
-
728
- if (existingIndex >= 0) {
729
- this.controller.config.functions[existingIndex].dirPath =
730
- downloadedPath;
731
- }
732
-
733
- await this.controller.reloadConfig();
734
- } catch (error) {
735
- console.error(
736
- chalk.red("Failed to download function deployment:"),
737
- error
738
- );
739
- return;
740
- }
741
- } else {
742
- console.log(
743
- chalk.red(
744
- `Function ${functionConfig.name} not found locally. Cannot deploy.`
745
- )
746
- );
747
- return;
748
- }
749
- }
750
-
751
- if (!this.controller.appwriteServer) {
752
- console.log(chalk.red("Appwrite server not initialized"));
753
- return;
754
- }
755
-
756
- try {
757
- await deployLocalFunction(
758
- this.controller.appwriteServer,
759
- functionConfig.name,
760
- {
761
- ...functionConfig,
762
- dirPath: functionPath,
763
- },
764
- functionPath
765
- );
766
- MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
767
- } catch (error) {
768
- console.error(chalk.red("Failed to deploy function:"), error);
769
- }
770
- }
771
- }
772
-
773
- private async deleteFunction(): Promise<void> {
774
- const functions = await this.selectFunctions(
775
- "Select functions to delete:",
776
- true,
777
- false
778
- );
779
-
780
- if (!functions.length) {
781
- console.log(chalk.red("No functions selected"));
782
- return;
783
- }
784
-
785
- for (const func of functions) {
786
- try {
787
- await deleteFunction(this.controller!.appwriteServer!, func.$id);
788
- console.log(
789
- chalk.green(`✨ Function ${func.name} deleted successfully!`)
790
- );
791
- } catch (error) {
792
- console.error(
793
- chalk.red(`Failed to delete function ${func.name}:`),
794
- error
795
- );
796
- }
797
- }
798
- }
799
617
 
800
618
  private async selectFunctions(
801
619
  message: string,
@@ -813,7 +631,7 @@ export class InteractiveCLI {
813
631
  const allFunctions = [
814
632
  ...localFunctions,
815
633
  ...remoteFunctions.functions.filter(
816
- (rf) => !localFunctions.some((lf) => lf.name === rf.name)
634
+ (rf: any) => !localFunctions.some((lf) => lf.name === rf.name)
817
635
  ),
818
636
  ];
819
637
 
@@ -893,21 +711,6 @@ export class InteractiveCLI {
893
711
  return selectedBuckets;
894
712
  }
895
713
 
896
- private async createCollectionConfig(): Promise<void> {
897
- const { collectionName } = await inquirer.prompt([
898
- {
899
- type: "input",
900
- name: "collectionName",
901
- message: chalk.blue("Enter the name of the collection:"),
902
- validate: (input) =>
903
- input.trim() !== "" || "Collection name cannot be empty.",
904
- },
905
- ]);
906
- console.log(
907
- chalk.green(`Creating collection config file for '${collectionName}'...`)
908
- );
909
- createEmptyCollection(collectionName);
910
- }
911
714
 
912
715
  private async configureBuckets(
913
716
  config: AppwriteConfig,
@@ -1146,873 +949,79 @@ export class InteractiveCLI {
1146
949
  );
1147
950
  }
1148
951
 
1149
- private async syncDb(): Promise<void> {
1150
- console.log(chalk.blue("Pushing local configuration to Appwrite..."));
1151
952
 
1152
- const databases = await this.selectDatabases(
1153
- this.getLocalDatabases(),
1154
- chalk.blue("Select local databases to push:"),
1155
- true
1156
- );
1157
953
 
1158
- if (!databases.length) {
1159
- console.log(
1160
- chalk.yellow("No databases selected. Skipping database sync.")
1161
- );
1162
- return;
1163
- }
954
+ private getLocalCollections(): (Models.Collection & {
955
+ _isFromTablesDir?: boolean;
956
+ _sourceFolder?: string;
957
+ databaseId?: string;
958
+ })[] {
959
+ const configCollections = this.controller!.config?.collections || [];
960
+ // @ts-expect-error - appwrite invalid types
961
+ return configCollections.map((c) => ({
962
+ $id: c.$id || ulid(),
963
+ $createdAt: DateTime.now().toISO(),
964
+ $updatedAt: DateTime.now().toISO(),
965
+ name: c.name,
966
+ enabled: c.enabled || true,
967
+ documentSecurity: c.documentSecurity || false,
968
+ attributes: c.attributes || [],
969
+ indexes: c.indexes || [],
970
+ $permissions: PermissionToAppwritePermission(c.$permissions) || [],
971
+ databaseId: c.databaseId,
972
+ _isFromTablesDir: (c as any)._isFromTablesDir || false,
973
+ _sourceFolder: (c as any)._isFromTablesDir ? 'tables' : 'collections',
974
+ }));
975
+ }
1164
976
 
1165
- const collections = await this.selectCollections(
1166
- databases[0],
1167
- this.controller!.database!,
1168
- chalk.blue("Select local collections to push:"),
1169
- true,
1170
- true, // prefer local
1171
- true // filter by selected database
1172
- );
977
+ private getLocalDatabases(): Models.Database[] {
978
+ const configDatabases = this.controller!.config?.databases || [];
979
+ return configDatabases.map((db) => ({
980
+ $id: db.$id || ulid(),
981
+ $createdAt: DateTime.now().toISO(),
982
+ $updatedAt: DateTime.now().toISO(),
983
+ name: db.name,
984
+ enabled: true,
985
+ }));
986
+ }
1173
987
 
1174
- const { syncFunctions } = await inquirer.prompt([
1175
- {
1176
- type: "confirm",
1177
- name: "syncFunctions",
1178
- message: "Do you want to push local functions to remote?",
1179
- default: false,
1180
- },
1181
- ]);
1182
988
 
1183
- try {
1184
- // First sync databases and collections
1185
- await this.controller!.syncDb(databases, collections);
1186
- console.log(chalk.green("Database and collections pushed successfully"));
1187
-
1188
- // Then handle functions if requested
1189
- if (syncFunctions && this.controller!.config?.functions?.length) {
1190
- const functions = await this.selectFunctions(
1191
- chalk.blue("Select local functions to push:"),
1192
- true,
1193
- true // prefer local
1194
- );
989
+ /**
990
+ * Extract session information from current controller for preservation
991
+ */
992
+ private extractSessionFromController(): {
993
+ appwriteEndpoint: string;
994
+ appwriteProject: string;
995
+ appwriteKey?: string;
996
+ sessionCookie?: string;
997
+ sessionMetadata?: any;
998
+ } | undefined {
999
+ if (!this.controller?.config) {
1000
+ return undefined;
1001
+ }
1195
1002
 
1196
- for (const func of functions) {
1197
- try {
1198
- await this.controller!.deployFunction(func.name);
1199
- console.log(
1200
- chalk.green(`Function ${func.name} deployed successfully`)
1201
- );
1202
- } catch (error) {
1203
- console.error(
1204
- chalk.red(`Failed to deploy function ${func.name}:`),
1205
- error
1206
- );
1207
- }
1208
- }
1209
- }
1003
+ const sessionInfo = this.controller.getSessionInfo();
1004
+ const config = this.controller.config;
1210
1005
 
1211
- console.log(
1212
- chalk.green("Local configuration push completed successfully!")
1213
- );
1214
- } catch (error) {
1215
- console.error(chalk.red("Failed to push local configuration:"), error);
1216
- throw error;
1006
+ if (!config.appwriteEndpoint || !config.appwriteProject) {
1007
+ return undefined;
1217
1008
  }
1218
- }
1219
-
1220
- private async synchronizeConfigurations(): Promise<void> {
1221
- console.log(chalk.blue("Synchronizing configurations..."));
1222
- await this.controller!.init();
1223
-
1224
- // Sync databases, collections, and buckets
1225
- const { syncDatabases } = await inquirer.prompt([
1226
- {
1227
- type: "confirm",
1228
- name: "syncDatabases",
1229
- message: "Do you want to synchronize databases, collections, and their buckets?",
1230
- default: true,
1231
- },
1232
- ]);
1233
1009
 
1234
- if (syncDatabases) {
1235
- const remoteDatabases = await fetchAllDatabases(
1236
- this.controller!.database!
1237
- );
1238
-
1239
- // Use the controller's synchronizeConfigurations method which handles collections properly
1240
- console.log(chalk.blue("Pulling collections and generating collection files..."));
1241
- await this.controller!.synchronizeConfigurations(remoteDatabases);
1242
-
1243
- // Also configure buckets for any new databases
1244
- const localDatabases = this.controller!.config?.databases || [];
1245
- const updatedConfig = await this.configureBuckets({
1246
- ...this.controller!.config!,
1247
- databases: [
1248
- ...localDatabases,
1249
- ...remoteDatabases.filter(
1250
- (rd) => !localDatabases.some((ld) => ld.name === rd.name)
1251
- ),
1252
- ],
1253
- });
1010
+ const result: any = {
1011
+ appwriteEndpoint: config.appwriteEndpoint,
1012
+ appwriteProject: config.appwriteProject,
1013
+ appwriteKey: config.appwriteKey
1014
+ };
1254
1015
 
1255
- this.controller!.config = updatedConfig;
1016
+ // Add session data if available
1017
+ if (sessionInfo.hasSession) {
1018
+ result.sessionCookie = (this.controller as any).sessionCookie;
1019
+ result.sessionMetadata = (this.controller as any).sessionMetadata;
1256
1020
  }
1257
1021
 
1258
- // Then sync functions
1259
- const { syncFunctions } = await inquirer.prompt([
1260
- {
1261
- type: "confirm",
1262
- name: "syncFunctions",
1263
- message: "Do you want to synchronize functions?",
1264
- default: true,
1265
- },
1266
- ]);
1267
-
1268
- if (syncFunctions) {
1269
- const remoteFunctions = await this.controller!.listAllFunctions();
1270
- const localFunctions = this.controller!.config?.functions || [];
1271
-
1272
- const allFunctions = [
1273
- ...remoteFunctions,
1274
- ...localFunctions.filter(
1275
- (f) => !remoteFunctions.some((rf) => rf.$id === f.$id)
1276
- ),
1277
- ];
1278
-
1279
- for (const func of allFunctions) {
1280
- const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
1281
- const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
1282
-
1283
- if (hasLocal && hasRemote) {
1284
- // First try to find the function locally
1285
- let functionPath = join(
1286
- this.controller!.getAppwriteFolderPath()!,
1287
- "functions",
1288
- func.name
1289
- );
1290
-
1291
- if (!fs.existsSync(functionPath)) {
1292
- console.log(
1293
- chalk.yellow(
1294
- `Function not found in primary location, searching subdirectories...`
1295
- )
1296
- );
1297
- const foundPath = await this.findFunctionInSubdirectories(
1298
- [this.controller!.getAppwriteFolderPath()!, process.cwd()],
1299
- func.name
1300
- );
1301
-
1302
- if (foundPath) {
1303
- console.log(chalk.green(`Found function at: ${foundPath}`));
1304
- functionPath = foundPath;
1305
- }
1306
- }
1307
-
1308
- const { preference } = await inquirer.prompt([
1309
- {
1310
- type: "list",
1311
- name: "preference",
1312
- message: `Function "${func.name}" ${
1313
- functionPath ? "found at " + functionPath : "not found locally"
1314
- }. What would you like to do?`,
1315
- choices: [
1316
- ...(functionPath
1317
- ? [
1318
- {
1319
- name: "Keep local version (deploy to remote)",
1320
- value: "local",
1321
- },
1322
- ]
1323
- : []),
1324
- { name: "Use remote version (download)", value: "remote" },
1325
- { name: "Update config only", value: "config" },
1326
- { name: "Skip this function", value: "skip" },
1327
- ],
1328
- },
1329
- ]);
1330
-
1331
- if (preference === "local" && functionPath) {
1332
- await this.controller!.deployFunction(func.name);
1333
- } else if (preference === "remote") {
1334
- await downloadLatestFunctionDeployment(
1335
- this.controller!.appwriteServer!,
1336
- func.$id,
1337
- join(this.controller!.getAppwriteFolderPath()!, "functions")
1338
- );
1339
- } else if (preference === "config") {
1340
- const remoteFunction = await getFunction(
1341
- this.controller!.appwriteServer!,
1342
- func.$id
1343
- );
1344
-
1345
- const newFunction = {
1346
- $id: remoteFunction.$id,
1347
- name: remoteFunction.name,
1348
- runtime: remoteFunction.runtime as Runtime,
1349
- execute: remoteFunction.execute || [],
1350
- events: remoteFunction.events || [],
1351
- schedule: remoteFunction.schedule || "",
1352
- timeout: remoteFunction.timeout || 15,
1353
- enabled: remoteFunction.enabled !== false,
1354
- logging: remoteFunction.logging !== false,
1355
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
1356
- commands: remoteFunction.commands || "npm install",
1357
- scopes: (remoteFunction.scopes || []) as FunctionScope[],
1358
- installationId: remoteFunction.installationId,
1359
- providerRepositoryId: remoteFunction.providerRepositoryId,
1360
- providerBranch: remoteFunction.providerBranch,
1361
- providerSilentMode: remoteFunction.providerSilentMode,
1362
- providerRootDirectory: remoteFunction.providerRootDirectory,
1363
- specification: remoteFunction.specification as Specification,
1364
- };
1365
-
1366
- const existingIndex = this.controller!.config!.functions!.findIndex(
1367
- (f) => f.$id === remoteFunction.$id
1368
- );
1369
-
1370
- if (existingIndex >= 0) {
1371
- this.controller!.config!.functions![existingIndex] = newFunction;
1372
- } else {
1373
- this.controller!.config!.functions!.push(newFunction);
1374
- }
1375
- console.log(
1376
- chalk.green(`Updated config for function: ${func.name}`)
1377
- );
1378
- }
1379
- } else if (hasLocal) {
1380
- // Similar check for local-only functions
1381
- let functionPath = join(
1382
- this.controller!.getAppwriteFolderPath()!,
1383
- "functions",
1384
- func.name
1385
- );
1386
-
1387
- if (!fs.existsSync(functionPath)) {
1388
- const foundPath = await this.findFunctionInSubdirectories(
1389
- [this.controller!.getAppwriteFolderPath()!, process.cwd()],
1390
- func.name
1391
- );
1392
-
1393
- if (foundPath) {
1394
- functionPath = foundPath;
1395
- }
1396
- }
1397
-
1398
- const { action } = await inquirer.prompt([
1399
- {
1400
- type: "list",
1401
- name: "action",
1402
- message: `Function "${func.name}" ${
1403
- functionPath ? "found at " + functionPath : "not found locally"
1404
- }. What would you like to do?`,
1405
- choices: [
1406
- ...(functionPath
1407
- ? [
1408
- {
1409
- name: "Deploy to remote",
1410
- value: "deploy",
1411
- },
1412
- ]
1413
- : []),
1414
- { name: "Skip this function", value: "skip" },
1415
- ],
1416
- },
1417
- ]);
1418
-
1419
- if (action === "deploy" && functionPath) {
1420
- await this.controller!.deployFunction(func.name);
1421
- }
1422
- } else if (hasRemote) {
1423
- const { action } = await inquirer.prompt([
1424
- {
1425
- type: "list",
1426
- name: "action",
1427
- message: `Function "${func.name}" exists only remotely. What would you like to do?`,
1428
- choices: [
1429
- { name: "Update config only", value: "config" },
1430
- { name: "Download locally", value: "download" },
1431
- { name: "Skip this function", value: "skip" },
1432
- ],
1433
- },
1434
- ]);
1435
-
1436
- if (action === "download") {
1437
- await downloadLatestFunctionDeployment(
1438
- this.controller!.appwriteServer!,
1439
- func.$id,
1440
- join(this.controller!.getAppwriteFolderPath()!, "functions")
1441
- );
1442
- } else if (action === "config") {
1443
- const remoteFunction = await getFunction(
1444
- this.controller!.appwriteServer!,
1445
- func.$id
1446
- );
1447
-
1448
- const newFunction = {
1449
- $id: remoteFunction.$id,
1450
- name: remoteFunction.name,
1451
- runtime: remoteFunction.runtime as Runtime,
1452
- execute: remoteFunction.execute || [],
1453
- events: remoteFunction.events || [],
1454
- schedule: remoteFunction.schedule || "",
1455
- timeout: remoteFunction.timeout || 15,
1456
- enabled: remoteFunction.enabled !== false,
1457
- logging: remoteFunction.logging !== false,
1458
- entrypoint: remoteFunction.entrypoint || "src/index.ts",
1459
- commands: remoteFunction.commands || "npm install",
1460
- scopes: (remoteFunction.scopes || []) as FunctionScope[],
1461
- installationId: remoteFunction.installationId,
1462
- providerRepositoryId: remoteFunction.providerRepositoryId,
1463
- providerBranch: remoteFunction.providerBranch,
1464
- providerSilentMode: remoteFunction.providerSilentMode,
1465
- providerRootDirectory: remoteFunction.providerRootDirectory,
1466
- specification: remoteFunction.specification as Specification,
1467
- };
1468
-
1469
- this.controller!.config!.functions =
1470
- this.controller!.config!.functions || [];
1471
- this.controller!.config!.functions.push(newFunction);
1472
- console.log(
1473
- chalk.green(`Added config for remote function: ${func.name}`)
1474
- );
1475
- }
1476
- }
1477
- }
1478
-
1479
- // Schema generation and collection file writing is handled by controller.synchronizeConfigurations()
1480
- }
1481
-
1482
- console.log(chalk.green("✨ Configurations synchronized successfully!"));
1483
- }
1484
-
1485
- private async backupDatabase(): Promise<void> {
1486
- if (!this.controller!.database) {
1487
- throw new Error(
1488
- "Database is not initialized, is the config file correct & created?"
1489
- );
1490
- }
1491
- const databases = await fetchAllDatabases(this.controller!.database);
1492
-
1493
- const selectedDatabases = await this.selectDatabases(
1494
- databases,
1495
- "Select databases to backup:"
1496
- );
1497
-
1498
- for (const db of selectedDatabases) {
1499
- console.log(chalk.yellow(`Backing up database: ${db.name}`));
1500
- await this.controller!.backupDatabase(db);
1501
- }
1502
- MessageFormatter.success("Database backup completed", { prefix: "Backup" });
1503
- }
1504
-
1505
- private async wipeDatabase(): Promise<void> {
1506
- if (!this.controller!.database || !this.controller!.storage) {
1507
- throw new Error(
1508
- "Database or Storage is not initialized, is the config file correct & created?"
1509
- );
1510
- }
1511
- const databases = await fetchAllDatabases(this.controller!.database);
1512
- const storage = await listBuckets(this.controller!.storage);
1513
-
1514
- const selectedDatabases = await this.selectDatabases(
1515
- databases,
1516
- "Select databases to wipe:"
1517
- );
1518
-
1519
- const { selectedStorage } = await inquirer.prompt([
1520
- {
1521
- type: "checkbox",
1522
- name: "selectedStorage",
1523
- message: "Select storage buckets to wipe:",
1524
- choices: storage.buckets.map((s) => ({ name: s.name, value: s.$id })),
1525
- },
1526
- ]);
1527
-
1528
- const { wipeUsers } = await inquirer.prompt([
1529
- {
1530
- type: "confirm",
1531
- name: "wipeUsers",
1532
- message: "Do you want to wipe users as well?",
1533
- default: false,
1534
- },
1535
- ]);
1536
-
1537
- const databaseNames = selectedDatabases.map(db => db.name);
1538
- const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
1539
- includeStorage: selectedStorage.length > 0,
1540
- includeUsers: wipeUsers
1541
- });
1542
-
1543
- if (confirmed) {
1544
- MessageFormatter.info("Starting wipe operation...", { prefix: "Wipe" });
1545
- for (const db of selectedDatabases) {
1546
- await this.controller!.wipeDatabase(db);
1547
- }
1548
- for (const bucketId of selectedStorage) {
1549
- await this.controller!.wipeDocumentStorage(bucketId);
1550
- }
1551
- if (wipeUsers) {
1552
- await this.controller!.wipeUsers();
1553
- }
1554
- MessageFormatter.success("Wipe operation completed", { prefix: "Wipe" });
1555
- } else {
1556
- MessageFormatter.info("Wipe operation cancelled", { prefix: "Wipe" });
1557
- }
1558
- }
1559
-
1560
- private async wipeCollections(): Promise<void> {
1561
- if (!this.controller!.database) {
1562
- throw new Error(
1563
- "Database is not initialized, is the config file correct & created?"
1564
- );
1565
- }
1566
- const databases = await fetchAllDatabases(this.controller!.database);
1567
- const selectedDatabases = await this.selectDatabases(
1568
- databases,
1569
- "Select the database(s) containing the collections to wipe:",
1570
- true
1571
- );
1572
-
1573
- for (const database of selectedDatabases) {
1574
- const collections = await this.selectCollections(
1575
- database,
1576
- this.controller!.database,
1577
- `Select collections to wipe from ${database.name}:`,
1578
- true,
1579
- undefined,
1580
- true
1581
- );
1582
-
1583
- const collectionNames = collections.map(c => c.name);
1584
- const confirmed = await ConfirmationDialogs.confirmCollectionWipe(
1585
- database.name,
1586
- collectionNames
1587
- );
1588
-
1589
- if (confirmed) {
1590
- MessageFormatter.info(
1591
- `Wiping selected collections from ${database.name}...`,
1592
- { prefix: "Wipe" }
1593
- );
1594
- for (const collection of collections) {
1595
- await this.controller!.wipeCollection(database, collection);
1596
- MessageFormatter.success(
1597
- `Collection ${collection.name} wiped successfully`,
1598
- { prefix: "Wipe" }
1599
- );
1600
- }
1601
- } else {
1602
- MessageFormatter.info(
1603
- `Wipe operation cancelled for ${database.name}`,
1604
- { prefix: "Wipe" }
1605
- );
1606
- }
1607
- }
1608
- MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
1609
- }
1610
-
1611
- private async generateSchemas(): Promise<void> {
1612
- console.log(chalk.yellow("Generating schemas..."));
1613
-
1614
- // Prompt user for schema type preference
1615
- const { schemaType } = await inquirer.prompt([
1616
- {
1617
- type: "list",
1618
- name: "schemaType",
1619
- message: "What type of schemas would you like to generate?",
1620
- choices: [
1621
- { name: "TypeScript (Zod) schemas", value: "zod" },
1622
- { name: "JSON schemas", value: "json" },
1623
- { name: "Both TypeScript and JSON schemas", value: "both" },
1624
- ],
1625
- default: "both",
1626
- },
1627
- ]);
1628
-
1629
- // Get the config folder path (where the config file is located)
1630
- const configFolderPath = this.controller!.getAppwriteFolderPath();
1631
- if (!configFolderPath) {
1632
- MessageFormatter.error("Failed to get config folder path", undefined, { prefix: "Schemas" });
1633
- return;
1634
- }
1635
-
1636
- // Create SchemaGenerator with the correct base path and generate schemas
1637
- const schemaGenerator = new SchemaGenerator(this.controller!.config!, configFolderPath);
1638
- schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
1639
-
1640
- MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
1641
- }
1642
-
1643
- private async generateConstants(): Promise<void> {
1644
- console.log(chalk.yellow("Generating cross-language constants..."));
1645
-
1646
- if (!this.controller?.config) {
1647
- MessageFormatter.error("No configuration found", undefined, { prefix: "Constants" });
1648
- return;
1649
- }
1650
-
1651
- // Prompt for languages
1652
- const { languages } = await inquirer.prompt([
1653
- {
1654
- type: "checkbox",
1655
- name: "languages",
1656
- message: "Select languages for constants generation:",
1657
- choices: [
1658
- { name: "TypeScript", value: "typescript", checked: true },
1659
- { name: "JavaScript", value: "javascript" },
1660
- { name: "Python", value: "python" },
1661
- { name: "PHP", value: "php" },
1662
- { name: "Dart", value: "dart" },
1663
- { name: "JSON", value: "json" },
1664
- { name: "Environment Variables", value: "env" },
1665
- ],
1666
- validate: (input) => {
1667
- if (input.length === 0) {
1668
- return "Please select at least one language";
1669
- }
1670
- return true;
1671
- },
1672
- },
1673
- ]);
1674
-
1675
- // Determine default output directory based on config location
1676
- const configPath = this.controller!.getAppwriteFolderPath();
1677
- const defaultOutputDir = configPath
1678
- ? path.join(configPath, "constants")
1679
- : path.join(process.cwd(), "constants");
1680
-
1681
- // Prompt for output directory
1682
- const { outputDir } = await inquirer.prompt([
1683
- {
1684
- type: "input",
1685
- name: "outputDir",
1686
- message: "Output directory for constants files:",
1687
- default: defaultOutputDir,
1688
- validate: (input) => {
1689
- if (!input.trim()) {
1690
- return "Output directory cannot be empty";
1691
- }
1692
- return true;
1693
- },
1694
- },
1695
- ]);
1696
-
1697
- try {
1698
- const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
1699
- const generator = new ConstantsGenerator(this.controller.config);
1700
-
1701
- MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
1702
- await generator.generateFiles(languages, outputDir);
1703
-
1704
- MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
1705
- } catch (error) {
1706
- MessageFormatter.error("Failed to generate constants", error instanceof Error ? error : new Error(String(error)), { prefix: "Constants" });
1707
- }
1708
- }
1709
-
1710
- private async importData(): Promise<void> {
1711
- console.log(chalk.yellow("Importing data..."));
1712
-
1713
- const { doBackup } = await inquirer.prompt([
1714
- {
1715
- type: "confirm",
1716
- name: "doBackup",
1717
- message: "Do you want to perform a backup before importing?",
1718
- default: true,
1719
- },
1720
- ]);
1721
-
1722
- const databases = await this.selectDatabases(
1723
- await fetchAllDatabases(this.controller!.database!),
1724
- "Select databases to import data into:",
1725
- true
1726
- );
1727
-
1728
- const collections = await this.selectCollections(
1729
- databases[0],
1730
- this.controller!.database!,
1731
- "Select collections to import data into (leave empty for all):",
1732
- true
1733
- );
1734
-
1735
- const { shouldWriteFile } = await inquirer.prompt([
1736
- {
1737
- type: "confirm",
1738
- name: "shouldWriteFile",
1739
- message: "Do you want to write the imported data to a file?",
1740
- default: false,
1741
- },
1742
- ]);
1743
-
1744
- const options = {
1745
- databases,
1746
- collections: collections.map((c) => c.name),
1747
- doBackup,
1748
- importData: true,
1749
- shouldWriteFile,
1750
- };
1751
-
1752
- try {
1753
- await this.controller!.importData(options);
1754
- console.log(chalk.green("Data import completed successfully."));
1755
- } catch (error) {
1756
- console.error(chalk.red("Error importing data:"), error);
1757
- }
1758
- }
1759
-
1760
- private async transferData(): Promise<void> {
1761
- if (!this.controller!.database) {
1762
- throw new Error(
1763
- "Database is not initialized, is the config file correct & created?"
1764
- );
1765
- }
1766
-
1767
- const { isRemote } = await inquirer.prompt([
1768
- {
1769
- type: "confirm",
1770
- name: "isRemote",
1771
- message: "Is this a remote transfer?",
1772
- default: false,
1773
- },
1774
- ]);
1775
-
1776
- let sourceClient = this.controller!.database;
1777
- let targetClient: Databases;
1778
- let sourceDatabases: Models.Database[];
1779
- let targetDatabases: Models.Database[];
1780
- let remoteOptions:
1781
- | {
1782
- transferEndpoint: string;
1783
- transferProject: string;
1784
- transferKey: string;
1785
- }
1786
- | undefined;
1787
-
1788
- if (isRemote) {
1789
- remoteOptions = await inquirer.prompt([
1790
- {
1791
- type: "input",
1792
- name: "transferEndpoint",
1793
- message: "Enter the remote endpoint:",
1794
- },
1795
- {
1796
- type: "input",
1797
- name: "transferProject",
1798
- message: "Enter the remote project ID:",
1799
- },
1800
- {
1801
- type: "input",
1802
- name: "transferKey",
1803
- message: "Enter the remote API key:",
1804
- },
1805
- ]);
1806
-
1807
- const remoteClient = getClient(
1808
- remoteOptions!.transferEndpoint,
1809
- remoteOptions!.transferProject,
1810
- remoteOptions!.transferKey
1811
- );
1812
- targetClient = new Databases(remoteClient);
1813
-
1814
- sourceDatabases = await fetchAllDatabases(sourceClient);
1815
- targetDatabases = await fetchAllDatabases(targetClient);
1816
- } else {
1817
- targetClient = sourceClient;
1818
- const allDatabases = await fetchAllDatabases(sourceClient);
1819
- sourceDatabases = targetDatabases = allDatabases;
1820
- }
1821
-
1822
- const fromDbs = await this.selectDatabases(
1823
- sourceDatabases,
1824
- "Select the source database:",
1825
- false
1826
- );
1827
- const fromDb = fromDbs[0];
1828
- if (!fromDb) {
1829
- throw new Error("No source database selected");
1830
- }
1831
- const availableDbs = targetDatabases.filter((db) => db.$id !== fromDb.$id);
1832
- const targetDbs = await this.selectDatabases(
1833
- availableDbs,
1834
- "Select the target database:",
1835
- false
1836
- );
1837
- const targetDb = targetDbs[0];
1838
- if (!targetDb) {
1839
- throw new Error("No target database selected");
1840
- }
1841
-
1842
- const selectedCollections = await this.selectCollections(
1843
- fromDb,
1844
- sourceClient,
1845
- "Select collections to transfer:",
1846
- true,
1847
- false // don't prefer local for transfers
1848
- );
1849
-
1850
- const { transferStorage } = await inquirer.prompt([
1851
- {
1852
- type: "confirm",
1853
- name: "transferStorage",
1854
- message: "Do you want to transfer storage as well?",
1855
- default: false,
1856
- },
1857
- ]);
1858
-
1859
- let sourceBucket, targetBucket;
1860
-
1861
- if (transferStorage) {
1862
- const sourceStorage = new Storage(this.controller!.appwriteServer!);
1863
- const targetStorage = isRemote
1864
- ? new Storage(
1865
- getClient(
1866
- remoteOptions!.transferEndpoint,
1867
- remoteOptions!.transferProject,
1868
- remoteOptions!.transferKey
1869
- )
1870
- )
1871
- : sourceStorage;
1872
-
1873
- const sourceBuckets = await listBuckets(sourceStorage);
1874
- const targetBuckets = isRemote
1875
- ? await listBuckets(targetStorage)
1876
- : sourceBuckets;
1877
-
1878
- const sourceBucketPicked = await this.selectBuckets(
1879
- sourceBuckets.buckets,
1880
- "Select the source bucket:",
1881
- false
1882
- );
1883
- const targetBucketPicked = await this.selectBuckets(
1884
- targetBuckets.buckets,
1885
- "Select the target bucket:",
1886
- false
1887
- );
1888
- sourceBucket = sourceBucketPicked[0];
1889
- targetBucket = targetBucketPicked[0];
1890
- }
1891
-
1892
- let transferOptions: TransferOptions = {
1893
- fromDb,
1894
- targetDb,
1895
- isRemote,
1896
- collections:
1897
- selectedCollections.length > 0
1898
- ? selectedCollections.map((c) => c.$id)
1899
- : undefined,
1900
- sourceBucket,
1901
- targetBucket,
1902
- };
1903
-
1904
- if (isRemote && remoteOptions) {
1905
- transferOptions = {
1906
- ...transferOptions,
1907
- ...remoteOptions,
1908
- };
1909
- }
1910
-
1911
- console.log(chalk.yellow("Transferring data..."));
1912
- await this.controller!.transferData(transferOptions);
1913
- console.log(chalk.green("Data transfer completed."));
1022
+ return result;
1914
1023
  }
1915
1024
 
1916
- private getLocalCollections(): Models.Collection[] {
1917
- const configCollections = this.controller!.config?.collections || [];
1918
- // @ts-expect-error - appwrite invalid types
1919
- return configCollections.map((c) => ({
1920
- $id: c.$id || ulid(),
1921
- $createdAt: DateTime.now().toISO(),
1922
- $updatedAt: DateTime.now().toISO(),
1923
- name: c.name,
1924
- enabled: c.enabled || true,
1925
- documentSecurity: c.documentSecurity || false,
1926
- attributes: c.attributes || [],
1927
- indexes: c.indexes || [],
1928
- $permissions: PermissionToAppwritePermission(c.$permissions) || [],
1929
- databaseId: c.databaseId!,
1930
- }));
1931
- }
1932
-
1933
- private getLocalDatabases(): Models.Database[] {
1934
- const configDatabases = this.controller!.config?.databases || [];
1935
- return configDatabases.map((db) => ({
1936
- $id: db.$id || ulid(),
1937
- $createdAt: DateTime.now().toISO(),
1938
- $updatedAt: DateTime.now().toISO(),
1939
- name: db.name,
1940
- enabled: true,
1941
- }));
1942
- }
1943
-
1944
- private async reloadConfig(): Promise<void> {
1945
- MessageFormatter.progress("Reloading configuration files...", { prefix: "Config" });
1946
- try {
1947
- await this.controller!.reloadConfig();
1948
- MessageFormatter.success("Configuration files reloaded successfully", { prefix: "Config" });
1949
- } catch (error) {
1950
- MessageFormatter.error("Failed to reload configuration files", error instanceof Error ? error : new Error(String(error)), { prefix: "Config" });
1951
- }
1952
- }
1953
-
1954
- private async updateFunctionSpec(): Promise<void> {
1955
- const remoteFunctions = await listFunctions(
1956
- this.controller!.appwriteServer!,
1957
- [Query.limit(1000)]
1958
- );
1959
- const localFunctions = this.getLocalFunctions();
1960
-
1961
- const allFunctions = [
1962
- ...remoteFunctions.functions,
1963
- ...localFunctions.filter(
1964
- (f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)
1965
- ),
1966
- ];
1967
-
1968
- const functionsToUpdate = await inquirer.prompt([
1969
- {
1970
- type: "checkbox",
1971
- name: "functionId",
1972
- message: "Select functions to update:",
1973
- choices: allFunctions.map((f) => ({
1974
- name: `${f.name} (${f.$id})${
1975
- localFunctions.some((lf) => lf.name === f.name)
1976
- ? " (Local)"
1977
- : " (Remote)"
1978
- }`,
1979
- value: f.$id,
1980
- })),
1981
- loop: true,
1982
- },
1983
- ]);
1984
-
1985
- const specifications = await listSpecifications(
1986
- this.controller!.appwriteServer!
1987
- );
1988
- const { specification } = await inquirer.prompt([
1989
- {
1990
- type: "list",
1991
- name: "specification",
1992
- message: "Select new specification:",
1993
- choices: specifications.specifications.map((s) => ({
1994
- name: `${s.slug}`,
1995
- value: s.slug,
1996
- })),
1997
- },
1998
- ]);
1999
-
2000
- try {
2001
- for (const functionId of functionsToUpdate.functionId) {
2002
- await this.controller!.updateFunctionSpecifications(
2003
- functionId,
2004
- specification
2005
- );
2006
- console.log(
2007
- chalk.green(
2008
- `Successfully updated function specification to ${specification}`
2009
- )
2010
- );
2011
- }
2012
- } catch (error) {
2013
- console.error(chalk.red("Error updating function specification:"), error);
2014
- }
2015
- }
2016
1025
 
2017
1026
  private async detectConfigurationType(): Promise<void> {
2018
1027
  try {
@@ -2060,302 +1069,4 @@ export class InteractiveCLI {
2060
1069
  }
2061
1070
  }
2062
1071
 
2063
- private async migrateTypeScriptConfig(): Promise<void> {
2064
- try {
2065
- MessageFormatter.info("Starting TypeScript to YAML configuration migration...", { prefix: "Migration" });
2066
-
2067
- // Perform the migration
2068
- await migrateConfig(this.currentDir);
2069
-
2070
- // Reset the detection flag
2071
- this.isUsingTypeScriptConfig = false;
2072
-
2073
- // Reset the controller to pick up the new config
2074
- this.controller = undefined;
2075
-
2076
- MessageFormatter.success("Migration completed successfully!", { prefix: "Migration" });
2077
- MessageFormatter.info("Your configuration has been migrated to the .appwrite directory structure", { prefix: "Migration" });
2078
- MessageFormatter.info("You can now use YAML configuration for easier management", { prefix: "Migration" });
2079
-
2080
- } catch (error) {
2081
- MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
2082
- }
2083
- }
2084
-
2085
- private async comprehensiveTransfer(): Promise<void> {
2086
- MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
2087
-
2088
- try {
2089
- // Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
2090
- await this.initControllerIfNeeded();
2091
-
2092
- // Check if user has an appwrite config for easier setup
2093
- const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
2094
- this.controller?.config?.appwriteProject &&
2095
- this.controller?.config?.appwriteKey;
2096
-
2097
- let sourceConfig: any;
2098
- let targetConfig: any;
2099
-
2100
- if (hasAppwriteConfig) {
2101
- // Offer to use existing config for source
2102
- const { useConfigForSource } = await inquirer.prompt([
2103
- {
2104
- type: "confirm",
2105
- name: "useConfigForSource",
2106
- message: "Use your current appwriteConfig as the source?",
2107
- default: true,
2108
- },
2109
- ]);
2110
-
2111
- if (useConfigForSource) {
2112
- sourceConfig = {
2113
- sourceEndpoint: this.controller!.config!.appwriteEndpoint,
2114
- sourceProject: this.controller!.config!.appwriteProject,
2115
- sourceKey: this.controller!.config!.appwriteKey,
2116
- };
2117
- MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
2118
- } else {
2119
- // Get source configuration manually
2120
- sourceConfig = await inquirer.prompt([
2121
- {
2122
- type: "input",
2123
- name: "sourceEndpoint",
2124
- message: "Enter the source Appwrite endpoint:",
2125
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2126
- },
2127
- {
2128
- type: "input",
2129
- name: "sourceProject",
2130
- message: "Enter the source project ID:",
2131
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2132
- },
2133
- {
2134
- type: "password",
2135
- name: "sourceKey",
2136
- message: "Enter the source API key:",
2137
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2138
- },
2139
- ]);
2140
- }
2141
-
2142
- // Offer to use existing config for target
2143
- const { useConfigForTarget } = await inquirer.prompt([
2144
- {
2145
- type: "confirm",
2146
- name: "useConfigForTarget",
2147
- message: "Use your current appwriteConfig as the target?",
2148
- default: false,
2149
- },
2150
- ]);
2151
-
2152
- if (useConfigForTarget) {
2153
- targetConfig = {
2154
- targetEndpoint: this.controller!.config!.appwriteEndpoint,
2155
- targetProject: this.controller!.config!.appwriteProject,
2156
- targetKey: this.controller!.config!.appwriteKey,
2157
- };
2158
- MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
2159
- } else {
2160
- // Get target configuration manually
2161
- targetConfig = await inquirer.prompt([
2162
- {
2163
- type: "input",
2164
- name: "targetEndpoint",
2165
- message: "Enter the target Appwrite endpoint:",
2166
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2167
- },
2168
- {
2169
- type: "input",
2170
- name: "targetProject",
2171
- message: "Enter the target project ID:",
2172
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2173
- },
2174
- {
2175
- type: "password",
2176
- name: "targetKey",
2177
- message: "Enter the target API key:",
2178
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2179
- },
2180
- ]);
2181
- }
2182
- } else {
2183
- // No appwrite config found, get both configurations manually
2184
- MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
2185
-
2186
- // Get source configuration
2187
- sourceConfig = await inquirer.prompt([
2188
- {
2189
- type: "input",
2190
- name: "sourceEndpoint",
2191
- message: "Enter the source Appwrite endpoint:",
2192
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2193
- },
2194
- {
2195
- type: "input",
2196
- name: "sourceProject",
2197
- message: "Enter the source project ID:",
2198
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2199
- },
2200
- {
2201
- type: "password",
2202
- name: "sourceKey",
2203
- message: "Enter the source API key:",
2204
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2205
- },
2206
- ]);
2207
-
2208
- // Get target configuration
2209
- targetConfig = await inquirer.prompt([
2210
- {
2211
- type: "input",
2212
- name: "targetEndpoint",
2213
- message: "Enter the target Appwrite endpoint:",
2214
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2215
- },
2216
- {
2217
- type: "input",
2218
- name: "targetProject",
2219
- message: "Enter the target project ID:",
2220
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2221
- },
2222
- {
2223
- type: "password",
2224
- name: "targetKey",
2225
- message: "Enter the target API key:",
2226
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2227
- },
2228
- ]);
2229
- }
2230
-
2231
- // Get transfer options
2232
- const transferOptions = await inquirer.prompt([
2233
- {
2234
- type: "checkbox",
2235
- name: "transferTypes",
2236
- message: "Select what to transfer:",
2237
- choices: [
2238
- { name: "👥 Users", value: "users", checked: true },
2239
- { name: "👥 Teams", value: "teams", checked: true },
2240
- { name: "🗄️ Databases", value: "databases", checked: true },
2241
- { name: "📦 Storage Buckets", value: "buckets", checked: true },
2242
- { name: "⚡ Functions", value: "functions", checked: true },
2243
- ],
2244
- validate: (input) => input.length > 0 || "Select at least one transfer type",
2245
- },
2246
- {
2247
- type: "list",
2248
- name: "concurrencyLimit",
2249
- message: "Select concurrency limit:",
2250
- choices: [
2251
- { name: "5 (Conservative) - Users: 2, Files: 1", value: 5 },
2252
- { name: "10 (Balanced) - Users: 5, Files: 2", value: 10 },
2253
- { name: "15 - Users: 7, Files: 3", value: 15 },
2254
- { name: "20 - Users: 10, Files: 5", value: 20 },
2255
- { name: "25 - Users: 12, Files: 6", value: 25 },
2256
- { name: "30 - Users: 15, Files: 7", value: 30 },
2257
- { name: "35 - Users: 17, Files: 8", value: 35 },
2258
- { name: "40 - Users: 20, Files: 10", value: 40 },
2259
- { name: "45 - Users: 22, Files: 11", value: 45 },
2260
- { name: "50 - Users: 25, Files: 12", value: 50 },
2261
- { name: "55 - Users: 27, Files: 13", value: 55 },
2262
- { name: "60 - Users: 30, Files: 15", value: 60 },
2263
- { name: "65 - Users: 32, Files: 16", value: 65 },
2264
- { name: "70 - Users: 35, Files: 17", value: 70 },
2265
- { name: "75 - Users: 37, Files: 18", value: 75 },
2266
- { name: "80 - Users: 40, Files: 20", value: 80 },
2267
- { name: "85 - Users: 42, Files: 21", value: 85 },
2268
- { name: "90 - Users: 45, Files: 22", value: 90 },
2269
- { name: "95 - Users: 47, Files: 23", value: 95 },
2270
- { name: "100 (Aggressive) - Users: 50, Files: 25", value: 100 },
2271
- ],
2272
- default: 10,
2273
- },
2274
- {
2275
- type: "confirm",
2276
- name: "dryRun",
2277
- message: "Run in dry-run mode (no actual changes)?",
2278
- default: false,
2279
- },
2280
- ]);
2281
-
2282
- // Confirmation
2283
- const { confirmed } = await inquirer.prompt([
2284
- {
2285
- type: "confirm",
2286
- name: "confirmed",
2287
- message: `Are you sure you want to ${transferOptions.dryRun ? "dry-run" : "perform"} comprehensive transfer from ${sourceConfig.sourceEndpoint} to ${targetConfig.targetEndpoint}?`,
2288
- default: false,
2289
- },
2290
- ]);
2291
-
2292
- if (!confirmed) {
2293
- MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
2294
- return;
2295
- }
2296
-
2297
- // Password preservation information
2298
- if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
2299
- MessageFormatter.info("User Password Transfer Information:", { prefix: "Transfer" });
2300
- MessageFormatter.info("✅ Users with hashed passwords (Argon2, Bcrypt, Scrypt, MD5, SHA, PHPass) will preserve their passwords", { prefix: "Transfer" });
2301
- MessageFormatter.info("⚠️ Users without hash information will receive temporary passwords and need to reset", { prefix: "Transfer" });
2302
- MessageFormatter.info("🔒 All user data (preferences, labels, verification status) will be preserved", { prefix: "Transfer" });
2303
-
2304
- const { continueWithUsers } = await inquirer.prompt([
2305
- {
2306
- type: "confirm",
2307
- name: "continueWithUsers",
2308
- message: "Continue with user transfer?",
2309
- default: true,
2310
- },
2311
- ]);
2312
-
2313
- if (!continueWithUsers) {
2314
- // Remove users from transfer types
2315
- transferOptions.transferTypes = transferOptions.transferTypes.filter((type: string) => type !== "users");
2316
- if (transferOptions.transferTypes.length === 0) {
2317
- MessageFormatter.info("No transfer types selected, cancelling", { prefix: "Transfer" });
2318
- return;
2319
- }
2320
- }
2321
- }
2322
-
2323
- // Execute comprehensive transfer
2324
- const comprehensiveTransferOptions: ComprehensiveTransferOptions = {
2325
- sourceEndpoint: sourceConfig.sourceEndpoint,
2326
- sourceProject: sourceConfig.sourceProject,
2327
- sourceKey: sourceConfig.sourceKey,
2328
- targetEndpoint: targetConfig.targetEndpoint,
2329
- targetProject: targetConfig.targetProject,
2330
- targetKey: targetConfig.targetKey,
2331
- transferUsers: transferOptions.transferTypes.includes("users"),
2332
- transferTeams: transferOptions.transferTypes.includes("teams"),
2333
- transferDatabases: transferOptions.transferTypes.includes("databases"),
2334
- transferBuckets: transferOptions.transferTypes.includes("buckets"),
2335
- transferFunctions: transferOptions.transferTypes.includes("functions"),
2336
- concurrencyLimit: transferOptions.concurrencyLimit,
2337
- dryRun: transferOptions.dryRun,
2338
- };
2339
-
2340
- const transfer = new ComprehensiveTransfer(comprehensiveTransferOptions);
2341
- const results = await transfer.execute();
2342
-
2343
- // Display results
2344
- if (transferOptions.dryRun) {
2345
- MessageFormatter.success("Dry run completed successfully!", { prefix: "Transfer" });
2346
- } else {
2347
- MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
2348
- if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
2349
- MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
2350
- MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
2351
- }
2352
- if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
2353
- MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
2354
- }
2355
- }
2356
-
2357
- } catch (error) {
2358
- MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
2359
- }
2360
- }
2361
1072
  }