appwrite-utils-cli 1.9.7 → 1.11.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 (425) hide show
  1. package/CONFIG_TODO.md +1189 -1189
  2. package/README.md +1004 -1004
  3. package/SELECTION_DIALOGS.md +145 -145
  4. package/SERVICE_IMPLEMENTATION_REPORT.md +462 -462
  5. package/package.json +6 -3
  6. package/scripts/copy-templates.ts +23 -23
  7. package/src/adapters/index.ts +11 -37
  8. package/src/backups/operations/bucketBackup.ts +277 -277
  9. package/src/backups/operations/collectionBackup.ts +310 -310
  10. package/src/backups/operations/comprehensiveBackup.ts +342 -342
  11. package/src/backups/schemas/bucketManifest.ts +78 -78
  12. package/src/backups/schemas/comprehensiveManifest.ts +76 -76
  13. package/src/backups/tracking/centralizedTracking.ts +352 -352
  14. package/src/cli/commands/configCommands.ts +265 -201
  15. package/src/cli/commands/databaseCommands.ts +931 -879
  16. package/src/cli/commands/functionCommands.ts +333 -332
  17. package/src/cli/commands/importFileCommands.ts +815 -0
  18. package/src/cli/commands/schemaCommands.ts +141 -141
  19. package/src/cli/commands/storageCommands.ts +2 -3
  20. package/src/cli/commands/transferCommands.ts +454 -457
  21. package/src/collections/attributes.ts.backup +1555 -1555
  22. package/src/collections/{attributes.ts → columns.ts} +15 -10
  23. package/src/collections/indexes.ts +350 -352
  24. package/src/collections/methods.ts +714 -700
  25. package/src/collections/tableOperations.ts +29 -8
  26. package/src/collections/transferOperations.ts +376 -377
  27. package/src/collections/wipeOperations.ts +449 -346
  28. package/src/databases/methods.ts +49 -49
  29. package/src/databases/setup.ts +77 -77
  30. package/src/examples/yamlTerminologyExample.ts +346 -346
  31. package/src/functions/deployments.ts +221 -220
  32. package/src/functions/fnConfigDiscovery.ts +2 -2
  33. package/src/functions/methods.ts +284 -284
  34. package/src/functions/templates/count-docs-in-collection/README.md +53 -53
  35. package/src/functions/templates/count-docs-in-collection/src/main.ts +159 -159
  36. package/src/functions/templates/count-docs-in-collection/src/request.ts +8 -8
  37. package/src/functions/templates/hono-typescript/README.md +285 -285
  38. package/src/functions/templates/hono-typescript/src/adapters/request.ts +73 -73
  39. package/src/functions/templates/hono-typescript/src/adapters/response.ts +105 -105
  40. package/src/functions/templates/hono-typescript/src/app.ts +179 -179
  41. package/src/functions/templates/hono-typescript/src/context.ts +102 -102
  42. package/src/functions/templates/hono-typescript/src/{index.ts → main.ts} +53 -53
  43. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +118 -118
  44. package/src/functions/templates/typescript-node/README.md +31 -31
  45. package/src/functions/templates/typescript-node/src/context.ts +102 -102
  46. package/src/functions/templates/typescript-node/src/{index.ts → main.ts} +29 -29
  47. package/src/functions/templates/uv/README.md +30 -30
  48. package/src/functions/templates/uv/pyproject.toml +29 -29
  49. package/src/functions/templates/uv/src/context.py +124 -124
  50. package/src/functions/templates/uv/src/{index.py → main.py} +45 -45
  51. package/src/init.ts +62 -62
  52. package/src/interactiveCLI.ts +1095 -1030
  53. package/src/main.ts +1517 -1670
  54. package/src/migrations/afterImportActions.ts +579 -580
  55. package/src/migrations/appwriteToX.ts +634 -630
  56. package/src/migrations/comprehensiveTransfer.ts +2149 -2149
  57. package/src/migrations/dataLoader.ts +1729 -1702
  58. package/src/migrations/importController.ts +440 -428
  59. package/src/migrations/importDataActions.ts +315 -315
  60. package/src/migrations/relationships.ts +333 -334
  61. package/src/migrations/services/DataTransformationService.ts +195 -195
  62. package/src/migrations/services/FileHandlerService.ts +310 -310
  63. package/src/migrations/services/ImportOrchestrator.ts +674 -665
  64. package/src/migrations/services/RateLimitManager.ts +362 -362
  65. package/src/migrations/services/RelationshipResolver.ts +460 -460
  66. package/src/migrations/services/UserMappingService.ts +344 -344
  67. package/src/migrations/services/ValidationService.ts +333 -333
  68. package/src/migrations/transfer.ts +987 -942
  69. package/src/migrations/yaml/YamlImportConfigLoader.ts +438 -438
  70. package/src/migrations/yaml/YamlImportIntegration.ts +438 -438
  71. package/src/migrations/yaml/generateImportSchemas.ts +1347 -1347
  72. package/src/schemas/authUser.ts +23 -23
  73. package/src/setup.ts +8 -8
  74. package/src/setupCommands.ts +5 -6
  75. package/src/setupController.ts +42 -42
  76. package/src/shared/backupMetadataSchema.ts +93 -93
  77. package/src/shared/backupTracking.ts +211 -211
  78. package/src/shared/confirmationDialogs.ts +326 -326
  79. package/src/shared/migrationHelpers.ts +232 -232
  80. package/src/shared/operationLogger.ts +20 -20
  81. package/src/shared/operationQueue.ts +326 -327
  82. package/src/shared/operationsTable.ts +338 -338
  83. package/src/shared/operationsTableSchema.ts +60 -60
  84. package/src/shared/progressManager.ts +277 -277
  85. package/src/shared/relationshipExtractor.ts +214 -214
  86. package/src/shared/selectionDialogs.ts +775 -722
  87. package/src/storage/backupCompression.ts +88 -88
  88. package/src/storage/methods.ts +695 -682
  89. package/src/storage/schemas.ts +205 -205
  90. package/src/tables/indexManager.ts +408 -408
  91. package/src/types/node-appwrite-tablesdb.d.ts +43 -43
  92. package/src/types.ts +9 -9
  93. package/src/users/methods.ts +358 -359
  94. package/src/utils/configMigration.ts +347 -347
  95. package/src/utils/index.ts +2 -2
  96. package/src/utils/loadConfigs.ts +457 -449
  97. package/src/utils/setupFiles.ts +1236 -1238
  98. package/src/utilsController.ts +1263 -1213
  99. package/tests/README.md +496 -496
  100. package/tests/adapters/AdapterFactory.test.ts +276 -276
  101. package/tests/integration/syncOperations.test.ts +462 -462
  102. package/tests/jest.config.js +24 -24
  103. package/tests/migration/configMigration.test.ts +545 -545
  104. package/tests/setup.ts +61 -61
  105. package/tests/testUtils.ts +339 -339
  106. package/tests/utils/loadConfigs.test.ts +349 -349
  107. package/tests/validation/configValidation.test.ts +411 -411
  108. package/tsconfig.json +44 -44
  109. package/.appwrite/.yaml_schemas/appwrite-config.schema.json +0 -380
  110. package/.appwrite/.yaml_schemas/collection.schema.json +0 -255
  111. package/.appwrite/collections/Categories.yaml +0 -182
  112. package/.appwrite/collections/ExampleCollection.yaml +0 -36
  113. package/.appwrite/collections/Posts.yaml +0 -227
  114. package/.appwrite/collections/Users.yaml +0 -149
  115. package/.appwrite/config.yaml +0 -109
  116. package/.appwrite/import/README.md +0 -148
  117. package/.appwrite/import/categories-import.yaml +0 -129
  118. package/.appwrite/import/posts-import.yaml +0 -208
  119. package/.appwrite/import/users-import.yaml +0 -130
  120. package/.appwrite/importData/categories.json +0 -194
  121. package/.appwrite/importData/posts.json +0 -270
  122. package/.appwrite/importData/users.json +0 -220
  123. package/.appwrite/schemas/categories.json +0 -128
  124. package/.appwrite/schemas/exampleCollection.json +0 -52
  125. package/.appwrite/schemas/posts.json +0 -173
  126. package/.appwrite/schemas/users.json +0 -125
  127. package/dist/adapters/AdapterFactory.d.ts +0 -94
  128. package/dist/adapters/AdapterFactory.js +0 -420
  129. package/dist/adapters/DatabaseAdapter.d.ts +0 -243
  130. package/dist/adapters/DatabaseAdapter.js +0 -50
  131. package/dist/adapters/LegacyAdapter.d.ts +0 -50
  132. package/dist/adapters/LegacyAdapter.js +0 -615
  133. package/dist/adapters/TablesDBAdapter.d.ts +0 -45
  134. package/dist/adapters/TablesDBAdapter.js +0 -611
  135. package/dist/adapters/index.d.ts +0 -11
  136. package/dist/adapters/index.js +0 -12
  137. package/dist/backups/operations/bucketBackup.d.ts +0 -19
  138. package/dist/backups/operations/bucketBackup.js +0 -197
  139. package/dist/backups/operations/collectionBackup.d.ts +0 -30
  140. package/dist/backups/operations/collectionBackup.js +0 -201
  141. package/dist/backups/operations/comprehensiveBackup.d.ts +0 -25
  142. package/dist/backups/operations/comprehensiveBackup.js +0 -238
  143. package/dist/backups/schemas/bucketManifest.d.ts +0 -93
  144. package/dist/backups/schemas/bucketManifest.js +0 -33
  145. package/dist/backups/schemas/comprehensiveManifest.d.ts +0 -108
  146. package/dist/backups/schemas/comprehensiveManifest.js +0 -32
  147. package/dist/backups/tracking/centralizedTracking.d.ts +0 -34
  148. package/dist/backups/tracking/centralizedTracking.js +0 -274
  149. package/dist/cli/commands/configCommands.d.ts +0 -8
  150. package/dist/cli/commands/configCommands.js +0 -166
  151. package/dist/cli/commands/databaseCommands.d.ts +0 -14
  152. package/dist/cli/commands/databaseCommands.js +0 -644
  153. package/dist/cli/commands/functionCommands.d.ts +0 -7
  154. package/dist/cli/commands/functionCommands.js +0 -330
  155. package/dist/cli/commands/schemaCommands.d.ts +0 -7
  156. package/dist/cli/commands/schemaCommands.js +0 -169
  157. package/dist/cli/commands/storageCommands.d.ts +0 -5
  158. package/dist/cli/commands/storageCommands.js +0 -143
  159. package/dist/cli/commands/transferCommands.d.ts +0 -5
  160. package/dist/cli/commands/transferCommands.js +0 -384
  161. package/dist/collections/attributes.d.ts +0 -13
  162. package/dist/collections/attributes.js +0 -1333
  163. package/dist/collections/indexes.d.ts +0 -12
  164. package/dist/collections/indexes.js +0 -217
  165. package/dist/collections/methods.d.ts +0 -19
  166. package/dist/collections/methods.js +0 -587
  167. package/dist/collections/tableOperations.d.ts +0 -86
  168. package/dist/collections/tableOperations.js +0 -447
  169. package/dist/collections/transferOperations.d.ts +0 -8
  170. package/dist/collections/transferOperations.js +0 -412
  171. package/dist/collections/wipeOperations.d.ts +0 -16
  172. package/dist/collections/wipeOperations.js +0 -233
  173. package/dist/config/ConfigManager.d.ts +0 -450
  174. package/dist/config/ConfigManager.js +0 -650
  175. package/dist/config/configMigration.d.ts +0 -87
  176. package/dist/config/configMigration.js +0 -390
  177. package/dist/config/configValidation.d.ts +0 -66
  178. package/dist/config/configValidation.js +0 -358
  179. package/dist/config/index.d.ts +0 -8
  180. package/dist/config/index.js +0 -7
  181. package/dist/config/services/ConfigDiscoveryService.d.ts +0 -122
  182. package/dist/config/services/ConfigDiscoveryService.js +0 -322
  183. package/dist/config/services/ConfigLoaderService.d.ts +0 -129
  184. package/dist/config/services/ConfigLoaderService.js +0 -535
  185. package/dist/config/services/ConfigMergeService.d.ts +0 -208
  186. package/dist/config/services/ConfigMergeService.js +0 -308
  187. package/dist/config/services/ConfigValidationService.d.ts +0 -214
  188. package/dist/config/services/ConfigValidationService.js +0 -310
  189. package/dist/config/services/SessionAuthService.d.ts +0 -225
  190. package/dist/config/services/SessionAuthService.js +0 -456
  191. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +0 -1
  192. package/dist/config/services/__tests__/ConfigMergeService.test.js +0 -271
  193. package/dist/config/services/index.d.ts +0 -13
  194. package/dist/config/services/index.js +0 -10
  195. package/dist/config/yamlConfig.d.ts +0 -722
  196. package/dist/config/yamlConfig.js +0 -702
  197. package/dist/databases/methods.d.ts +0 -6
  198. package/dist/databases/methods.js +0 -35
  199. package/dist/databases/setup.d.ts +0 -5
  200. package/dist/databases/setup.js +0 -45
  201. package/dist/examples/yamlTerminologyExample.d.ts +0 -42
  202. package/dist/examples/yamlTerminologyExample.js +0 -272
  203. package/dist/functions/deployments.d.ts +0 -4
  204. package/dist/functions/deployments.js +0 -146
  205. package/dist/functions/fnConfigDiscovery.d.ts +0 -3
  206. package/dist/functions/fnConfigDiscovery.js +0 -108
  207. package/dist/functions/methods.d.ts +0 -16
  208. package/dist/functions/methods.js +0 -174
  209. package/dist/functions/pathResolution.d.ts +0 -37
  210. package/dist/functions/pathResolution.js +0 -185
  211. package/dist/functions/templates/count-docs-in-collection/README.md +0 -54
  212. package/dist/functions/templates/count-docs-in-collection/package.json +0 -25
  213. package/dist/functions/templates/count-docs-in-collection/src/main.ts +0 -159
  214. package/dist/functions/templates/count-docs-in-collection/src/request.ts +0 -9
  215. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  216. package/dist/functions/templates/hono-typescript/README.md +0 -286
  217. package/dist/functions/templates/hono-typescript/package.json +0 -26
  218. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +0 -74
  219. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +0 -106
  220. package/dist/functions/templates/hono-typescript/src/app.ts +0 -180
  221. package/dist/functions/templates/hono-typescript/src/context.ts +0 -103
  222. package/dist/functions/templates/hono-typescript/src/index.ts +0 -54
  223. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +0 -119
  224. package/dist/functions/templates/hono-typescript/tsconfig.json +0 -20
  225. package/dist/functions/templates/typescript-node/README.md +0 -32
  226. package/dist/functions/templates/typescript-node/package.json +0 -25
  227. package/dist/functions/templates/typescript-node/src/context.ts +0 -103
  228. package/dist/functions/templates/typescript-node/src/index.ts +0 -29
  229. package/dist/functions/templates/typescript-node/tsconfig.json +0 -28
  230. package/dist/functions/templates/uv/README.md +0 -31
  231. package/dist/functions/templates/uv/pyproject.toml +0 -30
  232. package/dist/functions/templates/uv/src/__init__.py +0 -0
  233. package/dist/functions/templates/uv/src/context.py +0 -125
  234. package/dist/functions/templates/uv/src/index.py +0 -46
  235. package/dist/init.d.ts +0 -2
  236. package/dist/init.js +0 -57
  237. package/dist/interactiveCLI.d.ts +0 -31
  238. package/dist/interactiveCLI.js +0 -898
  239. package/dist/main.d.ts +0 -2
  240. package/dist/main.js +0 -1180
  241. package/dist/migrations/afterImportActions.d.ts +0 -17
  242. package/dist/migrations/afterImportActions.js +0 -306
  243. package/dist/migrations/appwriteToX.d.ts +0 -211
  244. package/dist/migrations/appwriteToX.js +0 -491
  245. package/dist/migrations/comprehensiveTransfer.d.ts +0 -147
  246. package/dist/migrations/comprehensiveTransfer.js +0 -1317
  247. package/dist/migrations/dataLoader.d.ts +0 -753
  248. package/dist/migrations/dataLoader.js +0 -1250
  249. package/dist/migrations/importController.d.ts +0 -23
  250. package/dist/migrations/importController.js +0 -268
  251. package/dist/migrations/importDataActions.d.ts +0 -50
  252. package/dist/migrations/importDataActions.js +0 -230
  253. package/dist/migrations/relationships.d.ts +0 -29
  254. package/dist/migrations/relationships.js +0 -204
  255. package/dist/migrations/services/DataTransformationService.d.ts +0 -55
  256. package/dist/migrations/services/DataTransformationService.js +0 -158
  257. package/dist/migrations/services/FileHandlerService.d.ts +0 -75
  258. package/dist/migrations/services/FileHandlerService.js +0 -236
  259. package/dist/migrations/services/ImportOrchestrator.d.ts +0 -97
  260. package/dist/migrations/services/ImportOrchestrator.js +0 -485
  261. package/dist/migrations/services/RateLimitManager.d.ts +0 -138
  262. package/dist/migrations/services/RateLimitManager.js +0 -279
  263. package/dist/migrations/services/RelationshipResolver.d.ts +0 -120
  264. package/dist/migrations/services/RelationshipResolver.js +0 -332
  265. package/dist/migrations/services/UserMappingService.d.ts +0 -109
  266. package/dist/migrations/services/UserMappingService.js +0 -277
  267. package/dist/migrations/services/ValidationService.d.ts +0 -74
  268. package/dist/migrations/services/ValidationService.js +0 -260
  269. package/dist/migrations/transfer.d.ts +0 -26
  270. package/dist/migrations/transfer.js +0 -608
  271. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +0 -131
  272. package/dist/migrations/yaml/YamlImportConfigLoader.js +0 -383
  273. package/dist/migrations/yaml/YamlImportIntegration.d.ts +0 -93
  274. package/dist/migrations/yaml/YamlImportIntegration.js +0 -341
  275. package/dist/migrations/yaml/generateImportSchemas.d.ts +0 -30
  276. package/dist/migrations/yaml/generateImportSchemas.js +0 -1327
  277. package/dist/schemas/authUser.d.ts +0 -24
  278. package/dist/schemas/authUser.js +0 -17
  279. package/dist/setup.d.ts +0 -2
  280. package/dist/setup.js +0 -5
  281. package/dist/setupCommands.d.ts +0 -58
  282. package/dist/setupCommands.js +0 -490
  283. package/dist/setupController.d.ts +0 -9
  284. package/dist/setupController.js +0 -34
  285. package/dist/shared/attributeMapper.d.ts +0 -20
  286. package/dist/shared/attributeMapper.js +0 -203
  287. package/dist/shared/backupMetadataSchema.d.ts +0 -94
  288. package/dist/shared/backupMetadataSchema.js +0 -38
  289. package/dist/shared/backupTracking.d.ts +0 -18
  290. package/dist/shared/backupTracking.js +0 -176
  291. package/dist/shared/confirmationDialogs.d.ts +0 -75
  292. package/dist/shared/confirmationDialogs.js +0 -236
  293. package/dist/shared/errorUtils.d.ts +0 -54
  294. package/dist/shared/errorUtils.js +0 -95
  295. package/dist/shared/functionManager.d.ts +0 -48
  296. package/dist/shared/functionManager.js +0 -348
  297. package/dist/shared/jsonSchemaGenerator.d.ts +0 -50
  298. package/dist/shared/jsonSchemaGenerator.js +0 -290
  299. package/dist/shared/logging.d.ts +0 -61
  300. package/dist/shared/logging.js +0 -116
  301. package/dist/shared/messageFormatter.d.ts +0 -39
  302. package/dist/shared/messageFormatter.js +0 -162
  303. package/dist/shared/migrationHelpers.d.ts +0 -61
  304. package/dist/shared/migrationHelpers.js +0 -145
  305. package/dist/shared/operationLogger.d.ts +0 -10
  306. package/dist/shared/operationLogger.js +0 -12
  307. package/dist/shared/operationQueue.d.ts +0 -40
  308. package/dist/shared/operationQueue.js +0 -311
  309. package/dist/shared/operationsTable.d.ts +0 -26
  310. package/dist/shared/operationsTable.js +0 -286
  311. package/dist/shared/operationsTableSchema.d.ts +0 -48
  312. package/dist/shared/operationsTableSchema.js +0 -35
  313. package/dist/shared/progressManager.d.ts +0 -62
  314. package/dist/shared/progressManager.js +0 -215
  315. package/dist/shared/pydanticModelGenerator.d.ts +0 -17
  316. package/dist/shared/pydanticModelGenerator.js +0 -615
  317. package/dist/shared/relationshipExtractor.d.ts +0 -56
  318. package/dist/shared/relationshipExtractor.js +0 -138
  319. package/dist/shared/schemaGenerator.d.ts +0 -40
  320. package/dist/shared/schemaGenerator.js +0 -556
  321. package/dist/shared/selectionDialogs.d.ts +0 -214
  322. package/dist/shared/selectionDialogs.js +0 -544
  323. package/dist/storage/backupCompression.d.ts +0 -20
  324. package/dist/storage/backupCompression.js +0 -67
  325. package/dist/storage/methods.d.ts +0 -32
  326. package/dist/storage/methods.js +0 -472
  327. package/dist/storage/schemas.d.ts +0 -842
  328. package/dist/storage/schemas.js +0 -175
  329. package/dist/tables/indexManager.d.ts +0 -65
  330. package/dist/tables/indexManager.js +0 -294
  331. package/dist/types.d.ts +0 -4
  332. package/dist/types.js +0 -3
  333. package/dist/users/methods.d.ts +0 -16
  334. package/dist/users/methods.js +0 -277
  335. package/dist/utils/ClientFactory.d.ts +0 -87
  336. package/dist/utils/ClientFactory.js +0 -212
  337. package/dist/utils/configDiscovery.d.ts +0 -78
  338. package/dist/utils/configDiscovery.js +0 -472
  339. package/dist/utils/configMigration.d.ts +0 -1
  340. package/dist/utils/configMigration.js +0 -261
  341. package/dist/utils/constantsGenerator.d.ts +0 -31
  342. package/dist/utils/constantsGenerator.js +0 -321
  343. package/dist/utils/dataConverters.d.ts +0 -46
  344. package/dist/utils/dataConverters.js +0 -139
  345. package/dist/utils/directoryUtils.d.ts +0 -22
  346. package/dist/utils/directoryUtils.js +0 -59
  347. package/dist/utils/getClientFromConfig.d.ts +0 -39
  348. package/dist/utils/getClientFromConfig.js +0 -199
  349. package/dist/utils/helperFunctions.d.ts +0 -63
  350. package/dist/utils/helperFunctions.js +0 -156
  351. package/dist/utils/index.d.ts +0 -2
  352. package/dist/utils/index.js +0 -2
  353. package/dist/utils/loadConfigs.d.ts +0 -50
  354. package/dist/utils/loadConfigs.js +0 -358
  355. package/dist/utils/pathResolvers.d.ts +0 -53
  356. package/dist/utils/pathResolvers.js +0 -72
  357. package/dist/utils/projectConfig.d.ts +0 -122
  358. package/dist/utils/projectConfig.js +0 -206
  359. package/dist/utils/retryFailedPromises.d.ts +0 -2
  360. package/dist/utils/retryFailedPromises.js +0 -23
  361. package/dist/utils/sessionAuth.d.ts +0 -48
  362. package/dist/utils/sessionAuth.js +0 -164
  363. package/dist/utils/setupFiles.d.ts +0 -4
  364. package/dist/utils/setupFiles.js +0 -1192
  365. package/dist/utils/typeGuards.d.ts +0 -35
  366. package/dist/utils/typeGuards.js +0 -57
  367. package/dist/utils/validationRules.d.ts +0 -43
  368. package/dist/utils/validationRules.js +0 -42
  369. package/dist/utils/versionDetection.d.ts +0 -58
  370. package/dist/utils/versionDetection.js +0 -251
  371. package/dist/utils/yamlConverter.d.ts +0 -100
  372. package/dist/utils/yamlConverter.js +0 -428
  373. package/dist/utils/yamlLoader.d.ts +0 -70
  374. package/dist/utils/yamlLoader.js +0 -267
  375. package/dist/utilsController.d.ts +0 -107
  376. package/dist/utilsController.js +0 -873
  377. package/src/adapters/AdapterFactory.ts +0 -529
  378. package/src/adapters/DatabaseAdapter.ts +0 -319
  379. package/src/adapters/LegacyAdapter.ts +0 -844
  380. package/src/adapters/TablesDBAdapter.ts +0 -823
  381. package/src/config/ConfigManager.ts +0 -849
  382. package/src/config/README.md +0 -274
  383. package/src/config/configMigration.ts +0 -575
  384. package/src/config/configValidation.ts +0 -445
  385. package/src/config/index.ts +0 -10
  386. package/src/config/services/ConfigDiscoveryService.ts +0 -410
  387. package/src/config/services/ConfigLoaderService.ts +0 -732
  388. package/src/config/services/ConfigMergeService.ts +0 -388
  389. package/src/config/services/ConfigValidationService.ts +0 -394
  390. package/src/config/services/SessionAuthService.ts +0 -565
  391. package/src/config/services/__tests__/ConfigMergeService.test.ts +0 -351
  392. package/src/config/services/index.ts +0 -29
  393. package/src/config/yamlConfig.ts +0 -761
  394. package/src/functions/pathResolution.ts +0 -227
  395. package/src/functions/templates/count-docs-in-collection/package.json +0 -25
  396. package/src/functions/templates/count-docs-in-collection/tsconfig.json +0 -28
  397. package/src/functions/templates/hono-typescript/package.json +0 -26
  398. package/src/functions/templates/hono-typescript/tsconfig.json +0 -20
  399. package/src/functions/templates/typescript-node/package.json +0 -25
  400. package/src/functions/templates/typescript-node/tsconfig.json +0 -28
  401. package/src/shared/attributeMapper.ts +0 -229
  402. package/src/shared/errorUtils.ts +0 -110
  403. package/src/shared/functionManager.ts +0 -537
  404. package/src/shared/jsonSchemaGenerator.ts +0 -383
  405. package/src/shared/logging.ts +0 -149
  406. package/src/shared/messageFormatter.ts +0 -208
  407. package/src/shared/pydanticModelGenerator.ts +0 -618
  408. package/src/shared/schemaGenerator.ts +0 -644
  409. package/src/utils/ClientFactory.ts +0 -240
  410. package/src/utils/configDiscovery.ts +0 -557
  411. package/src/utils/constantsGenerator.ts +0 -369
  412. package/src/utils/dataConverters.ts +0 -159
  413. package/src/utils/directoryUtils.ts +0 -61
  414. package/src/utils/getClientFromConfig.ts +0 -257
  415. package/src/utils/helperFunctions.ts +0 -228
  416. package/src/utils/pathResolvers.ts +0 -81
  417. package/src/utils/projectConfig.ts +0 -340
  418. package/src/utils/retryFailedPromises.ts +0 -29
  419. package/src/utils/sessionAuth.ts +0 -230
  420. package/src/utils/typeGuards.ts +0 -65
  421. package/src/utils/validationRules.ts +0 -88
  422. package/src/utils/versionDetection.ts +0 -292
  423. package/src/utils/yamlConverter.ts +0 -542
  424. package/src/utils/yamlLoader.ts +0 -371
  425. package/tmp-sync-test/.appwrite/collections/TestCollection.yaml +0 -7
package/src/main.ts CHANGED
@@ -1,1670 +1,1517 @@
1
- #!/usr/bin/env node
2
- import yargs from "yargs";
3
- import { type ArgumentsCamelCase } from "yargs";
4
- import { hideBin } from "yargs/helpers";
5
- import { InteractiveCLI } from "./interactiveCLI.js";
6
- import { UtilsController, type SetupOptions } from "./utilsController.js";
7
- import type { TransferOptions } from "./migrations/transfer.js";
8
- import { Databases, Storage, type Models } from "node-appwrite";
9
- import { getClient } from "./utils/getClientFromConfig.js";
10
- import { fetchAllDatabases } from "./databases/methods.js";
11
- import { setupDirsFiles } from "./utils/setupFiles.js";
12
- import { fetchAllCollections } from "./collections/methods.js";
13
- import type { Specification } from "appwrite-utils";
14
- import chalk from "chalk";
15
- import { listSpecifications } from "./functions/methods.js";
16
- import { MessageFormatter } from "./shared/messageFormatter.js";
17
- import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
18
- import { SelectionDialogs } from "./shared/selectionDialogs.js";
19
- import { logger } from "./shared/logging.js";
20
- import type { SyncSelectionSummary, DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
21
- import path from "path";
22
- import fs from "fs";
23
- import { createRequire } from "node:module";
24
- import {
25
- loadAppwriteProjectConfig,
26
- findAppwriteProjectConfig,
27
- projectConfigToAppwriteConfig,
28
- } from "./utils/projectConfig.js";
29
- import {
30
- hasSessionAuth,
31
- getAvailableSessions,
32
- getAuthenticationStatus,
33
- } from "./utils/sessionAuth.js";
34
- import {
35
- findYamlConfig,
36
- loadYamlConfigWithSession,
37
- } from "./config/yamlConfig.js";
38
-
39
- const require = createRequire(import.meta.url);
40
- if (!(globalThis as any).require) {
41
- (globalThis as any).require = require;
42
- }
43
-
44
- interface CliOptions {
45
- config?: string;
46
- appwriteConfig?: boolean;
47
- it?: boolean;
48
- dbIds?: string;
49
- collectionIds?: string;
50
- bucketIds?: string;
51
- wipe?: "all" | "storage" | "docs" | "users";
52
- wipeCollections?: boolean;
53
- generate?: boolean;
54
- import?: boolean;
55
- backup?: boolean;
56
- backupFormat?: "json" | "zip";
57
- comprehensiveBackup?: boolean;
58
- trackingDatabaseId?: string;
59
- parallelDownloads?: number;
60
- writeData?: boolean;
61
- push?: boolean;
62
- sync?: boolean;
63
- endpoint?: string;
64
- projectId?: string;
65
- apiKey?: string;
66
- transfer?: boolean;
67
- transferUsers?: boolean;
68
- fromDbId?: string;
69
- toDbId?: string;
70
- fromCollectionId?: string;
71
- toCollectionId?: string;
72
- fromBucketId?: string;
73
- toBucketId?: string;
74
- remoteEndpoint?: string;
75
- remoteProjectId?: string;
76
- remoteApiKey?: string;
77
- setup?: boolean;
78
- updateFunctionSpec?: boolean;
79
- functionId?: string;
80
- specification?: string;
81
- migrateConfig?: boolean;
82
- generateConstants?: boolean;
83
- constantsLanguages?: string;
84
- constantsOutput?: string;
85
- migrateCollectionsToTables?: boolean;
86
- useSession?: boolean;
87
- session?: string;
88
- listBackups?: boolean;
89
- autoSync?: boolean;
90
- selectBuckets?: boolean;
91
- // New schema/constant CLI flags
92
- generateSchemas?: boolean;
93
- schemaFormat?: 'zod' | 'json' | 'pydantic' | 'both' | 'all';
94
- schemaOutDir?: string;
95
- constantsInclude?: string;
96
- }
97
-
98
- type ParsedArgv = ArgumentsCamelCase<CliOptions>;
99
-
100
- /**
101
- * Enhanced sync function with intelligent configuration detection and selection dialogs
102
- */
103
- async function performEnhancedSync(
104
- controller: UtilsController,
105
- parsedArgv: ParsedArgv
106
- ): Promise<SyncSelectionSummary | null> {
107
- try {
108
- MessageFormatter.banner("Enhanced Sync", "Intelligent configuration detection and selection");
109
-
110
- if (!controller.config) {
111
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Sync" });
112
- return null;
113
- }
114
-
115
- // Get all available databases from remote
116
- const availableDatabases = await fetchAllDatabases(controller.database!);
117
- if (availableDatabases.length === 0) {
118
- MessageFormatter.warning("No databases found in remote project", { prefix: "Sync" });
119
- return null;
120
- }
121
-
122
- // Get existing configuration
123
- const configuredDatabases = controller.config.databases || [];
124
- const configuredBuckets = controller.config.buckets || [];
125
-
126
- // Check if we have existing configuration
127
- const hasExistingConfig = configuredDatabases.length > 0 || configuredBuckets.length > 0;
128
-
129
- let syncExisting = false;
130
- let modifyConfiguration = true;
131
-
132
- if (hasExistingConfig) {
133
- // Prompt about existing configuration
134
- const response = await SelectionDialogs.promptForExistingConfig([
135
- ...configuredDatabases,
136
- ...configuredBuckets
137
- ]);
138
- syncExisting = response.syncExisting;
139
- modifyConfiguration = response.modifyConfiguration;
140
-
141
- if (syncExisting && !modifyConfiguration) {
142
- // Just sync existing configuration without changes
143
- MessageFormatter.info("Syncing existing configuration without modifications", { prefix: "Sync" });
144
-
145
- // Convert configured databases to DatabaseSelection format
146
- const databaseSelections: DatabaseSelection[] = configuredDatabases.map(db => ({
147
- databaseId: db.$id,
148
- databaseName: db.name,
149
- tableIds: [], // Tables will be populated from collections config
150
- tableNames: [],
151
- isNew: false
152
- }));
153
-
154
- // Convert configured buckets to BucketSelection format
155
- const bucketSelections: BucketSelection[] = configuredBuckets.map(bucket => ({
156
- bucketId: bucket.$id,
157
- bucketName: bucket.name,
158
- databaseId: undefined,
159
- databaseName: undefined,
160
- isNew: false
161
- }));
162
-
163
- const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
164
- databaseSelections,
165
- bucketSelections
166
- );
167
-
168
- const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
169
- if (!confirmed) {
170
- MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
171
- return null;
172
- }
173
-
174
- // Perform sync with existing configuration (pull from remote)
175
- await controller.selectivePull(databaseSelections, bucketSelections);
176
- return selectionSummary;
177
- }
178
- }
179
-
180
- if (!modifyConfiguration) {
181
- MessageFormatter.info("No configuration changes requested", { prefix: "Sync" });
182
- return null;
183
- }
184
-
185
- // Allow new items selection based on user choice
186
- const allowNewOnly = !syncExisting;
187
-
188
- // Select databases
189
- const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
190
- availableDatabases,
191
- configuredDatabases,
192
- {
193
- showSelectAll: false,
194
- allowNewOnly,
195
- defaultSelected: []
196
- }
197
- );
198
-
199
- if (selectedDatabaseIds.length === 0) {
200
- MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
201
- return null;
202
- }
203
-
204
- // For each selected database, get available tables and select them
205
- const tableSelectionsMap = new Map<string, string[]>();
206
- const availableTablesMap = new Map<string, any[]>();
207
-
208
- for (const databaseId of selectedDatabaseIds) {
209
- const database = availableDatabases.find(db => db.$id === databaseId)!;
210
-
211
- SelectionDialogs.showProgress(`Fetching tables for database: ${database.name}`);
212
-
213
- // Get available tables from remote
214
- const availableTables = await fetchAllCollections(databaseId, controller.database!);
215
- availableTablesMap.set(databaseId, availableTables);
216
-
217
- // Get configured tables for this database
218
- // Note: Collections are stored globally in the config, not per database
219
- const configuredTables = controller.config.collections || [];
220
-
221
- // Select tables for this database
222
- const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
223
- databaseId,
224
- database.name,
225
- availableTables,
226
- configuredTables,
227
- {
228
- showSelectAll: false,
229
- allowNewOnly,
230
- defaultSelected: []
231
- }
232
- );
233
-
234
- tableSelectionsMap.set(databaseId, selectedTableIds);
235
-
236
- if (selectedTableIds.length === 0) {
237
- MessageFormatter.warning(`No tables selected for database: ${database.name}`, { prefix: "Sync" });
238
- }
239
- }
240
-
241
- // Select buckets
242
- let selectedBucketIds: string[] = [];
243
-
244
- // Get available buckets from remote
245
- if (controller.storage) {
246
- try {
247
- // Note: We need to implement fetchAllBuckets or use storage.listBuckets
248
- // For now, we'll use configured buckets as available
249
- SelectionDialogs.showProgress("Fetching storage buckets...");
250
-
251
- // Create a mock availableBuckets array - in real implementation,
252
- // you'd fetch this from the Appwrite API
253
- const availableBuckets = configuredBuckets; // Placeholder
254
-
255
- selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
256
- selectedDatabaseIds,
257
- availableBuckets,
258
- configuredBuckets,
259
- {
260
- showSelectAll: false,
261
- allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
262
- groupByDatabase: true,
263
- defaultSelected: []
264
- }
265
- );
266
- } catch (error) {
267
- MessageFormatter.warning("Could not fetch storage buckets", { prefix: "Sync" });
268
- logger.warn("Failed to fetch buckets during sync", { error });
269
- }
270
- }
271
-
272
- // Create selection objects
273
- const databaseSelections = SelectionDialogs.createDatabaseSelection(
274
- selectedDatabaseIds,
275
- availableDatabases,
276
- tableSelectionsMap,
277
- configuredDatabases,
278
- availableTablesMap
279
- );
280
-
281
- const bucketSelections = SelectionDialogs.createBucketSelection(
282
- selectedBucketIds,
283
- [], // availableBuckets - would be populated from API
284
- configuredBuckets,
285
- availableDatabases
286
- );
287
-
288
- // Show final confirmation
289
- const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
290
- databaseSelections,
291
- bucketSelections
292
- );
293
-
294
- const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
295
- if (!confirmed) {
296
- MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
297
- return null;
298
- }
299
-
300
- // Perform the selective sync (pull from remote)
301
- await controller.selectivePull(databaseSelections, bucketSelections);
302
-
303
- MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
304
- return selectionSummary;
305
-
306
- } catch (error) {
307
- SelectionDialogs.showError("Enhanced sync failed", error instanceof Error ? error : new Error(String(error)));
308
- return null;
309
- }
310
- }
311
-
312
- /**
313
- * Performs selective sync with the given database and bucket selections
314
- */
315
-
316
- /**
317
- * Checks if the migration from collections to tables should be allowed
318
- * Returns an object with:
319
- * - allowed: boolean indicating if migration should proceed
320
- * - reason: string explaining why migration was blocked (if not allowed)
321
- */
322
- function checkMigrationConditions(configPath: string): {
323
- allowed: boolean;
324
- reason?: string;
325
- } {
326
- const collectionsPath = path.join(configPath, "collections");
327
- const tablesPath = path.join(configPath, "tables");
328
-
329
- // Check if collections/ folder exists
330
- if (!fs.existsSync(collectionsPath)) {
331
- return {
332
- allowed: false,
333
- reason:
334
- "No collections/ folder found. Migration requires existing collections to migrate.",
335
- };
336
- }
337
-
338
- // Check if collections/ folder has YAML files
339
- const collectionFiles = fs
340
- .readdirSync(collectionsPath)
341
- .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
342
-
343
- if (collectionFiles.length === 0) {
344
- return {
345
- allowed: false,
346
- reason:
347
- "No YAML files found in collections/ folder. Migration requires existing collection YAML files.",
348
- };
349
- }
350
-
351
- // Check if tables/ folder exists and has YAML files
352
- if (fs.existsSync(tablesPath)) {
353
- const tableFiles = fs
354
- .readdirSync(tablesPath)
355
- .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
356
-
357
- if (tableFiles.length > 0) {
358
- return {
359
- allowed: false,
360
- reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`,
361
- };
362
- }
363
- }
364
-
365
- // All conditions met
366
- return { allowed: true };
367
- }
368
-
369
- const argv = yargs(hideBin(process.argv))
370
- .option("config", {
371
- type: "string",
372
- description: "Path to Appwrite configuration file (appwriteConfig.ts)",
373
- })
374
- .option("appwriteConfig", {
375
- alias: ["appwrite-config", "use-appwrite-config"],
376
- type: "boolean",
377
- description: "Prefer loading from appwrite.config.json instead of config.yaml",
378
- })
379
- .option("it", {
380
- alias: ["interactive", "i"],
381
- type: "boolean",
382
- description: "Launch interactive CLI mode with guided prompts",
383
- })
384
- .option("dbIds", {
385
- type: "string",
386
- description:
387
- "Comma-separated list of database IDs to target (e.g., 'db1,db2,db3')",
388
- })
389
- .option("collectionIds", {
390
- alias: ["collIds", "tableIds", "tables"],
391
- type: "string",
392
- description:
393
- "Comma-separated list of collection/table IDs to target (e.g., 'users,posts')",
394
- })
395
- .option("bucketIds", {
396
- type: "string",
397
- description: "Comma-separated list of bucket IDs to operate on",
398
- })
399
- .option("wipe", {
400
- choices: ["all", "docs", "users"] as const,
401
- description:
402
- "⚠️ DESTRUCTIVE: Wipe data (all: databases+storage+users, docs: documents only, users: user accounts only)",
403
- })
404
- .option("wipeCollections", {
405
- type: "boolean",
406
- description:
407
- "⚠️ DESTRUCTIVE: Wipe specific collections/tables (requires --collectionIds or --tableIds)",
408
- })
409
- .option("transferUsers", {
410
- type: "boolean",
411
- description: "Transfer users between projects",
412
- })
413
- .option("generate", {
414
- type: "boolean",
415
- description:
416
- "Generate TypeScript schemas and types from your Appwrite database schemas",
417
- })
418
- .option("import", {
419
- type: "boolean",
420
- description:
421
- "Import data from importData/ directory into your Appwrite databases",
422
- })
423
- .option("backup", {
424
- type: "boolean",
425
- description: "Create a complete backup of your databases and collections",
426
- })
427
- .option("backupFormat", {
428
- type: "string",
429
- choices: ["json", "zip"] as const,
430
- default: "json",
431
- description: "Backup file format (json or zip)",
432
- })
433
- .option("listBackups", {
434
- type: "boolean",
435
- description: "List all backups for databases",
436
- })
437
- .option("comprehensiveBackup", {
438
- alias: ["comprehensive", "backup-all"],
439
- type: "boolean",
440
- description:
441
- "🚀 Create comprehensive backup of ALL databases and ALL storage buckets",
442
- })
443
- .option("trackingDatabaseId", {
444
- alias: ["tracking-db"],
445
- type: "string",
446
- description:
447
- "Database ID to use for centralized backup tracking (interactive prompt if not specified)",
448
- })
449
- .option("parallelDownloads", {
450
- type: "number",
451
- default: 10,
452
- description:
453
- "Number of parallel file downloads for bucket backups (default: 10)",
454
- })
455
- .option("writeData", {
456
- type: "boolean",
457
- description:
458
- "Output converted import data to files for validation before importing",
459
- })
460
- .option("push", {
461
- type: "boolean",
462
- description:
463
- "Deploy your local configuration (collections, attributes, indexes) to Appwrite",
464
- })
465
- .option("sync", {
466
- type: "boolean",
467
- description:
468
- "Pull and synchronize your local config with the remote Appwrite project schema",
469
- })
470
- .option("autoSync", {
471
- alias: ["auto"],
472
- type: "boolean",
473
- description: "Skip prompts and sync all databases, tables, and buckets (current behavior)"
474
- })
475
- .option("selectBuckets", {
476
- type: "boolean",
477
- description: "Force bucket selection dialog even if buckets are already configured"
478
- })
479
- .option("endpoint", {
480
- type: "string",
481
- description: "Set the Appwrite endpoint",
482
- })
483
- .option("projectId", {
484
- type: "string",
485
- description: "Set the Appwrite project ID",
486
- })
487
- .option("apiKey", {
488
- type: "string",
489
- description: "Set the Appwrite API key",
490
- })
491
- .option("transfer", {
492
- type: "boolean",
493
- description:
494
- "Transfer documents and files between databases, collections, or projects",
495
- })
496
- .option("fromDbId", {
497
- alias: ["fromDb", "sourceDbId", "sourceDb"],
498
- type: "string",
499
- description: "Source database ID for transfer operations",
500
- })
501
- .option("toDbId", {
502
- alias: ["toDb", "targetDbId", "targetDb"],
503
- type: "string",
504
- description: "Target database ID for transfer operations",
505
- })
506
- .option("fromCollectionId", {
507
- alias: ["fromCollId", "fromColl"],
508
- type: "string",
509
- description: "Set the source collection ID for transfer",
510
- })
511
- .option("toCollectionId", {
512
- alias: ["toCollId", "toColl"],
513
- type: "string",
514
- description: "Set the destination collection ID for transfer",
515
- })
516
- .option("fromBucketId", {
517
- type: "string",
518
- description: "Set the source bucket ID for transfer",
519
- })
520
- .option("toBucketId", {
521
- type: "string",
522
- description: "Set the destination bucket ID for transfer",
523
- })
524
- .option("remoteEndpoint", {
525
- type: "string",
526
- description: "Set the remote Appwrite endpoint for transfer",
527
- })
528
- .option("remoteProjectId", {
529
- type: "string",
530
- description: "Set the remote Appwrite project ID for transfer",
531
- })
532
- .option("remoteApiKey", {
533
- type: "string",
534
- description: "Set the remote Appwrite API key for transfer",
535
- })
536
- .option("setup", {
537
- type: "boolean",
538
- description:
539
- "Initialize project with configuration files and directory structure",
540
- })
541
- .option("updateFunctionSpec", {
542
- type: "boolean",
543
- description: "Update function specifications",
544
- })
545
- .option("functionId", {
546
- type: "string",
547
- description: "Function ID to update",
548
- })
549
- .option("specification", {
550
- type: "string",
551
- description: "New function specification (e.g., 's-1vcpu-1gb')",
552
- choices: [
553
- "s-0.5vcpu-512mb",
554
- "s-1vcpu-1gb",
555
- "s-2vcpu-2gb",
556
- "s-2vcpu-4gb",
557
- "s-4vcpu-4gb",
558
- "s-4vcpu-8gb",
559
- "s-8vcpu-4gb",
560
- "s-8vcpu-8gb",
561
- ],
562
- })
563
- .option("migrateConfig", {
564
- alias: ["migrate"],
565
- type: "boolean",
566
- description:
567
- "Migrate appwriteConfig.ts to .appwrite structure with YAML configuration",
568
- })
569
- .option("generateConstants", {
570
- alias: ["constants"],
571
- type: "boolean",
572
- description:
573
- "Generate cross-language constants file with database, collection, bucket, and function IDs",
574
- })
575
- .option("constantsLanguages", {
576
- type: "string",
577
- description:
578
- "Comma-separated list of languages for constants (typescript,javascript,python,php,dart,json,env)",
579
- default: "typescript",
580
- })
581
- .option("constantsOutput", {
582
- type: "string",
583
- description:
584
- "Output directory for generated constants files (default: config-folder/constants)",
585
- default: "auto",
586
- })
587
- .option("constantsInclude", {
588
- type: "string",
589
- description:
590
- "Comma-separated categories to include: databases,collections,buckets,functions",
591
- })
592
- .option("generateSchemas", {
593
- type: "boolean",
594
- description: "Generate schemas/models without interactive prompts",
595
- })
596
- .option("schemaFormat", {
597
- type: "string",
598
- choices: ["zod", "json", "pydantic", "both", "all"],
599
- description: "Schema format: zod, json, pydantic, both (zod+json), or all",
600
- })
601
- .option("schemaOutDir", {
602
- type: "string",
603
- description: "Output directory for generated schemas (absolute path respected)",
604
- })
605
- .option("migrateCollectionsToTables", {
606
- alias: ["migrate-collections"],
607
- type: "boolean",
608
- description:
609
- "Migrate collections to tables format for TablesDB API compatibility",
610
- })
611
- .option("useSession", {
612
- alias: ["session"],
613
- type: "boolean",
614
- description: "Use Appwrite CLI session authentication instead of API key",
615
- })
616
- .option("sessionCookie", {
617
- type: "string",
618
- description: "Explicit session cookie to use for authentication",
619
- })
620
- .parse() as ParsedArgv;
621
-
622
- async function main() {
623
- const startTime = Date.now();
624
- const operationStats: Record<string, number> = {};
625
-
626
- // Early session detection for better user guidance
627
- const availableSessions = getAvailableSessions();
628
- let hasAnyValidSessions = availableSessions.length > 0;
629
-
630
- if (argv.it) {
631
- const cli = new InteractiveCLI(process.cwd());
632
- await cli.run();
633
- } else {
634
- // Enhanced config creation with session and project file support
635
- let directConfig: any = undefined;
636
-
637
- // Show authentication status on startup if no config provided
638
- if (
639
- !argv.config &&
640
- !argv.endpoint &&
641
- !argv.projectId &&
642
- !argv.apiKey &&
643
- !argv.useSession &&
644
- !argv.sessionCookie
645
- ) {
646
- if (hasAnyValidSessions) {
647
- MessageFormatter.info(
648
- `Found ${availableSessions.length} available session(s)`,
649
- { prefix: "Auth" }
650
- );
651
- availableSessions.forEach((session) => {
652
- MessageFormatter.info(
653
- ` \u2022 ${session.projectId} (${session.email || "unknown"}) at ${
654
- session.endpoint
655
- }`,
656
- { prefix: "Auth" }
657
- );
658
- });
659
- MessageFormatter.info(
660
- "Use --session to enable session authentication",
661
- { prefix: "Auth" }
662
- );
663
- } else {
664
- MessageFormatter.info("No active Appwrite sessions found", {
665
- prefix: "Auth",
666
- });
667
- MessageFormatter.info(
668
- "\u2022 Run 'appwrite login' to authenticate with session",
669
- { prefix: "Auth" }
670
- );
671
- MessageFormatter.info(
672
- "\u2022 Or provide --apiKey for API key authentication",
673
- { prefix: "Auth" }
674
- );
675
- }
676
- }
677
-
678
- // Priority 1: Check for appwrite.json project configuration
679
- const projectConfigPath = findAppwriteProjectConfig(process.cwd());
680
- if (projectConfigPath) {
681
- const projectConfig = loadAppwriteProjectConfig(projectConfigPath);
682
- if (projectConfig) {
683
- directConfig = projectConfigToAppwriteConfig(projectConfig);
684
- MessageFormatter.info(
685
- `Loaded project configuration from ${projectConfigPath}`,
686
- { prefix: "CLI" }
687
- );
688
- }
689
- }
690
-
691
- // Priority 2: CLI arguments override project config
692
- if (
693
- argv.endpoint ||
694
- argv.projectId ||
695
- argv.apiKey ||
696
- argv.useSession ||
697
- argv.sessionCookie
698
- ) {
699
- directConfig = {
700
- ...directConfig,
701
- appwriteEndpoint: argv.endpoint || directConfig?.appwriteEndpoint,
702
- appwriteProject: argv.projectId || directConfig?.appwriteProject,
703
- appwriteKey: argv.apiKey || directConfig?.appwriteKey,
704
- };
705
- }
706
-
707
- // Priority 3: Session authentication support with improved detection
708
- let sessionAuthAvailable = false;
709
-
710
- if (directConfig?.appwriteEndpoint && directConfig?.appwriteProject) {
711
- sessionAuthAvailable = hasSessionAuth(
712
- directConfig.appwriteEndpoint,
713
- directConfig.appwriteProject
714
- );
715
- }
716
-
717
- if (argv.useSession || argv.sessionCookie) {
718
- if (argv.sessionCookie) {
719
- // Explicit session cookie provided
720
- MessageFormatter.info(
721
- "Using explicit session cookie for authentication",
722
- { prefix: "Auth" }
723
- );
724
- } else if (sessionAuthAvailable) {
725
- MessageFormatter.info(
726
- "Session authentication detected and will be used",
727
- { prefix: "Auth" }
728
- );
729
- } else {
730
- MessageFormatter.warning(
731
- "Session authentication requested but no valid session found",
732
- { prefix: "Auth" }
733
- );
734
- const availableSessions = getAvailableSessions();
735
- if (availableSessions.length > 0) {
736
- MessageFormatter.info(
737
- `Available sessions: ${availableSessions
738
- .map((s) => `${s.projectId} (${s.email || "unknown"})`)
739
- .join(", ")}`,
740
- { prefix: "Auth" }
741
- );
742
- MessageFormatter.info(
743
- "Use --session flag to enable session authentication",
744
- { prefix: "Auth" }
745
- );
746
- } else {
747
- MessageFormatter.warning(
748
- "No Appwrite CLI sessions found. Please run 'appwrite login' first.",
749
- { prefix: "Auth" }
750
- );
751
- }
752
- MessageFormatter.error(
753
- "Session authentication requested but not available",
754
- undefined,
755
- { prefix: "Auth" }
756
- );
757
- return; // Exit early if session auth was requested but not available
758
- }
759
- } else if (sessionAuthAvailable && !argv.apiKey) {
760
- // Auto-detect session authentication when no API key is provided
761
- MessageFormatter.info(
762
- "Session authentication detected - no API key required",
763
- { prefix: "Auth" }
764
- );
765
- MessageFormatter.info(
766
- "Use --session flag to explicitly enable session authentication",
767
- { prefix: "Auth" }
768
- );
769
- }
770
-
771
- // Enhanced session authentication support:
772
- // 1. If session auth is explicitly requested via flags, use it
773
- // 2. If no API key is provided but sessions are available, offer to use session auth
774
- // 3. Auto-detect session authentication when possible
775
- let finalDirectConfig = directConfig;
776
-
777
- if (
778
- (argv.useSession || argv.sessionCookie) &&
779
- (!directConfig ||
780
- !directConfig.appwriteEndpoint ||
781
- !directConfig.appwriteProject)
782
- ) {
783
- // Don't pass incomplete directConfig - let UtilsController load YAML config normally
784
- finalDirectConfig = null;
785
- } else if (
786
- finalDirectConfig &&
787
- !finalDirectConfig.appwriteKey &&
788
- !argv.useSession &&
789
- !argv.sessionCookie
790
- ) {
791
- // Auto-detect session authentication when no API key provided
792
- if (sessionAuthAvailable) {
793
- MessageFormatter.info(
794
- "No API key provided, but session authentication is available",
795
- { prefix: "Auth" }
796
- );
797
- MessageFormatter.info(
798
- "Automatically using session authentication (add --session to suppress this message)",
799
- { prefix: "Auth" }
800
- );
801
- // Implicitly enable session authentication
802
- argv.useSession = true;
803
- }
804
- }
805
-
806
- // Create controller with session authentication support using singleton
807
- const controller = UtilsController.getInstance(
808
- process.cwd(),
809
- finalDirectConfig
810
- );
811
-
812
- // Pass session authentication and config options to the controller
813
- const initOptions: any = {};
814
- if (argv.useSession || argv.sessionCookie) {
815
- initOptions.useSession = true;
816
- if (argv.sessionCookie) {
817
- initOptions.sessionCookie = argv.sessionCookie;
818
- }
819
- }
820
- if (argv.appwriteConfig) {
821
- initOptions.preferJson = true;
822
- }
823
-
824
- await controller.init(initOptions);
825
-
826
- if (argv.setup) {
827
- await setupDirsFiles(false, process.cwd());
828
- return;
829
- }
830
-
831
- if (argv.migrateConfig) {
832
- const { migrateConfig } = await import("./utils/configMigration.js");
833
- await migrateConfig(process.cwd());
834
- return;
835
- }
836
-
837
- if (argv.generateConstants) {
838
- const { ConstantsGenerator } = await import(
839
- "./utils/constantsGenerator.js"
840
- );
841
- type SupportedLanguage =
842
- import("./utils/constantsGenerator.js").SupportedLanguage;
843
-
844
- if (!controller.config) {
845
- MessageFormatter.error("No Appwrite configuration found", undefined, {
846
- prefix: "Constants",
847
- });
848
- return;
849
- }
850
-
851
- const languages = argv
852
- .constantsLanguages!.split(",")
853
- .map((l) => l.trim()) as SupportedLanguage[];
854
-
855
- // Determine output directory - use config folder/constants by default, or custom path if specified
856
- let outputDir: string;
857
- if (argv.constantsOutput === "auto") {
858
- // Default case: use config directory + constants, fallback to current directory
859
- const configPath = controller.getAppwriteFolderPath();
860
- outputDir = configPath
861
- ? path.join(configPath, "constants")
862
- : path.join(process.cwd(), "constants");
863
- } else {
864
- // Custom output directory specified
865
- outputDir = argv.constantsOutput!;
866
- }
867
-
868
- MessageFormatter.info(
869
- `Generating constants for languages: ${languages.join(", ")}`,
870
- { prefix: "Constants" }
871
- );
872
-
873
- const generator = new ConstantsGenerator(controller.config);
874
- await generator.generateFiles(languages, outputDir);
875
-
876
- operationStats.generatedConstants = languages.length;
877
- MessageFormatter.success(`Constants generated in ${outputDir}`, {
878
- prefix: "Constants",
879
- });
880
- return;
881
- }
882
-
883
- if (argv.migrateCollectionsToTables) {
884
- try {
885
- if (!controller.config) {
886
- MessageFormatter.error("No Appwrite configuration found", undefined, {
887
- prefix: "Migration",
888
- });
889
- return;
890
- }
891
-
892
- // Get the config path from the controller or use .appwrite in current directory
893
- let configPath = controller.getAppwriteFolderPath();
894
- if (!configPath) {
895
- // Try .appwrite in current directory
896
- const defaultPath = path.join(process.cwd(), ".appwrite");
897
- if (fs.existsSync(defaultPath)) {
898
- configPath = defaultPath;
899
- } else {
900
- MessageFormatter.error(
901
- "Could not determine configuration folder path",
902
- undefined,
903
- { prefix: "Migration" }
904
- );
905
- MessageFormatter.info(
906
- "Make sure you have a .appwrite/ folder in your current directory",
907
- { prefix: "Migration" }
908
- );
909
- return;
910
- }
911
- }
912
-
913
- // Check if migration conditions are met
914
- const migrationCheck = checkMigrationConditions(configPath);
915
- if (!migrationCheck.allowed) {
916
- MessageFormatter.error(
917
- `Migration not allowed: ${migrationCheck.reason}`,
918
- undefined,
919
- { prefix: "Migration" }
920
- );
921
- MessageFormatter.info("Migration requirements:", {
922
- prefix: "Migration",
923
- });
924
- MessageFormatter.info(
925
- " • Configuration must be loaded (use --config or have .appwrite/ folder)",
926
- { prefix: "Migration" }
927
- );
928
- MessageFormatter.info(
929
- " • collections/ folder must exist with YAML files",
930
- { prefix: "Migration" }
931
- );
932
- MessageFormatter.info(
933
- " • tables/ folder must not exist or be empty",
934
- { prefix: "Migration" }
935
- );
936
- return;
937
- }
938
-
939
- const { migrateCollectionsToTables } = await import(
940
- "./config/configMigration.js"
941
- );
942
-
943
- MessageFormatter.info("Starting collections to tables migration...", {
944
- prefix: "Migration",
945
- });
946
- const result = migrateCollectionsToTables(controller.config, {
947
- strategy: "full_migration",
948
- validateResult: true,
949
- dryRun: false,
950
- });
951
-
952
- if (result.success) {
953
- operationStats.migratedCollections = result.changes.length;
954
- MessageFormatter.success(
955
- "Collections migration completed successfully",
956
- { prefix: "Migration" }
957
- );
958
- } else {
959
- MessageFormatter.error(
960
- `Migration failed: ${result.errors.join(", ")}`,
961
- undefined,
962
- { prefix: "Migration" }
963
- );
964
- process.exit(1);
965
- }
966
- } catch (error) {
967
- MessageFormatter.error(
968
- "Migration failed",
969
- error instanceof Error ? error : new Error(String(error)),
970
- { prefix: "Migration" }
971
- );
972
- process.exit(1);
973
- }
974
- return;
975
- }
976
-
977
- if (!controller.config) {
978
- // Provide better guidance based on available authentication methods
979
- const availableSessions = getAvailableSessions();
980
-
981
- if (availableSessions.length > 0) {
982
- MessageFormatter.error("No Appwrite configuration found", undefined, {
983
- prefix: "CLI",
984
- });
985
- MessageFormatter.info("Available authentication options:", {
986
- prefix: "Auth",
987
- });
988
- MessageFormatter.info("• Session authentication: Add --session flag", {
989
- prefix: "Auth",
990
- });
991
- MessageFormatter.info(
992
- "• API key authentication: Add --apiKey YOUR_API_KEY",
993
- { prefix: "Auth" }
994
- );
995
- MessageFormatter.info(
996
- `• Available sessions: ${availableSessions
997
- .map((s) => `${s.projectId} (${s.email || "unknown"})`)
998
- .join(", ")}`,
999
- { prefix: "Auth" }
1000
- );
1001
- } else {
1002
- MessageFormatter.error("No Appwrite configuration found", undefined, {
1003
- prefix: "CLI",
1004
- });
1005
- MessageFormatter.info("Authentication options:", { prefix: "Auth" });
1006
- MessageFormatter.info(
1007
- "• Login with Appwrite CLI: Run 'appwrite login' then use --session flag",
1008
- { prefix: "Auth" }
1009
- );
1010
- MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", {
1011
- prefix: "Auth",
1012
- });
1013
- MessageFormatter.info(
1014
- "• Create config file: Run with --setup to initialize project configuration",
1015
- { prefix: "Auth" }
1016
- );
1017
- }
1018
- return;
1019
- }
1020
-
1021
- const parsedArgv = argv;
1022
-
1023
- // List backups if requested
1024
- if (parsedArgv.listBackups) {
1025
- const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
1026
- const { listBackups } = await import("./shared/backupTracking.js");
1027
-
1028
- if (!controller.config) {
1029
- MessageFormatter.error("No Appwrite configuration found", undefined, {
1030
- prefix: "Backups",
1031
- });
1032
- return;
1033
- }
1034
-
1035
- const { adapter } = await AdapterFactory.create({
1036
- appwriteEndpoint: controller.config.appwriteEndpoint,
1037
- appwriteProject: controller.config.appwriteProject,
1038
- appwriteKey: controller.config.appwriteKey,
1039
- });
1040
-
1041
- const databases = parsedArgv.dbIds
1042
- ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
1043
- : await fetchAllDatabases(controller.database!);
1044
-
1045
- if (!databases || databases.length === 0) {
1046
- MessageFormatter.info("No databases found", { prefix: "Backups" });
1047
- return;
1048
- }
1049
-
1050
- for (const db of databases!) {
1051
- const backups = await listBackups(adapter, db.$id);
1052
-
1053
- MessageFormatter.info(
1054
- `\nBackups for database: ${db.name} (${db.$id})`,
1055
- { prefix: "Backups" }
1056
- );
1057
-
1058
- if (backups.length === 0) {
1059
- MessageFormatter.info(" No backups found", { prefix: "Backups" });
1060
- } else {
1061
- backups.forEach((backup, index) => {
1062
- const date = new Date(backup.$createdAt).toLocaleString();
1063
- const size = MessageFormatter.formatBytes(backup.sizeBytes);
1064
- MessageFormatter.info(
1065
- ` ${
1066
- index + 1
1067
- }. ${date} - ${backup.format.toUpperCase()} - ${size} - ${
1068
- backup.collections
1069
- } collections, ${backup.documents} documents`,
1070
- { prefix: "Backups" }
1071
- );
1072
- });
1073
- }
1074
- }
1075
-
1076
- return;
1077
- }
1078
-
1079
- const options: SetupOptions = {
1080
- databases: parsedArgv.dbIds
1081
- ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
1082
- : undefined,
1083
- collections: parsedArgv.collectionIds?.split(","),
1084
- doBackup: parsedArgv.backup,
1085
- wipeDatabase: parsedArgv.wipe === "all" || parsedArgv.wipe === "docs",
1086
- wipeDocumentStorage:
1087
- parsedArgv.wipe === "all" || parsedArgv.wipe === "storage",
1088
- wipeUsers: parsedArgv.wipe === "all" || parsedArgv.wipe === "users",
1089
- generateSchemas: parsedArgv.generate,
1090
- importData: parsedArgv.import,
1091
- shouldWriteFile: parsedArgv.writeData,
1092
- wipeCollections: parsedArgv.wipeCollections,
1093
- transferUsers: parsedArgv.transferUsers,
1094
- };
1095
-
1096
- if (parsedArgv.updateFunctionSpec) {
1097
- if (!parsedArgv.functionId || !parsedArgv.specification) {
1098
- throw new Error(
1099
- "Function ID and specification are required for updating function specs"
1100
- );
1101
- }
1102
- MessageFormatter.info(
1103
- `Updating function specification for ${parsedArgv.functionId} to ${parsedArgv.specification}`,
1104
- { prefix: "Functions" }
1105
- );
1106
- const specifications = await listSpecifications(
1107
- controller.appwriteServer!
1108
- );
1109
- if (
1110
- !specifications.specifications.some(
1111
- (s: { slug: string }) => s.slug === parsedArgv.specification
1112
- )
1113
- ) {
1114
- MessageFormatter.error(
1115
- `Specification ${parsedArgv.specification} not found`,
1116
- undefined,
1117
- { prefix: "Functions" }
1118
- );
1119
- return;
1120
- }
1121
- await controller.updateFunctionSpecifications(
1122
- parsedArgv.functionId,
1123
- parsedArgv.specification as Specification
1124
- );
1125
- }
1126
-
1127
- // Add default databases if not specified (only if we need them for operations)
1128
- const needsDatabases =
1129
- options.doBackup ||
1130
- options.wipeDatabase ||
1131
- options.wipeDocumentStorage ||
1132
- options.wipeUsers ||
1133
- options.wipeCollections ||
1134
- options.importData ||
1135
- parsedArgv.sync ||
1136
- parsedArgv.transfer;
1137
-
1138
- if (
1139
- needsDatabases &&
1140
- (!options.databases || options.databases.length === 0)
1141
- ) {
1142
- const allDatabases = await fetchAllDatabases(controller.database!);
1143
- options.databases = allDatabases;
1144
- }
1145
-
1146
- // Add default collections if not specified
1147
- if (!options.collections || options.collections.length === 0) {
1148
- if (controller.config && controller.config.collections) {
1149
- options.collections = controller.config.collections.map(
1150
- (c: any) => c.name
1151
- );
1152
- } else {
1153
- options.collections = [];
1154
- }
1155
- }
1156
-
1157
- // Comprehensive backup (all databases + all buckets)
1158
- if (parsedArgv.comprehensiveBackup) {
1159
- const { comprehensiveBackup } = await import(
1160
- "./backups/operations/comprehensiveBackup.js"
1161
- );
1162
- const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
1163
-
1164
- // Get tracking database ID (interactive prompt if not specified)
1165
- let trackingDatabaseId = parsedArgv.trackingDatabaseId;
1166
-
1167
- if (!trackingDatabaseId) {
1168
- // Fetch all databases for selection
1169
- const allDatabases = await fetchAllDatabases(controller.database!);
1170
-
1171
- if (allDatabases.length === 0) {
1172
- MessageFormatter.error(
1173
- "No databases found. Cannot create comprehensive backup without a tracking database.",
1174
- undefined,
1175
- { prefix: "Backup" }
1176
- );
1177
- return;
1178
- }
1179
-
1180
- if (allDatabases.length === 1) {
1181
- trackingDatabaseId = allDatabases[0].$id;
1182
- MessageFormatter.info(
1183
- `Using only available database for tracking: ${allDatabases[0].name} (${trackingDatabaseId})`,
1184
- { prefix: "Backup" }
1185
- );
1186
- } else {
1187
- // Interactive selection
1188
- const inquirer = (await import("inquirer")).default;
1189
- const answer = await inquirer.prompt([
1190
- {
1191
- type: "list",
1192
- name: "trackingDb",
1193
- message: "Select database to store backup tracking metadata:",
1194
- choices: allDatabases.map((db) => ({
1195
- name: `${db.name} (${db.$id})`,
1196
- value: db.$id,
1197
- })),
1198
- },
1199
- ]);
1200
- trackingDatabaseId = answer.trackingDb;
1201
- }
1202
- }
1203
-
1204
- // Ensure trackingDatabaseId is defined before proceeding
1205
- if (!trackingDatabaseId) {
1206
- throw new Error(
1207
- "Tracking database ID is required for comprehensive backup"
1208
- );
1209
- }
1210
-
1211
- MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, {
1212
- prefix: "Backup",
1213
- });
1214
-
1215
- // Create adapter for backup tracking
1216
- const { adapter } = await AdapterFactory.create({
1217
- appwriteEndpoint: controller.config!.appwriteEndpoint,
1218
- appwriteProject: controller.config!.appwriteProject,
1219
- appwriteKey: controller.config!.appwriteKey,
1220
- sessionCookie: controller.config!.sessionCookie,
1221
- });
1222
-
1223
- const result = await comprehensiveBackup(
1224
- controller.config!,
1225
- controller.database!,
1226
- controller.storage!,
1227
- adapter,
1228
- {
1229
- trackingDatabaseId,
1230
- backupFormat: parsedArgv.backupFormat || "zip",
1231
- parallelDownloads: parsedArgv.parallelDownloads || 10,
1232
- onProgress: (message) => {
1233
- MessageFormatter.info(message, { prefix: "Backup" });
1234
- },
1235
- }
1236
- );
1237
-
1238
- operationStats.comprehensiveBackup = 1;
1239
- operationStats.databasesBackedUp = result.databaseBackups.length;
1240
- operationStats.bucketsBackedUp = result.bucketBackups.length;
1241
- operationStats.totalBackupSize = result.totalSizeBytes;
1242
-
1243
- if (result.status === "completed") {
1244
- MessageFormatter.success(
1245
- `Comprehensive backup completed successfully (ID: ${result.backupId})`,
1246
- { prefix: "Backup" }
1247
- );
1248
- } else if (result.status === "partial") {
1249
- MessageFormatter.warning(
1250
- `Comprehensive backup completed with errors (ID: ${result.backupId})`,
1251
- { prefix: "Backup" }
1252
- );
1253
- result.errors.forEach((err) =>
1254
- MessageFormatter.warning(err, { prefix: "Backup" })
1255
- );
1256
- } else {
1257
- MessageFormatter.error(
1258
- `Comprehensive backup failed (ID: ${result.backupId})`,
1259
- undefined,
1260
- { prefix: "Backup" }
1261
- );
1262
- result.errors.forEach((err) =>
1263
- MessageFormatter.error(err, undefined, { prefix: "Backup" })
1264
- );
1265
- }
1266
- }
1267
-
1268
- if (options.doBackup && options.databases) {
1269
- MessageFormatter.info(
1270
- `Creating backups for ${options.databases.length} database(s) in ${parsedArgv.backupFormat} format`,
1271
- { prefix: "Backup" }
1272
- );
1273
- for (const db of options.databases) {
1274
- await controller.backupDatabase(db, parsedArgv.backupFormat || "json");
1275
- }
1276
- operationStats.backups = options.databases.length;
1277
- MessageFormatter.success(
1278
- `Backup completed for ${options.databases.length} database(s)`,
1279
- { prefix: "Backup" }
1280
- );
1281
- }
1282
-
1283
- if (
1284
- options.wipeDatabase ||
1285
- options.wipeDocumentStorage ||
1286
- options.wipeUsers ||
1287
- options.wipeCollections
1288
- ) {
1289
- // Confirm destructive operations
1290
- const databaseNames = options.databases?.map((db) => db.name) || [];
1291
- const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(
1292
- databaseNames,
1293
- {
1294
- includeStorage: options.wipeDocumentStorage,
1295
- includeUsers: options.wipeUsers,
1296
- }
1297
- );
1298
-
1299
- if (!confirmed) {
1300
- MessageFormatter.info("Operation cancelled by user", { prefix: "CLI" });
1301
- return;
1302
- }
1303
-
1304
- let wipeStats = { databases: 0, collections: 0, users: 0, buckets: 0 };
1305
-
1306
- if (parsedArgv.wipe === "all") {
1307
- if (options.databases) {
1308
- for (const db of options.databases) {
1309
- await controller.wipeDatabase(db, true); // true to wipe associated buckets
1310
- }
1311
- wipeStats.databases = options.databases.length;
1312
- }
1313
- await controller.wipeUsers();
1314
- wipeStats.users = 1;
1315
- } else if (parsedArgv.wipe === "docs") {
1316
- if (options.databases) {
1317
- for (const db of options.databases) {
1318
- await controller.wipeBucketFromDatabase(db);
1319
- }
1320
- wipeStats.databases = options.databases.length;
1321
- }
1322
- if (parsedArgv.bucketIds) {
1323
- const bucketIds = parsedArgv.bucketIds.split(",");
1324
- for (const bucketId of bucketIds) {
1325
- await controller.wipeDocumentStorage(bucketId);
1326
- }
1327
- wipeStats.buckets = bucketIds.length;
1328
- }
1329
- } else if (parsedArgv.wipe === "users") {
1330
- await controller.wipeUsers();
1331
- wipeStats.users = 1;
1332
- }
1333
-
1334
- // Handle specific collection wipes
1335
- if (options.wipeCollections && options.databases) {
1336
- for (const db of options.databases) {
1337
- const dbCollections = await fetchAllCollections(
1338
- db.$id,
1339
- controller.database!
1340
- );
1341
- const collectionsToWipe = dbCollections.filter((c) =>
1342
- options.collections!.includes(c.$id)
1343
- );
1344
-
1345
- // Confirm collection wipe
1346
- const collectionNames = collectionsToWipe.map((c) => c.name);
1347
- const collectionConfirmed =
1348
- await ConfirmationDialogs.confirmCollectionWipe(
1349
- db.name,
1350
- collectionNames
1351
- );
1352
-
1353
- if (collectionConfirmed) {
1354
- for (const collection of collectionsToWipe) {
1355
- await controller.wipeCollection(db, collection);
1356
- }
1357
- wipeStats.collections += collectionsToWipe.length;
1358
- }
1359
- }
1360
- }
1361
-
1362
- // Show wipe operation summary
1363
- if (
1364
- wipeStats.databases > 0 ||
1365
- wipeStats.collections > 0 ||
1366
- wipeStats.users > 0 ||
1367
- wipeStats.buckets > 0
1368
- ) {
1369
- operationStats.wipedDatabases = wipeStats.databases;
1370
- operationStats.wipedCollections = wipeStats.collections;
1371
- operationStats.wipedUsers = wipeStats.users;
1372
- operationStats.wipedBuckets = wipeStats.buckets;
1373
- }
1374
- }
1375
-
1376
- if (parsedArgv.push) {
1377
- await controller.init();
1378
- if (!controller.database || !controller.config) {
1379
- MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
1380
- return;
1381
- }
1382
-
1383
- // Fetch available DBs
1384
- const availableDatabases = await fetchAllDatabases(controller.database);
1385
- if (availableDatabases.length === 0) {
1386
- MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
1387
- return;
1388
- }
1389
-
1390
- // Determine selected DBs
1391
- let selectedDbIds: string[] = [];
1392
- if (parsedArgv.dbIds) {
1393
- selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
1394
- } else {
1395
- selectedDbIds = await SelectionDialogs.selectDatabases(
1396
- availableDatabases,
1397
- controller.config.databases || [],
1398
- { showSelectAll: false, allowNewOnly: false, defaultSelected: [] }
1399
- );
1400
- }
1401
-
1402
- if (selectedDbIds.length === 0) {
1403
- MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
1404
- return;
1405
- }
1406
-
1407
- // Build DatabaseSelection[] with tableIds per DB
1408
- const databaseSelections: DatabaseSelection[] = [];
1409
- const allConfigItems = controller.config.collections || controller.config.tables || [];
1410
- let lastSelectedTableIds: string[] | null = null;
1411
-
1412
- for (const dbId of selectedDbIds) {
1413
- const db = availableDatabases.find(d => d.$id === dbId);
1414
- if (!db) continue;
1415
-
1416
- // Filter config items eligible for this DB according to databaseId/databaseIds rule
1417
- const eligibleConfigItems = (allConfigItems as any[]).filter(item => {
1418
- const one = item.databaseId as string | undefined;
1419
- const many = item.databaseIds as string[] | undefined;
1420
- if (Array.isArray(many) && many.length > 0) return many.includes(dbId);
1421
- if (one) return one === dbId;
1422
- return true; // eligible everywhere if unspecified
1423
- });
1424
-
1425
- // Fetch available tables from remote for selection context
1426
- const availableTables = await fetchAllCollections(dbId, controller.database);
1427
-
1428
- // Determine selected table IDs
1429
- let selectedTableIds: string[] = [];
1430
- if (parsedArgv.collectionIds) {
1431
- // Non-interactive: respect provided table IDs as-is (apply to each selected DB)
1432
- selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
1433
- } else {
1434
- // If we have a previous selection, offer to reuse it
1435
- if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
1436
- const inquirer = (await import("inquirer")).default;
1437
- const { reuseMode } = await inquirer.prompt([
1438
- {
1439
- type: "list",
1440
- name: "reuseMode",
1441
- message: `How do you want to select tables for ${db.name}?`,
1442
- choices: [
1443
- { name: `Use same selection as previous (${lastSelectedTableIds.length} items)`, value: "same" },
1444
- { name: `Filter by this database (manual select)`, value: "filter" },
1445
- { name: `Show all available in this database (manual select)`, value: "all" }
1446
- ],
1447
- default: "same"
1448
- }
1449
- ]);
1450
-
1451
- if (reuseMode === "same") {
1452
- selectedTableIds = [...lastSelectedTableIds];
1453
- } else if (reuseMode === "all") {
1454
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1455
- dbId,
1456
- db.name,
1457
- availableTables,
1458
- allConfigItems as any[],
1459
- { showSelectAll: false, allowNewOnly: false, defaultSelected: lastSelectedTableIds }
1460
- );
1461
- } else {
1462
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1463
- dbId,
1464
- db.name,
1465
- availableTables,
1466
- eligibleConfigItems,
1467
- { showSelectAll: false, allowNewOnly: true, defaultSelected: lastSelectedTableIds }
1468
- );
1469
- }
1470
- } else {
1471
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1472
- dbId,
1473
- db.name,
1474
- availableTables,
1475
- eligibleConfigItems,
1476
- { showSelectAll: false, allowNewOnly: true, defaultSelected: [] }
1477
- );
1478
- }
1479
- }
1480
-
1481
- databaseSelections.push({
1482
- databaseId: db.$id,
1483
- databaseName: db.name,
1484
- tableIds: selectedTableIds,
1485
- tableNames: [],
1486
- isNew: false,
1487
- });
1488
- if (!parsedArgv.collectionIds) {
1489
- lastSelectedTableIds = selectedTableIds;
1490
- }
1491
- }
1492
-
1493
- if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
1494
- MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
1495
- return;
1496
- }
1497
-
1498
- const pushSummary: Record<string, string | number | string[]> = {
1499
- databases: databaseSelections.length,
1500
- collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
1501
- details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
1502
- };
1503
- // Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
1504
- if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
1505
- const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
1506
- if (!confirmed) {
1507
- MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
1508
- return;
1509
- }
1510
- }
1511
-
1512
- await controller.selectivePush(databaseSelections, []);
1513
- operationStats.pushedDatabases = databaseSelections.length;
1514
- operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
1515
- } else if (parsedArgv.sync) {
1516
- // Enhanced SYNC: Pull from remote with intelligent configuration detection
1517
- if (parsedArgv.autoSync) {
1518
- // Legacy behavior: sync everything without prompts
1519
- MessageFormatter.info("Using auto-sync mode (legacy behavior)", { prefix: "Sync" });
1520
- const databases =
1521
- options.databases || (await fetchAllDatabases(controller.database!));
1522
- await controller.synchronizeConfigurations(databases);
1523
- operationStats.syncedDatabases = databases.length;
1524
- } else {
1525
- // Enhanced sync flow with selection dialogs
1526
- const syncResult = await performEnhancedSync(controller, parsedArgv);
1527
- if (syncResult) {
1528
- operationStats.syncedDatabases = syncResult.databases.length;
1529
- operationStats.syncedCollections = syncResult.totalTables;
1530
- operationStats.syncedBuckets = syncResult.buckets.length;
1531
- }
1532
- }
1533
- }
1534
-
1535
- if (options.generateSchemas) {
1536
- await controller.generateSchemas();
1537
- operationStats.generatedSchemas = 1;
1538
- }
1539
-
1540
- if (options.importData) {
1541
- await controller.importData(options);
1542
- operationStats.importCompleted = 1;
1543
- }
1544
-
1545
- if (parsedArgv.transfer) {
1546
- const isRemote = !!parsedArgv.remoteEndpoint;
1547
- let fromDb, toDb: Models.Database | undefined;
1548
- let targetDatabases: Databases | undefined;
1549
- let targetStorage: Storage | undefined;
1550
-
1551
- // Only fetch databases if database IDs are provided
1552
- if (parsedArgv.fromDbId && parsedArgv.toDbId) {
1553
- MessageFormatter.info(
1554
- `Starting database transfer from ${parsedArgv.fromDbId} to ${parsedArgv.toDbId}`,
1555
- { prefix: "Transfer" }
1556
- );
1557
- fromDb = (
1558
- await controller.getDatabasesByIds([parsedArgv.fromDbId])
1559
- )?.[0];
1560
- if (!fromDb) {
1561
- MessageFormatter.error("Source database not found", undefined, {
1562
- prefix: "Transfer",
1563
- });
1564
- return;
1565
- }
1566
- if (isRemote) {
1567
- if (
1568
- !parsedArgv.remoteEndpoint ||
1569
- !parsedArgv.remoteProjectId ||
1570
- !parsedArgv.remoteApiKey
1571
- ) {
1572
- throw new Error("Remote transfer details are missing");
1573
- }
1574
- const remoteClient = getClient(
1575
- parsedArgv.remoteEndpoint,
1576
- parsedArgv.remoteProjectId,
1577
- parsedArgv.remoteApiKey
1578
- );
1579
- targetDatabases = new Databases(remoteClient);
1580
- targetStorage = new Storage(remoteClient);
1581
- const remoteDbs = await fetchAllDatabases(targetDatabases);
1582
- toDb = remoteDbs.find((db) => db.$id === parsedArgv.toDbId);
1583
- if (!toDb) {
1584
- MessageFormatter.error("Target database not found", undefined, {
1585
- prefix: "Transfer",
1586
- });
1587
- return;
1588
- }
1589
- } else {
1590
- toDb = (await controller.getDatabasesByIds([parsedArgv.toDbId]))?.[0];
1591
- if (!toDb) {
1592
- MessageFormatter.error("Target database not found", undefined, {
1593
- prefix: "Transfer",
1594
- });
1595
- return;
1596
- }
1597
- }
1598
-
1599
- if (!fromDb || !toDb) {
1600
- MessageFormatter.error(
1601
- "Source or target database not found",
1602
- undefined,
1603
- { prefix: "Transfer" }
1604
- );
1605
- return;
1606
- }
1607
- }
1608
-
1609
- // Handle storage setup
1610
- let sourceBucket, targetBucket;
1611
- if (parsedArgv.fromBucketId) {
1612
- sourceBucket = await controller.storage?.getBucket(
1613
- parsedArgv.fromBucketId
1614
- );
1615
- }
1616
- if (parsedArgv.toBucketId) {
1617
- if (isRemote) {
1618
- if (!targetStorage) {
1619
- const remoteClient = getClient(
1620
- parsedArgv.remoteEndpoint!,
1621
- parsedArgv.remoteProjectId!,
1622
- parsedArgv.remoteApiKey!
1623
- );
1624
- targetStorage = new Storage(remoteClient);
1625
- }
1626
- targetBucket = await targetStorage?.getBucket(parsedArgv.toBucketId);
1627
- } else {
1628
- targetBucket = await controller.storage?.getBucket(
1629
- parsedArgv.toBucketId
1630
- );
1631
- }
1632
- }
1633
-
1634
- // Validate that at least one transfer type is specified
1635
- if (!fromDb && !sourceBucket && !options.transferUsers) {
1636
- throw new Error("No source database or bucket specified for transfer");
1637
- }
1638
-
1639
- const transferOptions: TransferOptions = {
1640
- isRemote,
1641
- fromDb,
1642
- targetDb: toDb,
1643
- transferEndpoint: parsedArgv.remoteEndpoint,
1644
- transferProject: parsedArgv.remoteProjectId,
1645
- transferKey: parsedArgv.remoteApiKey,
1646
- sourceBucket: sourceBucket,
1647
- targetBucket: targetBucket,
1648
- transferUsers: options.transferUsers,
1649
- };
1650
-
1651
- await controller.transferData(transferOptions);
1652
- operationStats.transfers = 1;
1653
- }
1654
-
1655
- // Show final operation summary if any operations were performed
1656
- if (Object.keys(operationStats).length > 0) {
1657
- const duration = Date.now() - startTime;
1658
- MessageFormatter.operationSummary(
1659
- "CLI Operations",
1660
- operationStats,
1661
- duration
1662
- );
1663
- }
1664
- }
1665
- }
1666
-
1667
- main().catch((error) => {
1668
- MessageFormatter.error("CLI execution failed", error, { prefix: "CLI" });
1669
- process.exit(1);
1670
- });
1
+ #!/usr/bin/env node
2
+ import yargs from "yargs";
3
+ import { type ArgumentsCamelCase } from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { InteractiveCLI } from "./interactiveCLI.js";
6
+ import { UtilsController, type SetupOptions } from "./utilsController.js";
7
+ import type { TransferOptions } from "./migrations/transfer.js";
8
+ import { Databases, Storage, type Models } from "node-appwrite";
9
+ import { getClient } from "appwrite-utils-helpers";
10
+ import { fetchAllDatabases } from "./databases/methods.js";
11
+ import { setupDirsFiles } from "./utils/setupFiles.js";
12
+ import { fetchAllCollections } from "./collections/methods.js";
13
+ import type { Specification } from "appwrite-utils";
14
+ import chalk from "chalk";
15
+ import { listSpecifications } from "./functions/methods.js";
16
+ import { MessageFormatter, logger, AuthenticationError } from "appwrite-utils-helpers";
17
+ import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
18
+ import { SelectionDialogs } from "./shared/selectionDialogs.js";
19
+ import type { SyncSelectionSummary, DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
20
+ import path from "path";
21
+ import fs from "fs";
22
+ import { createRequire } from "node:module";
23
+
24
+ const require = createRequire(import.meta.url);
25
+ if (!(globalThis as any).require) {
26
+ (globalThis as any).require = require;
27
+ }
28
+
29
+ interface CliOptions {
30
+ config?: string;
31
+ appwriteConfig?: boolean;
32
+ it?: boolean;
33
+ dbIds?: string;
34
+ collectionIds?: string;
35
+ bucketIds?: string;
36
+ wipe?: "all" | "storage" | "docs" | "users";
37
+ wipeCollections?: boolean;
38
+ generate?: boolean;
39
+ import?: boolean;
40
+ backup?: boolean;
41
+ backupFormat?: "json" | "zip";
42
+ comprehensiveBackup?: boolean;
43
+ trackingDatabaseId?: string;
44
+ parallelDownloads?: number;
45
+ writeData?: boolean;
46
+ push?: boolean;
47
+ sync?: boolean;
48
+ endpoint?: string;
49
+ projectId?: string;
50
+ apiKey?: string;
51
+ transfer?: boolean;
52
+ transferUsers?: boolean;
53
+ fromDbId?: string;
54
+ toDbId?: string;
55
+ fromCollectionId?: string;
56
+ toCollectionId?: string;
57
+ fromBucketId?: string;
58
+ toBucketId?: string;
59
+ remoteEndpoint?: string;
60
+ remoteProjectId?: string;
61
+ remoteApiKey?: string;
62
+ setup?: boolean;
63
+ updateFunctionSpec?: boolean;
64
+ functionId?: string;
65
+ specification?: string;
66
+ migrateConfig?: boolean;
67
+ generateConstants?: boolean;
68
+ constantsLanguages?: string;
69
+ constantsOutput?: string;
70
+ migrateCollectionsToTables?: boolean;
71
+ useSession?: boolean;
72
+ sessionCookie?: string;
73
+ listBackups?: boolean;
74
+ autoSync?: boolean;
75
+ selectBuckets?: boolean;
76
+ // New schema/constant CLI flags
77
+ generateSchemas?: boolean;
78
+ schemaFormat?: 'zod' | 'json' | 'pydantic' | 'both' | 'all';
79
+ schemaOutDir?: string;
80
+ constantsInclude?: string;
81
+ // Direct file import
82
+ importFile?: string;
83
+ targetDb?: string;
84
+ targetTable?: string;
85
+ }
86
+
87
+ type ParsedArgv = ArgumentsCamelCase<CliOptions>;
88
+
89
+ /**
90
+ * Enhanced sync function with intelligent configuration detection and selection dialogs
91
+ */
92
+ async function performEnhancedSync(
93
+ controller: UtilsController,
94
+ parsedArgv: ParsedArgv
95
+ ): Promise<SyncSelectionSummary | null> {
96
+ try {
97
+ MessageFormatter.banner("Enhanced Sync", "Intelligent configuration detection and selection");
98
+
99
+ if (!controller.config) {
100
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Sync" });
101
+ return null;
102
+ }
103
+
104
+ // Get all available databases from remote
105
+ const availableDatabases = await fetchAllDatabases(controller.database!);
106
+ if (availableDatabases.length === 0) {
107
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Sync" });
108
+ return null;
109
+ }
110
+
111
+ // Get existing configuration
112
+ const configuredDatabases = controller.config.databases || [];
113
+ const configuredBuckets = controller.config.buckets || [];
114
+
115
+ // Check if we have existing configuration
116
+ const hasExistingConfig = configuredDatabases.length > 0 || configuredBuckets.length > 0;
117
+
118
+ let syncExisting = false;
119
+ let modifyConfiguration = true;
120
+
121
+ if (hasExistingConfig) {
122
+ // Prompt about existing configuration
123
+ const response = await SelectionDialogs.promptForExistingConfig([
124
+ ...configuredDatabases,
125
+ ...configuredBuckets
126
+ ]);
127
+ syncExisting = response.syncExisting;
128
+ modifyConfiguration = response.modifyConfiguration;
129
+
130
+ if (syncExisting && !modifyConfiguration) {
131
+ // Just sync existing configuration without changes
132
+ MessageFormatter.info("Syncing existing configuration without modifications", { prefix: "Sync" });
133
+
134
+ // Convert configured databases to DatabaseSelection format
135
+ const databaseSelections: DatabaseSelection[] = configuredDatabases.map(db => ({
136
+ databaseId: db.$id,
137
+ databaseName: db.name,
138
+ tableIds: [], // Tables will be populated from collections config
139
+ tableNames: [],
140
+ isNew: false
141
+ }));
142
+
143
+ // Convert configured buckets to BucketSelection format
144
+ const bucketSelections: BucketSelection[] = configuredBuckets.map(bucket => ({
145
+ bucketId: bucket.$id,
146
+ bucketName: bucket.name,
147
+ databaseId: undefined,
148
+ databaseName: undefined,
149
+ isNew: false
150
+ }));
151
+
152
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
153
+ databaseSelections,
154
+ bucketSelections
155
+ );
156
+
157
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
158
+ if (!confirmed) {
159
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
160
+ return null;
161
+ }
162
+
163
+ // Perform sync with existing configuration (pull from remote)
164
+ await controller.selectivePull(databaseSelections, bucketSelections);
165
+ return selectionSummary;
166
+ }
167
+ }
168
+
169
+ if (!modifyConfiguration) {
170
+ MessageFormatter.info("No configuration changes requested", { prefix: "Sync" });
171
+ return null;
172
+ }
173
+
174
+ // Allow new items selection based on user choice
175
+ const allowNewOnly = !syncExisting;
176
+
177
+ // Select databases
178
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
179
+ availableDatabases,
180
+ configuredDatabases,
181
+ {
182
+ showSelectAll: false,
183
+ allowNewOnly,
184
+ defaultSelected: []
185
+ }
186
+ );
187
+
188
+ if (selectedDatabaseIds.length === 0) {
189
+ MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
190
+ return null;
191
+ }
192
+
193
+ // For each selected database, get available tables and select them
194
+ const tableSelectionsMap = new Map<string, string[]>();
195
+ const availableTablesMap = new Map<string, any[]>();
196
+
197
+ for (const databaseId of selectedDatabaseIds) {
198
+ const database = availableDatabases.find(db => db.$id === databaseId)!;
199
+
200
+ SelectionDialogs.showProgress(`Fetching tables for database: ${database.name}`);
201
+
202
+ // Get available tables from remote
203
+ const availableTables = await fetchAllCollections(databaseId, controller.database!);
204
+ availableTablesMap.set(databaseId, availableTables);
205
+
206
+ // Get configured tables for this database
207
+ // Note: Collections are stored globally in the config, not per database
208
+ const configuredTables = controller.config.collections || [];
209
+
210
+ // Select tables for this database
211
+ const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
212
+ databaseId,
213
+ database.name,
214
+ availableTables,
215
+ configuredTables,
216
+ {
217
+ showSelectAll: false,
218
+ allowNewOnly,
219
+ defaultSelected: []
220
+ }
221
+ );
222
+
223
+ tableSelectionsMap.set(databaseId, selectedTableIds);
224
+
225
+ if (selectedTableIds.length === 0) {
226
+ MessageFormatter.warning(`No tables selected for database: ${database.name}`, { prefix: "Sync" });
227
+ }
228
+ }
229
+
230
+ // Select buckets
231
+ let selectedBucketIds: string[] = [];
232
+
233
+ // Get available buckets from remote
234
+ if (controller.storage) {
235
+ try {
236
+ // Note: We need to implement fetchAllBuckets or use storage.listBuckets
237
+ // For now, we'll use configured buckets as available
238
+ SelectionDialogs.showProgress("Fetching storage buckets...");
239
+
240
+ // Create a mock availableBuckets array - in real implementation,
241
+ // you'd fetch this from the Appwrite API
242
+ const availableBuckets = configuredBuckets; // Placeholder
243
+
244
+ selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
245
+ selectedDatabaseIds,
246
+ availableBuckets,
247
+ configuredBuckets,
248
+ {
249
+ showSelectAll: false,
250
+ allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
251
+ groupByDatabase: true,
252
+ defaultSelected: []
253
+ }
254
+ );
255
+ } catch (error) {
256
+ MessageFormatter.warning("Could not fetch storage buckets", { prefix: "Sync" });
257
+ logger.warn("Failed to fetch buckets during sync", { error });
258
+ }
259
+ }
260
+
261
+ // Create selection objects
262
+ const databaseSelections = SelectionDialogs.createDatabaseSelection(
263
+ selectedDatabaseIds,
264
+ availableDatabases,
265
+ tableSelectionsMap,
266
+ configuredDatabases,
267
+ availableTablesMap
268
+ );
269
+
270
+ const bucketSelections = SelectionDialogs.createBucketSelection(
271
+ selectedBucketIds,
272
+ [], // availableBuckets - would be populated from API
273
+ configuredBuckets,
274
+ availableDatabases
275
+ );
276
+
277
+ // Show final confirmation
278
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
279
+ databaseSelections,
280
+ bucketSelections
281
+ );
282
+
283
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
284
+ if (!confirmed) {
285
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
286
+ return null;
287
+ }
288
+
289
+ // Perform the selective sync (pull from remote)
290
+ await controller.selectivePull(databaseSelections, bucketSelections);
291
+
292
+ MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
293
+ return selectionSummary;
294
+
295
+ } catch (error) {
296
+ SelectionDialogs.showError("Enhanced sync failed", error instanceof Error ? error : new Error(String(error)));
297
+ return null;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Performs selective sync with the given database and bucket selections
303
+ */
304
+
305
+ /**
306
+ * Checks if the migration from collections to tables should be allowed
307
+ * Returns an object with:
308
+ * - allowed: boolean indicating if migration should proceed
309
+ * - reason: string explaining why migration was blocked (if not allowed)
310
+ */
311
+ function checkMigrationConditions(configPath: string): {
312
+ allowed: boolean;
313
+ reason?: string;
314
+ } {
315
+ const collectionsPath = path.join(configPath, "collections");
316
+ const tablesPath = path.join(configPath, "tables");
317
+
318
+ // Check if collections/ folder exists
319
+ if (!fs.existsSync(collectionsPath)) {
320
+ return {
321
+ allowed: false,
322
+ reason:
323
+ "No collections/ folder found. Migration requires existing collections to migrate.",
324
+ };
325
+ }
326
+
327
+ // Check if collections/ folder has YAML files
328
+ const collectionFiles = fs
329
+ .readdirSync(collectionsPath)
330
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
331
+
332
+ if (collectionFiles.length === 0) {
333
+ return {
334
+ allowed: false,
335
+ reason:
336
+ "No YAML files found in collections/ folder. Migration requires existing collection YAML files.",
337
+ };
338
+ }
339
+
340
+ // Check if tables/ folder exists and has YAML files
341
+ if (fs.existsSync(tablesPath)) {
342
+ const tableFiles = fs
343
+ .readdirSync(tablesPath)
344
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
345
+
346
+ if (tableFiles.length > 0) {
347
+ return {
348
+ allowed: false,
349
+ reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`,
350
+ };
351
+ }
352
+ }
353
+
354
+ // All conditions met
355
+ return { allowed: true };
356
+ }
357
+
358
+ const argv = yargs(hideBin(process.argv))
359
+ .option("config", {
360
+ type: "string",
361
+ description: "Path to Appwrite configuration file (appwriteConfig.ts)",
362
+ })
363
+ .option("appwriteConfig", {
364
+ alias: ["appwrite-config", "use-appwrite-config"],
365
+ type: "boolean",
366
+ description: "Prefer loading from appwrite.config.json instead of config.yaml",
367
+ })
368
+ .option("it", {
369
+ alias: ["interactive", "i"],
370
+ type: "boolean",
371
+ description: "Launch interactive CLI mode with guided prompts",
372
+ })
373
+ .option("dbIds", {
374
+ type: "string",
375
+ description:
376
+ "Comma-separated list of database IDs to target (e.g., 'db1,db2,db3')",
377
+ })
378
+ .option("collectionIds", {
379
+ alias: ["collIds", "tableIds", "tables"],
380
+ type: "string",
381
+ description:
382
+ "Comma-separated list of collection/table IDs to target (e.g., 'users,posts')",
383
+ })
384
+ .option("bucketIds", {
385
+ type: "string",
386
+ description: "Comma-separated list of bucket IDs to operate on",
387
+ })
388
+ .option("wipe", {
389
+ choices: ["all", "docs", "users"] as const,
390
+ description:
391
+ "⚠️ DESTRUCTIVE: Wipe data (all: databases+storage+users, docs: documents only, users: user accounts only)",
392
+ })
393
+ .option("wipeCollections", {
394
+ type: "boolean",
395
+ description:
396
+ "⚠️ DESTRUCTIVE: Wipe specific collections/tables (requires --collectionIds or --tableIds)",
397
+ })
398
+ .option("transferUsers", {
399
+ type: "boolean",
400
+ description: "Transfer users between projects",
401
+ })
402
+ .option("generate", {
403
+ type: "boolean",
404
+ description:
405
+ "Generate TypeScript schemas and types from your Appwrite database schemas",
406
+ })
407
+ .option("import", {
408
+ type: "boolean",
409
+ description:
410
+ "Import data from importData/ directory into your Appwrite databases",
411
+ })
412
+ .option("backup", {
413
+ type: "boolean",
414
+ description: "Create a complete backup of your databases and collections",
415
+ })
416
+ .option("backupFormat", {
417
+ type: "string",
418
+ choices: ["json", "zip"] as const,
419
+ default: "json",
420
+ description: "Backup file format (json or zip)",
421
+ })
422
+ .option("listBackups", {
423
+ type: "boolean",
424
+ description: "List all backups for databases",
425
+ })
426
+ .option("comprehensiveBackup", {
427
+ alias: ["comprehensive", "backup-all"],
428
+ type: "boolean",
429
+ description:
430
+ "🚀 Create comprehensive backup of ALL databases and ALL storage buckets",
431
+ })
432
+ .option("trackingDatabaseId", {
433
+ alias: ["tracking-db"],
434
+ type: "string",
435
+ description:
436
+ "Database ID to use for centralized backup tracking (interactive prompt if not specified)",
437
+ })
438
+ .option("parallelDownloads", {
439
+ type: "number",
440
+ default: 10,
441
+ description:
442
+ "Number of parallel file downloads for bucket backups (default: 10)",
443
+ })
444
+ .option("writeData", {
445
+ type: "boolean",
446
+ description:
447
+ "Output converted import data to files for validation before importing",
448
+ })
449
+ .option("push", {
450
+ type: "boolean",
451
+ description:
452
+ "Deploy your local configuration (collections, attributes, indexes) to Appwrite",
453
+ })
454
+ .option("sync", {
455
+ type: "boolean",
456
+ description:
457
+ "Pull and synchronize your local config with the remote Appwrite project schema",
458
+ })
459
+ .option("autoSync", {
460
+ alias: ["auto"],
461
+ type: "boolean",
462
+ description: "Skip prompts and sync all databases, tables, and buckets (current behavior)"
463
+ })
464
+ .option("selectBuckets", {
465
+ type: "boolean",
466
+ description: "Force bucket selection dialog even if buckets are already configured"
467
+ })
468
+ .option("endpoint", {
469
+ type: "string",
470
+ description: "Set the Appwrite endpoint",
471
+ })
472
+ .option("projectId", {
473
+ type: "string",
474
+ description: "Set the Appwrite project ID",
475
+ })
476
+ .option("apiKey", {
477
+ type: "string",
478
+ description: "Set the Appwrite API key",
479
+ })
480
+ .option("transfer", {
481
+ type: "boolean",
482
+ description:
483
+ "Transfer documents and files between databases, collections, or projects",
484
+ })
485
+ .option("fromDbId", {
486
+ alias: ["fromDb", "sourceDbId", "sourceDb"],
487
+ type: "string",
488
+ description: "Source database ID for transfer operations",
489
+ })
490
+ .option("toDbId", {
491
+ alias: ["toDb", "targetDbId", "targetDb"],
492
+ type: "string",
493
+ description: "Target database ID for transfer operations",
494
+ })
495
+ .option("fromCollectionId", {
496
+ alias: ["fromCollId", "fromColl"],
497
+ type: "string",
498
+ description: "Set the source collection ID for transfer",
499
+ })
500
+ .option("toCollectionId", {
501
+ alias: ["toCollId", "toColl"],
502
+ type: "string",
503
+ description: "Set the destination collection ID for transfer",
504
+ })
505
+ .option("fromBucketId", {
506
+ type: "string",
507
+ description: "Set the source bucket ID for transfer",
508
+ })
509
+ .option("toBucketId", {
510
+ type: "string",
511
+ description: "Set the destination bucket ID for transfer",
512
+ })
513
+ .option("remoteEndpoint", {
514
+ type: "string",
515
+ description: "Set the remote Appwrite endpoint for transfer",
516
+ })
517
+ .option("remoteProjectId", {
518
+ type: "string",
519
+ description: "Set the remote Appwrite project ID for transfer",
520
+ })
521
+ .option("remoteApiKey", {
522
+ type: "string",
523
+ description: "Set the remote Appwrite API key for transfer",
524
+ })
525
+ .option("setup", {
526
+ type: "boolean",
527
+ description:
528
+ "Initialize project with configuration files and directory structure",
529
+ })
530
+ .option("updateFunctionSpec", {
531
+ type: "boolean",
532
+ description: "Update function specifications",
533
+ })
534
+ .option("functionId", {
535
+ type: "string",
536
+ description: "Function ID to update",
537
+ })
538
+ .option("specification", {
539
+ type: "string",
540
+ description: "New function specification (e.g., 's-1vcpu-1gb')",
541
+ choices: [
542
+ "s-0.5vcpu-512mb",
543
+ "s-1vcpu-1gb",
544
+ "s-2vcpu-2gb",
545
+ "s-2vcpu-4gb",
546
+ "s-4vcpu-4gb",
547
+ "s-4vcpu-8gb",
548
+ "s-8vcpu-4gb",
549
+ "s-8vcpu-8gb",
550
+ ],
551
+ })
552
+ .option("migrateConfig", {
553
+ alias: ["migrate"],
554
+ type: "boolean",
555
+ description:
556
+ "Migrate appwriteConfig.ts to .appwrite structure with YAML configuration",
557
+ })
558
+ .option("generateConstants", {
559
+ alias: ["constants"],
560
+ type: "boolean",
561
+ description:
562
+ "Generate cross-language constants file with database, collection, bucket, and function IDs",
563
+ })
564
+ .option("constantsLanguages", {
565
+ type: "string",
566
+ description:
567
+ "Comma-separated list of languages for constants (typescript,javascript,python,php,dart,json,env)",
568
+ default: "typescript",
569
+ })
570
+ .option("constantsOutput", {
571
+ type: "string",
572
+ description:
573
+ "Output directory for generated constants files (default: config-folder/constants)",
574
+ default: "auto",
575
+ })
576
+ .option("constantsInclude", {
577
+ type: "string",
578
+ description:
579
+ "Comma-separated categories to include: databases,collections,buckets,functions",
580
+ })
581
+ .option("generateSchemas", {
582
+ type: "boolean",
583
+ description: "Generate schemas/models without interactive prompts",
584
+ })
585
+ .option("schemaFormat", {
586
+ type: "string",
587
+ choices: ["zod", "json", "pydantic", "both", "all"],
588
+ description: "Schema format: zod, json, pydantic, both (zod+json), or all",
589
+ })
590
+ .option("schemaOutDir", {
591
+ type: "string",
592
+ description: "Output directory for generated schemas (absolute path respected)",
593
+ })
594
+ .option("migrateCollectionsToTables", {
595
+ alias: ["migrate-collections"],
596
+ type: "boolean",
597
+ description:
598
+ "Migrate collections to tables format for TablesDB API compatibility",
599
+ })
600
+ .option("useSession", {
601
+ alias: ["session"],
602
+ type: "boolean",
603
+ description: "Use Appwrite CLI session authentication instead of API key",
604
+ })
605
+ .option("sessionCookie", {
606
+ type: "string",
607
+ description: "Explicit session cookie to use for authentication",
608
+ })
609
+ .option("importFile", {
610
+ alias: ["import-file"],
611
+ type: "string",
612
+ description: "Import a CSV or JSON file directly into a table (no config needed)",
613
+ })
614
+ .option("targetDb", {
615
+ alias: ["target-db"],
616
+ type: "string",
617
+ description: "Target database ID for --importFile (prompted if omitted)",
618
+ })
619
+ .option("targetTable", {
620
+ alias: ["target-table"],
621
+ type: "string",
622
+ description: "Target table ID for --importFile (prompted if omitted)",
623
+ })
624
+ .parse() as ParsedArgv;
625
+
626
+ async function main() {
627
+ const startTime = Date.now();
628
+ const operationStats: Record<string, number> = {};
629
+
630
+ if (argv.it) {
631
+ const cli = new InteractiveCLI(process.cwd(), {
632
+ useSession: argv.useSession,
633
+ sessionCookie: argv.sessionCookie
634
+ });
635
+ await cli.run();
636
+ } else {
637
+ // Non-interactive mode - pass auth flags through to controller
638
+ // ConfigManager will handle config discovery and auth decisions
639
+ // Users can provide credentials via CLI flags even without a config file
640
+
641
+ const controller = UtilsController.getInstance(process.cwd());
642
+
643
+ // Build init options from CLI flags
644
+ const initOptions: any = {
645
+ useSession: argv.useSession,
646
+ sessionCookie: argv.sessionCookie,
647
+ preferJson: argv.appwriteConfig,
648
+ };
649
+
650
+ // Add CLI overrides if provided - these can work even without a config file
651
+ if (argv.endpoint || argv.projectId || argv.apiKey) {
652
+ initOptions.overrides = {
653
+ appwriteEndpoint: argv.endpoint,
654
+ appwriteProject: argv.projectId,
655
+ appwriteKey: argv.apiKey,
656
+ };
657
+ }
658
+
659
+ try {
660
+ await controller.init(initOptions);
661
+ } catch (error) {
662
+ if (error instanceof AuthenticationError) {
663
+ MessageFormatter.error(error.getFormattedMessage(), undefined, { prefix: "Auth" });
664
+ process.exit(1);
665
+ }
666
+ // Re-throw other errors
667
+ throw error;
668
+ }
669
+
670
+ // After init, check if we have a valid config (from file OR CLI overrides)
671
+ if (!controller.config) {
672
+ MessageFormatter.error("No Appwrite configuration available", undefined, { prefix: "CLI" });
673
+ MessageFormatter.info("Provide credentials via CLI flags (--endpoint, --projectId, --apiKey or --session)", { prefix: "CLI" });
674
+ MessageFormatter.info("Or create a config file using --setup", { prefix: "CLI" });
675
+ return;
676
+ }
677
+
678
+ const parsedArgv = argv;
679
+
680
+ if (argv.importFile) {
681
+ const { importFileFromPath, importFilePromptMissing } = await import("./cli/commands/importFileCommands.js");
682
+ if (!controller.adapter) {
683
+ MessageFormatter.error("No adapter available — check your credentials", undefined, { prefix: "Import" });
684
+ return;
685
+ }
686
+ if (parsedArgv.targetDb && parsedArgv.targetTable) {
687
+ await importFileFromPath(controller.adapter, argv.importFile, parsedArgv.targetDb, parsedArgv.targetTable);
688
+ } else {
689
+ await importFilePromptMissing(controller.adapter, controller.database, argv.importFile, parsedArgv.targetDb, parsedArgv.targetTable);
690
+ }
691
+ return;
692
+ }
693
+
694
+ if (argv.setup) {
695
+ await setupDirsFiles(false, process.cwd());
696
+ return;
697
+ }
698
+
699
+ if (argv.migrateConfig) {
700
+ const { migrateConfig } = await import("./utils/configMigration.js");
701
+ await migrateConfig(process.cwd());
702
+ return;
703
+ }
704
+
705
+ if (argv.generateConstants) {
706
+ const { ConstantsGenerator } = await import(
707
+ "appwrite-utils-helpers"
708
+ );
709
+ type SupportedLanguage =
710
+ import("appwrite-utils-helpers").SupportedLanguage;
711
+
712
+ if (!controller.config) {
713
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
714
+ prefix: "Constants",
715
+ });
716
+ return;
717
+ }
718
+
719
+ const languages = argv
720
+ .constantsLanguages!.split(",")
721
+ .map((l) => l.trim()) as SupportedLanguage[];
722
+
723
+ // Determine output directory - use config folder/constants by default, or custom path if specified
724
+ let outputDir: string;
725
+ if (argv.constantsOutput === "auto") {
726
+ // Default case: use config directory + constants, fallback to current directory
727
+ const configPath = controller.getAppwriteFolderPath();
728
+ outputDir = configPath
729
+ ? path.join(configPath, "constants")
730
+ : path.join(process.cwd(), "constants");
731
+ } else {
732
+ // Custom output directory specified
733
+ outputDir = argv.constantsOutput!;
734
+ }
735
+
736
+ MessageFormatter.info(
737
+ `Generating constants for languages: ${languages.join(", ")}`,
738
+ { prefix: "Constants" }
739
+ );
740
+
741
+ const generator = new ConstantsGenerator(controller.config);
742
+ await generator.generateFiles(languages, outputDir);
743
+
744
+ operationStats.generatedConstants = languages.length;
745
+ MessageFormatter.success(`Constants generated in ${outputDir}`, {
746
+ prefix: "Constants",
747
+ });
748
+ return;
749
+ }
750
+
751
+ if (argv.migrateCollectionsToTables) {
752
+ try {
753
+ if (!controller.config) {
754
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
755
+ prefix: "Migration",
756
+ });
757
+ return;
758
+ }
759
+
760
+ // Get the config path from the controller or use .appwrite in current directory
761
+ let configPath = controller.getAppwriteFolderPath();
762
+ if (!configPath) {
763
+ // Try .appwrite in current directory
764
+ const defaultPath = path.join(process.cwd(), ".appwrite");
765
+ if (fs.existsSync(defaultPath)) {
766
+ configPath = defaultPath;
767
+ } else {
768
+ MessageFormatter.error(
769
+ "Could not determine configuration folder path",
770
+ undefined,
771
+ { prefix: "Migration" }
772
+ );
773
+ MessageFormatter.info(
774
+ "Make sure you have a .appwrite/ folder in your current directory",
775
+ { prefix: "Migration" }
776
+ );
777
+ return;
778
+ }
779
+ }
780
+
781
+ // Check if migration conditions are met
782
+ const migrationCheck = checkMigrationConditions(configPath);
783
+ if (!migrationCheck.allowed) {
784
+ MessageFormatter.error(
785
+ `Migration not allowed: ${migrationCheck.reason}`,
786
+ undefined,
787
+ { prefix: "Migration" }
788
+ );
789
+ MessageFormatter.info("Migration requirements:", {
790
+ prefix: "Migration",
791
+ });
792
+ MessageFormatter.info(
793
+ " • Configuration must be loaded (use --config or have .appwrite/ folder)",
794
+ { prefix: "Migration" }
795
+ );
796
+ MessageFormatter.info(
797
+ " • collections/ folder must exist with YAML files",
798
+ { prefix: "Migration" }
799
+ );
800
+ MessageFormatter.info(
801
+ " • tables/ folder must not exist or be empty",
802
+ { prefix: "Migration" }
803
+ );
804
+ return;
805
+ }
806
+
807
+ const { migrateCollectionsToTables } = await import(
808
+ "appwrite-utils-helpers"
809
+ );
810
+
811
+ MessageFormatter.info("Starting collections to tables migration...", {
812
+ prefix: "Migration",
813
+ });
814
+ const result = migrateCollectionsToTables(controller.config, {
815
+ strategy: "full_migration",
816
+ validateResult: true,
817
+ dryRun: false,
818
+ });
819
+
820
+ if (result.success) {
821
+ operationStats.migratedCollections = result.changes.length;
822
+ MessageFormatter.success(
823
+ "Collections migration completed successfully",
824
+ { prefix: "Migration" }
825
+ );
826
+ } else {
827
+ MessageFormatter.error(
828
+ `Migration failed: ${result.errors.join(", ")}`,
829
+ undefined,
830
+ { prefix: "Migration" }
831
+ );
832
+ process.exit(1);
833
+ }
834
+ } catch (error) {
835
+ MessageFormatter.error(
836
+ "Migration failed",
837
+ error instanceof Error ? error : new Error(String(error)),
838
+ { prefix: "Migration" }
839
+ );
840
+ process.exit(1);
841
+ }
842
+ return;
843
+ }
844
+
845
+ // List backups if requested
846
+ if (parsedArgv.listBackups) {
847
+ const { AdapterFactory } = await import("appwrite-utils-helpers");
848
+ const { listBackups } = await import("./shared/backupTracking.js");
849
+
850
+ if (!controller.config) {
851
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
852
+ prefix: "Backups",
853
+ });
854
+ return;
855
+ }
856
+
857
+ const { adapter } = await AdapterFactory.create({
858
+ appwriteEndpoint: controller.config.appwriteEndpoint,
859
+ appwriteProject: controller.config.appwriteProject,
860
+ appwriteKey: controller.config.appwriteKey,
861
+ });
862
+
863
+ const databases = parsedArgv.dbIds
864
+ ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
865
+ : await fetchAllDatabases(controller.database!);
866
+
867
+ if (!databases || databases.length === 0) {
868
+ MessageFormatter.info("No databases found", { prefix: "Backups" });
869
+ return;
870
+ }
871
+
872
+ for (const db of databases!) {
873
+ const backups = await listBackups(adapter, db.$id);
874
+
875
+ MessageFormatter.info(
876
+ `\nBackups for database: ${db.name} (${db.$id})`,
877
+ { prefix: "Backups" }
878
+ );
879
+
880
+ if (backups.length === 0) {
881
+ MessageFormatter.info(" No backups found", { prefix: "Backups" });
882
+ } else {
883
+ backups.forEach((backup, index) => {
884
+ const date = new Date(backup.$createdAt).toLocaleString();
885
+ const size = MessageFormatter.formatBytes(backup.sizeBytes);
886
+ MessageFormatter.info(
887
+ ` ${
888
+ index + 1
889
+ }. ${date} - ${backup.format.toUpperCase()} - ${size} - ${
890
+ backup.collections
891
+ } collections, ${backup.documents} documents`,
892
+ { prefix: "Backups" }
893
+ );
894
+ });
895
+ }
896
+ }
897
+
898
+ return;
899
+ }
900
+
901
+ const options: SetupOptions = {
902
+ databases: parsedArgv.dbIds
903
+ ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
904
+ : undefined,
905
+ collections: parsedArgv.collectionIds?.split(","),
906
+ doBackup: parsedArgv.backup,
907
+ wipeDatabase: parsedArgv.wipe === "all" || parsedArgv.wipe === "docs",
908
+ wipeDocumentStorage:
909
+ parsedArgv.wipe === "all" || parsedArgv.wipe === "storage",
910
+ wipeUsers: parsedArgv.wipe === "all" || parsedArgv.wipe === "users",
911
+ generateSchemas: parsedArgv.generate,
912
+ importData: parsedArgv.import,
913
+ shouldWriteFile: parsedArgv.writeData,
914
+ wipeCollections: parsedArgv.wipeCollections,
915
+ transferUsers: parsedArgv.transferUsers,
916
+ };
917
+
918
+ if (parsedArgv.updateFunctionSpec) {
919
+ if (!parsedArgv.functionId || !parsedArgv.specification) {
920
+ throw new Error(
921
+ "Function ID and specification are required for updating function specs"
922
+ );
923
+ }
924
+ MessageFormatter.info(
925
+ `Updating function specification for ${parsedArgv.functionId} to ${parsedArgv.specification}`,
926
+ { prefix: "Functions" }
927
+ );
928
+ const specifications = await listSpecifications(
929
+ controller.appwriteServer!
930
+ );
931
+ if (
932
+ !specifications.specifications.some(
933
+ (s: { slug: string }) => s.slug === parsedArgv.specification
934
+ )
935
+ ) {
936
+ MessageFormatter.error(
937
+ `Specification ${parsedArgv.specification} not found`,
938
+ undefined,
939
+ { prefix: "Functions" }
940
+ );
941
+ return;
942
+ }
943
+ await controller.updateFunctionSpecifications(
944
+ parsedArgv.functionId,
945
+ parsedArgv.specification as Specification
946
+ );
947
+ }
948
+
949
+ // Add default databases if not specified (only if we need them for operations)
950
+ const needsDatabases =
951
+ options.doBackup ||
952
+ options.wipeDatabase ||
953
+ options.wipeDocumentStorage ||
954
+ options.wipeUsers ||
955
+ options.wipeCollections ||
956
+ options.importData ||
957
+ parsedArgv.sync ||
958
+ parsedArgv.transfer;
959
+
960
+ if (
961
+ needsDatabases &&
962
+ (!options.databases || options.databases.length === 0)
963
+ ) {
964
+ const allDatabases = await fetchAllDatabases(controller.database!);
965
+ options.databases = allDatabases;
966
+ }
967
+
968
+ // Add default collections if not specified
969
+ if (!options.collections || options.collections.length === 0) {
970
+ if (controller.config && controller.config.collections) {
971
+ options.collections = controller.config.collections.map(
972
+ (c: any) => c.name
973
+ );
974
+ } else {
975
+ options.collections = [];
976
+ }
977
+ }
978
+
979
+ // Comprehensive backup (all databases + all buckets)
980
+ if (parsedArgv.comprehensiveBackup) {
981
+ const { comprehensiveBackup } = await import(
982
+ "./backups/operations/comprehensiveBackup.js"
983
+ );
984
+ const { AdapterFactory } = await import("appwrite-utils-helpers");
985
+
986
+ // Get tracking database ID (interactive prompt if not specified)
987
+ let trackingDatabaseId = parsedArgv.trackingDatabaseId;
988
+
989
+ if (!trackingDatabaseId) {
990
+ // Fetch all databases for selection
991
+ const allDatabases = await fetchAllDatabases(controller.database!);
992
+
993
+ if (allDatabases.length === 0) {
994
+ MessageFormatter.error(
995
+ "No databases found. Cannot create comprehensive backup without a tracking database.",
996
+ undefined,
997
+ { prefix: "Backup" }
998
+ );
999
+ return;
1000
+ }
1001
+
1002
+ if (allDatabases.length === 1) {
1003
+ trackingDatabaseId = allDatabases[0].$id;
1004
+ MessageFormatter.info(
1005
+ `Using only available database for tracking: ${allDatabases[0].name} (${trackingDatabaseId})`,
1006
+ { prefix: "Backup" }
1007
+ );
1008
+ } else {
1009
+ // Interactive selection
1010
+ const inquirer = (await import("inquirer")).default;
1011
+ const answer = await inquirer.prompt([
1012
+ {
1013
+ type: "list",
1014
+ name: "trackingDb",
1015
+ message: "Select database to store backup tracking metadata:",
1016
+ choices: allDatabases.map((db) => ({
1017
+ name: `${db.name} (${db.$id})`,
1018
+ value: db.$id,
1019
+ })),
1020
+ },
1021
+ ]);
1022
+ trackingDatabaseId = answer.trackingDb;
1023
+ }
1024
+ }
1025
+
1026
+ // Ensure trackingDatabaseId is defined before proceeding
1027
+ if (!trackingDatabaseId) {
1028
+ throw new Error(
1029
+ "Tracking database ID is required for comprehensive backup"
1030
+ );
1031
+ }
1032
+
1033
+ MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, {
1034
+ prefix: "Backup",
1035
+ });
1036
+
1037
+ // Create adapter for backup tracking
1038
+ const { adapter } = await AdapterFactory.create({
1039
+ appwriteEndpoint: controller.config!.appwriteEndpoint,
1040
+ appwriteProject: controller.config!.appwriteProject,
1041
+ appwriteKey: controller.config!.appwriteKey,
1042
+ sessionCookie: controller.config!.sessionCookie,
1043
+ });
1044
+
1045
+ const result = await comprehensiveBackup(
1046
+ controller.config!,
1047
+ controller.database!,
1048
+ controller.storage!,
1049
+ adapter,
1050
+ {
1051
+ trackingDatabaseId,
1052
+ backupFormat: parsedArgv.backupFormat || "zip",
1053
+ parallelDownloads: parsedArgv.parallelDownloads || 10,
1054
+ onProgress: (message) => {
1055
+ MessageFormatter.info(message, { prefix: "Backup" });
1056
+ },
1057
+ }
1058
+ );
1059
+
1060
+ operationStats.comprehensiveBackup = 1;
1061
+ operationStats.databasesBackedUp = result.databaseBackups.length;
1062
+ operationStats.bucketsBackedUp = result.bucketBackups.length;
1063
+ operationStats.totalBackupSize = result.totalSizeBytes;
1064
+
1065
+ if (result.status === "completed") {
1066
+ MessageFormatter.success(
1067
+ `Comprehensive backup completed successfully (ID: ${result.backupId})`,
1068
+ { prefix: "Backup" }
1069
+ );
1070
+ } else if (result.status === "partial") {
1071
+ MessageFormatter.warning(
1072
+ `Comprehensive backup completed with errors (ID: ${result.backupId})`,
1073
+ { prefix: "Backup" }
1074
+ );
1075
+ result.errors.forEach((err) =>
1076
+ MessageFormatter.warning(err, { prefix: "Backup" })
1077
+ );
1078
+ } else {
1079
+ MessageFormatter.error(
1080
+ `Comprehensive backup failed (ID: ${result.backupId})`,
1081
+ undefined,
1082
+ { prefix: "Backup" }
1083
+ );
1084
+ result.errors.forEach((err) =>
1085
+ MessageFormatter.error(err, undefined, { prefix: "Backup" })
1086
+ );
1087
+ }
1088
+ }
1089
+
1090
+ if (options.doBackup && options.databases) {
1091
+ MessageFormatter.info(
1092
+ `Creating backups for ${options.databases.length} database(s) in ${parsedArgv.backupFormat} format`,
1093
+ { prefix: "Backup" }
1094
+ );
1095
+ for (const db of options.databases) {
1096
+ await controller.backupDatabase(db, parsedArgv.backupFormat || "json");
1097
+ }
1098
+ operationStats.backups = options.databases.length;
1099
+ MessageFormatter.success(
1100
+ `Backup completed for ${options.databases.length} database(s)`,
1101
+ { prefix: "Backup" }
1102
+ );
1103
+ }
1104
+
1105
+ if (
1106
+ options.wipeDatabase ||
1107
+ options.wipeDocumentStorage ||
1108
+ options.wipeUsers ||
1109
+ options.wipeCollections
1110
+ ) {
1111
+ // Confirm destructive operations
1112
+ const databaseNames = options.databases?.map((db) => db.name) || [];
1113
+ const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(
1114
+ databaseNames,
1115
+ {
1116
+ includeStorage: options.wipeDocumentStorage,
1117
+ includeUsers: options.wipeUsers,
1118
+ }
1119
+ );
1120
+
1121
+ if (!confirmed) {
1122
+ MessageFormatter.info("Operation cancelled by user", { prefix: "CLI" });
1123
+ return;
1124
+ }
1125
+
1126
+ let wipeStats = { databases: 0, collections: 0, users: 0, buckets: 0 };
1127
+
1128
+ if (parsedArgv.wipe === "all") {
1129
+ if (options.databases) {
1130
+ for (const db of options.databases) {
1131
+ await controller.wipeDatabase(db, true); // true to wipe associated buckets
1132
+ }
1133
+ wipeStats.databases = options.databases.length;
1134
+ }
1135
+ await controller.wipeUsers();
1136
+ wipeStats.users = 1;
1137
+ } else if (parsedArgv.wipe === "docs") {
1138
+ if (options.databases) {
1139
+ for (const db of options.databases) {
1140
+ await controller.wipeBucketFromDatabase(db);
1141
+ }
1142
+ wipeStats.databases = options.databases.length;
1143
+ }
1144
+ if (parsedArgv.bucketIds) {
1145
+ const bucketIds = parsedArgv.bucketIds.split(",");
1146
+ for (const bucketId of bucketIds) {
1147
+ await controller.wipeDocumentStorage(bucketId);
1148
+ }
1149
+ wipeStats.buckets = bucketIds.length;
1150
+ }
1151
+ } else if (parsedArgv.wipe === "users") {
1152
+ await controller.wipeUsers();
1153
+ wipeStats.users = 1;
1154
+ }
1155
+
1156
+ // Handle specific collection wipes
1157
+ if (options.wipeCollections && options.databases) {
1158
+ for (const db of options.databases) {
1159
+ const dbCollections = await fetchAllCollections(
1160
+ db.$id,
1161
+ controller.database!
1162
+ );
1163
+ const collectionsToWipe = dbCollections.filter((c) =>
1164
+ options.collections!.includes(c.$id)
1165
+ );
1166
+
1167
+ // Confirm collection wipe
1168
+ const collectionNames = collectionsToWipe.map((c) => c.name);
1169
+ const collectionConfirmed =
1170
+ await ConfirmationDialogs.confirmCollectionWipe(
1171
+ db.name,
1172
+ collectionNames
1173
+ );
1174
+
1175
+ if (collectionConfirmed) {
1176
+ for (const collection of collectionsToWipe) {
1177
+ await controller.wipeCollection(db, collection);
1178
+ }
1179
+ wipeStats.collections += collectionsToWipe.length;
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ // Show wipe operation summary
1185
+ if (
1186
+ wipeStats.databases > 0 ||
1187
+ wipeStats.collections > 0 ||
1188
+ wipeStats.users > 0 ||
1189
+ wipeStats.buckets > 0
1190
+ ) {
1191
+ operationStats.wipedDatabases = wipeStats.databases;
1192
+ operationStats.wipedCollections = wipeStats.collections;
1193
+ operationStats.wipedUsers = wipeStats.users;
1194
+ operationStats.wipedBuckets = wipeStats.buckets;
1195
+ }
1196
+ }
1197
+
1198
+ if (parsedArgv.push) {
1199
+ await controller.init();
1200
+ if (!controller.database || !controller.config) {
1201
+ MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
1202
+ return;
1203
+ }
1204
+
1205
+ // Fetch available DBs
1206
+ const availableDatabases = await fetchAllDatabases(controller.database);
1207
+ if (availableDatabases.length === 0) {
1208
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
1209
+ return;
1210
+ }
1211
+
1212
+ // Determine selected DBs
1213
+ let selectedDbIds: string[] = [];
1214
+ if (parsedArgv.dbIds) {
1215
+ selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
1216
+ } else {
1217
+ selectedDbIds = await SelectionDialogs.selectDatabases(
1218
+ availableDatabases,
1219
+ controller.config.databases || [],
1220
+ { showSelectAll: false, allowNewOnly: false, defaultSelected: [] }
1221
+ );
1222
+ }
1223
+
1224
+ if (selectedDbIds.length === 0) {
1225
+ MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
1226
+ return;
1227
+ }
1228
+
1229
+ // Build DatabaseSelection[] with tableIds per DB
1230
+ const databaseSelections: DatabaseSelection[] = [];
1231
+ const allConfigItems = [
1232
+ ...(controller.config.collections || []),
1233
+ ...(controller.config.tables || [])
1234
+ ];
1235
+ let lastSelectedTableIds: string[] | null = null;
1236
+
1237
+ for (const dbId of selectedDbIds) {
1238
+ const db = availableDatabases.find(d => d.$id === dbId);
1239
+ if (!db) continue;
1240
+
1241
+ // Filter config items eligible for this DB according to databaseId/databaseIds rule
1242
+ const eligibleConfigItems = (allConfigItems as any[]).filter(item => {
1243
+ const one = item.databaseId as string | undefined;
1244
+ const many = item.databaseIds as string[] | undefined;
1245
+ if (Array.isArray(many) && many.length > 0) return many.includes(dbId);
1246
+ if (one) return one === dbId;
1247
+ return true; // eligible everywhere if unspecified
1248
+ });
1249
+
1250
+ // Fetch available tables from remote for status/context
1251
+ const availableTables = await fetchAllCollections(dbId, controller.database);
1252
+ const remoteTableIds = new Set(availableTables.map(table => table.$id));
1253
+ const localItems = eligibleConfigItems;
1254
+ const localItemIds = localItems.map(item => item.$id || (item as any).id || (item as any).tableId || item.name);
1255
+ const localNewItems = localItems.filter(item => {
1256
+ const itemId = item.$id || (item as any).id || (item as any).tableId || item.name;
1257
+ return !remoteTableIds.has(itemId);
1258
+ });
1259
+ const localNewIds = localNewItems.map(item => item.$id || (item as any).id || (item as any).tableId || item.name);
1260
+
1261
+ // Determine selected table IDs
1262
+ let selectedTableIds: string[] = [];
1263
+ if (parsedArgv.collectionIds) {
1264
+ // Non-interactive: respect provided table IDs as-is (apply to each selected DB)
1265
+ selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
1266
+ } else {
1267
+ const inquirer = (await import("inquirer")).default;
1268
+ const choices: Array<{ name: string; value: string }> = [];
1269
+
1270
+ if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
1271
+ choices.push({
1272
+ name: `Use same selection as previous (${lastSelectedTableIds.length} items)`,
1273
+ value: "same"
1274
+ });
1275
+ }
1276
+
1277
+ if (localItemIds.length > 0) {
1278
+ choices.push({
1279
+ name: `Select all local items for ${db.name} (${localItemIds.length} items)`,
1280
+ value: "all_local"
1281
+ });
1282
+ }
1283
+
1284
+ if (localNewIds.length > 0) {
1285
+ choices.push({
1286
+ name: `Select only new local items (not on remote) (${localNewIds.length} items)`,
1287
+ value: "new_only"
1288
+ });
1289
+ }
1290
+
1291
+ choices.push({
1292
+ name: "Manual selection",
1293
+ value: "manual"
1294
+ });
1295
+
1296
+ const { selectionMode } = await inquirer.prompt([
1297
+ {
1298
+ type: "list",
1299
+ name: "selectionMode",
1300
+ message: `How do you want to select tables for ${db.name}?`,
1301
+ choices,
1302
+ default: choices[0]?.value || "manual"
1303
+ }
1304
+ ]);
1305
+
1306
+ if (selectionMode === "same") {
1307
+ selectedTableIds = [...(lastSelectedTableIds || [])];
1308
+ } else if (selectionMode === "all_local") {
1309
+ selectedTableIds = [...localItemIds];
1310
+ } else if (selectionMode === "new_only") {
1311
+ selectedTableIds = [...localNewIds];
1312
+ } else {
1313
+ if (localItems.length === 0) {
1314
+ MessageFormatter.warning(`No local tables/collections available for ${db.name}`, { prefix: "Push" });
1315
+ selectedTableIds = [];
1316
+ } else {
1317
+ selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1318
+ dbId,
1319
+ db.name,
1320
+ localItems as any[],
1321
+ availableTables as any[],
1322
+ { showSelectAll: localItems.length > 1, allowNewOnly: false, defaultSelected: lastSelectedTableIds || [] }
1323
+ );
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ databaseSelections.push({
1329
+ databaseId: db.$id,
1330
+ databaseName: db.name,
1331
+ tableIds: selectedTableIds,
1332
+ tableNames: [],
1333
+ isNew: false,
1334
+ });
1335
+ if (!parsedArgv.collectionIds) {
1336
+ lastSelectedTableIds = selectedTableIds;
1337
+ }
1338
+ }
1339
+
1340
+ if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
1341
+ MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
1342
+ return;
1343
+ }
1344
+
1345
+ const pushSummary: Record<string, string | number | string[]> = {
1346
+ databases: databaseSelections.length,
1347
+ collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
1348
+ details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
1349
+ };
1350
+ // Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
1351
+ if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
1352
+ const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
1353
+ if (!confirmed) {
1354
+ MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
1355
+ return;
1356
+ }
1357
+ }
1358
+
1359
+ await controller.selectivePush(databaseSelections, []);
1360
+ operationStats.pushedDatabases = databaseSelections.length;
1361
+ operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
1362
+ } else if (parsedArgv.sync) {
1363
+ // Enhanced SYNC: Pull from remote with intelligent configuration detection
1364
+ if (parsedArgv.autoSync) {
1365
+ // Legacy behavior: sync everything without prompts
1366
+ MessageFormatter.info("Using auto-sync mode (legacy behavior)", { prefix: "Sync" });
1367
+ const databases =
1368
+ options.databases || (await fetchAllDatabases(controller.database!));
1369
+ await controller.synchronizeConfigurations(databases);
1370
+ operationStats.syncedDatabases = databases.length;
1371
+ } else {
1372
+ // Enhanced sync flow with selection dialogs
1373
+ const syncResult = await performEnhancedSync(controller, parsedArgv);
1374
+ if (syncResult) {
1375
+ operationStats.syncedDatabases = syncResult.databases.length;
1376
+ operationStats.syncedCollections = syncResult.totalTables;
1377
+ operationStats.syncedBuckets = syncResult.buckets.length;
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ if (options.generateSchemas) {
1383
+ await controller.generateSchemas();
1384
+ operationStats.generatedSchemas = 1;
1385
+ }
1386
+
1387
+ if (options.importData) {
1388
+ await controller.importData(options);
1389
+ operationStats.importCompleted = 1;
1390
+ }
1391
+
1392
+ if (parsedArgv.transfer) {
1393
+ const isRemote = !!parsedArgv.remoteEndpoint;
1394
+ let fromDb, toDb: Models.Database | undefined;
1395
+ let targetDatabases: Databases | undefined;
1396
+ let targetStorage: Storage | undefined;
1397
+
1398
+ // Only fetch databases if database IDs are provided
1399
+ if (parsedArgv.fromDbId && parsedArgv.toDbId) {
1400
+ MessageFormatter.info(
1401
+ `Starting database transfer from ${parsedArgv.fromDbId} to ${parsedArgv.toDbId}`,
1402
+ { prefix: "Transfer" }
1403
+ );
1404
+ fromDb = (
1405
+ await controller.getDatabasesByIds([parsedArgv.fromDbId])
1406
+ )?.[0];
1407
+ if (!fromDb) {
1408
+ MessageFormatter.error("Source database not found", undefined, {
1409
+ prefix: "Transfer",
1410
+ });
1411
+ return;
1412
+ }
1413
+ if (isRemote) {
1414
+ if (
1415
+ !parsedArgv.remoteEndpoint ||
1416
+ !parsedArgv.remoteProjectId ||
1417
+ !parsedArgv.remoteApiKey
1418
+ ) {
1419
+ throw new Error("Remote transfer details are missing");
1420
+ }
1421
+ const remoteClient = getClient(
1422
+ parsedArgv.remoteEndpoint,
1423
+ parsedArgv.remoteProjectId,
1424
+ parsedArgv.remoteApiKey
1425
+ );
1426
+ targetDatabases = new Databases(remoteClient);
1427
+ targetStorage = new Storage(remoteClient);
1428
+ const remoteDbs = await fetchAllDatabases(targetDatabases);
1429
+ toDb = remoteDbs.find((db) => db.$id === parsedArgv.toDbId);
1430
+ if (!toDb) {
1431
+ MessageFormatter.error("Target database not found", undefined, {
1432
+ prefix: "Transfer",
1433
+ });
1434
+ return;
1435
+ }
1436
+ } else {
1437
+ toDb = (await controller.getDatabasesByIds([parsedArgv.toDbId]))?.[0];
1438
+ if (!toDb) {
1439
+ MessageFormatter.error("Target database not found", undefined, {
1440
+ prefix: "Transfer",
1441
+ });
1442
+ return;
1443
+ }
1444
+ }
1445
+
1446
+ if (!fromDb || !toDb) {
1447
+ MessageFormatter.error(
1448
+ "Source or target database not found",
1449
+ undefined,
1450
+ { prefix: "Transfer" }
1451
+ );
1452
+ return;
1453
+ }
1454
+ }
1455
+
1456
+ // Handle storage setup
1457
+ let sourceBucket, targetBucket;
1458
+ if (parsedArgv.fromBucketId) {
1459
+ sourceBucket = await controller.storage?.getBucket(
1460
+ parsedArgv.fromBucketId
1461
+ );
1462
+ }
1463
+ if (parsedArgv.toBucketId) {
1464
+ if (isRemote) {
1465
+ if (!targetStorage) {
1466
+ const remoteClient = getClient(
1467
+ parsedArgv.remoteEndpoint!,
1468
+ parsedArgv.remoteProjectId!,
1469
+ parsedArgv.remoteApiKey!
1470
+ );
1471
+ targetStorage = new Storage(remoteClient);
1472
+ }
1473
+ targetBucket = await targetStorage?.getBucket(parsedArgv.toBucketId);
1474
+ } else {
1475
+ targetBucket = await controller.storage?.getBucket(
1476
+ parsedArgv.toBucketId
1477
+ );
1478
+ }
1479
+ }
1480
+
1481
+ // Validate that at least one transfer type is specified
1482
+ if (!fromDb && !sourceBucket && !options.transferUsers) {
1483
+ throw new Error("No source database or bucket specified for transfer");
1484
+ }
1485
+
1486
+ const transferOptions: TransferOptions = {
1487
+ isRemote,
1488
+ fromDb,
1489
+ targetDb: toDb,
1490
+ transferEndpoint: parsedArgv.remoteEndpoint,
1491
+ transferProject: parsedArgv.remoteProjectId,
1492
+ transferKey: parsedArgv.remoteApiKey,
1493
+ sourceBucket: sourceBucket,
1494
+ targetBucket: targetBucket,
1495
+ transferUsers: options.transferUsers,
1496
+ };
1497
+
1498
+ await controller.transferData(transferOptions);
1499
+ operationStats.transfers = 1;
1500
+ }
1501
+
1502
+ // Show final operation summary if any operations were performed
1503
+ if (Object.keys(operationStats).length > 0) {
1504
+ const duration = Date.now() - startTime;
1505
+ MessageFormatter.operationSummary(
1506
+ "CLI Operations",
1507
+ operationStats,
1508
+ duration
1509
+ );
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ main().catch((error) => {
1515
+ MessageFormatter.error("CLI execution failed", error, { prefix: "CLI" });
1516
+ process.exit(1);
1517
+ });