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