appwrite-utils-cli 1.5.1 → 1.6.0

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