appwrite-utils-cli 1.7.9 → 1.8.2

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 (70) hide show
  1. package/CHANGELOG.md +14 -199
  2. package/README.md +87 -30
  3. package/dist/adapters/AdapterFactory.js +5 -25
  4. package/dist/adapters/DatabaseAdapter.d.ts +17 -2
  5. package/dist/adapters/LegacyAdapter.d.ts +2 -1
  6. package/dist/adapters/LegacyAdapter.js +212 -16
  7. package/dist/adapters/TablesDBAdapter.d.ts +2 -12
  8. package/dist/adapters/TablesDBAdapter.js +261 -57
  9. package/dist/cli/commands/databaseCommands.js +4 -3
  10. package/dist/cli/commands/functionCommands.js +17 -8
  11. package/dist/collections/attributes.js +447 -125
  12. package/dist/collections/methods.js +197 -186
  13. package/dist/collections/tableOperations.d.ts +86 -0
  14. package/dist/collections/tableOperations.js +434 -0
  15. package/dist/collections/transferOperations.d.ts +3 -2
  16. package/dist/collections/transferOperations.js +93 -12
  17. package/dist/config/yamlConfig.d.ts +221 -88
  18. package/dist/examples/yamlTerminologyExample.d.ts +1 -1
  19. package/dist/examples/yamlTerminologyExample.js +6 -3
  20. package/dist/functions/fnConfigDiscovery.d.ts +3 -0
  21. package/dist/functions/fnConfigDiscovery.js +108 -0
  22. package/dist/interactiveCLI.js +18 -15
  23. package/dist/main.js +211 -73
  24. package/dist/migrations/appwriteToX.d.ts +88 -23
  25. package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
  26. package/dist/migrations/comprehensiveTransfer.js +83 -6
  27. package/dist/migrations/dataLoader.d.ts +227 -69
  28. package/dist/migrations/dataLoader.js +3 -3
  29. package/dist/migrations/importController.js +3 -3
  30. package/dist/migrations/relationships.d.ts +8 -2
  31. package/dist/migrations/services/ImportOrchestrator.js +3 -3
  32. package/dist/migrations/transfer.js +159 -37
  33. package/dist/shared/attributeMapper.d.ts +20 -0
  34. package/dist/shared/attributeMapper.js +203 -0
  35. package/dist/shared/selectionDialogs.js +8 -4
  36. package/dist/storage/schemas.d.ts +354 -92
  37. package/dist/utils/configDiscovery.js +4 -3
  38. package/dist/utils/versionDetection.d.ts +0 -4
  39. package/dist/utils/versionDetection.js +41 -173
  40. package/dist/utils/yamlConverter.js +89 -16
  41. package/dist/utils/yamlLoader.d.ts +1 -1
  42. package/dist/utils/yamlLoader.js +6 -2
  43. package/dist/utilsController.js +56 -19
  44. package/package.json +4 -4
  45. package/src/adapters/AdapterFactory.ts +119 -143
  46. package/src/adapters/DatabaseAdapter.ts +18 -3
  47. package/src/adapters/LegacyAdapter.ts +236 -105
  48. package/src/adapters/TablesDBAdapter.ts +773 -643
  49. package/src/cli/commands/databaseCommands.ts +13 -12
  50. package/src/cli/commands/functionCommands.ts +23 -14
  51. package/src/collections/attributes.ts +2054 -1611
  52. package/src/collections/methods.ts +208 -293
  53. package/src/collections/tableOperations.ts +506 -0
  54. package/src/collections/transferOperations.ts +218 -144
  55. package/src/examples/yamlTerminologyExample.ts +10 -5
  56. package/src/functions/fnConfigDiscovery.ts +103 -0
  57. package/src/interactiveCLI.ts +25 -20
  58. package/src/main.ts +549 -194
  59. package/src/migrations/comprehensiveTransfer.ts +126 -50
  60. package/src/migrations/dataLoader.ts +3 -3
  61. package/src/migrations/importController.ts +3 -3
  62. package/src/migrations/services/ImportOrchestrator.ts +3 -3
  63. package/src/migrations/transfer.ts +148 -131
  64. package/src/shared/attributeMapper.ts +229 -0
  65. package/src/shared/selectionDialogs.ts +29 -25
  66. package/src/utils/configDiscovery.ts +9 -3
  67. package/src/utils/versionDetection.ts +74 -228
  68. package/src/utils/yamlConverter.ts +94 -17
  69. package/src/utils/yamlLoader.ts +11 -4
  70. package/src/utilsController.ts +80 -30
@@ -2,7 +2,7 @@ import inquirer from "inquirer";
2
2
  import { UtilsController } from "./utilsController.js";
3
3
  import { fetchAllCollections } from "./collections/methods.js";
4
4
  import { listBuckets, createBucket } from "./storage/methods.js";
5
- import { Databases, Storage, Client, Compression, Query, Functions, } from "node-appwrite";
5
+ import { Databases, Storage, Client, Compression, Query, Functions, DatabaseType, } from "node-appwrite";
6
6
  import { PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
7
7
  import { ulid } from "ulidx";
8
8
  import chalk from "chalk";
@@ -192,10 +192,10 @@ export class InteractiveCLI {
192
192
  const configDatabases = this.getLocalDatabases();
193
193
  const allDatabases = [...databases, ...configDatabases]
194
194
  .reduce((acc, db) => {
195
- // Local config takes precedence - if a database with same name exists, use local version
196
- const existingIndex = acc.findIndex((d) => d.name === db.name);
195
+ // Local config takes precedence - if a database with same name or ID exists, use local version
196
+ const existingIndex = acc.findIndex((d) => d.name === db.name || d.$id === db.$id);
197
197
  if (existingIndex >= 0) {
198
- if (configDatabases.some((cdb) => cdb.name === db.name)) {
198
+ if (configDatabases.some((cdb) => cdb.name === db.name || cdb.$id === db.$id)) {
199
199
  acc[existingIndex] = db; // Replace with local version
200
200
  }
201
201
  }
@@ -204,14 +204,14 @@ export class InteractiveCLI {
204
204
  }
205
205
  return acc;
206
206
  }, []);
207
- const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name)) &&
208
- allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name));
207
+ const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name || c.$id === db.$id)) &&
208
+ allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name || c.$id === db.$id));
209
209
  const choices = allDatabases
210
210
  .sort((a, b) => a.name.localeCompare(b.name))
211
211
  .map((db) => ({
212
212
  name: db.name +
213
213
  (hasLocalAndRemote
214
- ? configDatabases.some((c) => c.name === db.name)
214
+ ? configDatabases.some((c) => c.name === db.name || c.$id === db.$id)
215
215
  ? " (Local)"
216
216
  : " (Remote)"
217
217
  : ""),
@@ -245,20 +245,20 @@ export class InteractiveCLI {
245
245
  }
246
246
  let allCollections = preferLocal
247
247
  ? remoteCollections.reduce((acc, remoteCollection) => {
248
- if (!acc.some((c) => c.name === remoteCollection.name)) {
248
+ if (!acc.some((c) => c.name === remoteCollection.name || c.$id === remoteCollection.$id)) {
249
249
  acc.push(remoteCollection);
250
250
  }
251
251
  return acc;
252
252
  }, [...configCollections])
253
253
  : [
254
254
  ...remoteCollections,
255
- ...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
255
+ ...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name || rc.$id === c.$id)),
256
256
  ];
257
257
  if (shouldFilterByDatabase) {
258
258
  // Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
259
259
  allCollections = allCollections.filter((c) => {
260
260
  // Include if it exists remotely in this database
261
- const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name);
261
+ const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name || rc.$id === c.$id);
262
262
  // Include if local metadata claims it belongs to this database
263
263
  const hasMatchingLocalMetadata = c.databaseId === database.$id;
264
264
  return existsInRemoteDb || hasMatchingLocalMetadata;
@@ -266,8 +266,8 @@ export class InteractiveCLI {
266
266
  }
267
267
  // Filter out system tables (those starting with underscore)
268
268
  allCollections = allCollections.filter((collection) => !collection.$id.startsWith('_'));
269
- const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
270
- allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
269
+ const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name || c.$id === coll.$id)) &&
270
+ allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name || c.$id === coll.$id));
271
271
  // Enhanced choice display with type indicators
272
272
  const choices = allCollections
273
273
  .sort((a, b) => {
@@ -280,7 +280,7 @@ export class InteractiveCLI {
280
280
  return a.name.localeCompare(b.name);
281
281
  })
282
282
  .map((collection) => {
283
- const localCollection = configCollections.find((c) => c.name === collection.name);
283
+ const localCollection = configCollections.find((c) => c.name === collection.name || c.$id === collection.$id);
284
284
  const isLocal = !!localCollection;
285
285
  const isTable = localCollection?._isFromTablesDir || collection._isFromTablesDir || false;
286
286
  const sourceFolder = localCollection?._sourceFolder || collection._sourceFolder || 'collections';
@@ -350,6 +350,8 @@ export class InteractiveCLI {
350
350
  else if (tablesCount > 0) {
351
351
  MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder`, { prefix: "Collections" });
352
352
  }
353
+ // Show current database context clearly before view mode selection
354
+ MessageFormatter.info(`DB: ${database.name}`, { prefix: "Collections" });
353
355
  // Ask user if they want to filter by database or show all
354
356
  const { filterChoice } = await inquirer.prompt([
355
357
  {
@@ -499,7 +501,7 @@ export class InteractiveCLI {
499
501
  // Combine functions, preferring local ones
500
502
  const allFunctions = [
501
503
  ...localFunctions,
502
- ...remoteFunctions.functions.filter((rf) => !localFunctions.some((lf) => lf.name === rf.name)),
504
+ ...remoteFunctions.functions.filter((rf) => !localFunctions.some((lf) => lf.name === rf.name || lf.$id === rf.$id)),
503
505
  ];
504
506
  const { selectedFunctions } = await inquirer.prompt([
505
507
  {
@@ -507,7 +509,7 @@ export class InteractiveCLI {
507
509
  name: "selectedFunctions",
508
510
  message,
509
511
  choices: allFunctions.map((f) => ({
510
- name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name)
512
+ name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name || lf.$id === f.$id)
511
513
  ? " (Local)"
512
514
  : " (Remote)"}`,
513
515
  value: f,
@@ -773,6 +775,7 @@ export class InteractiveCLI {
773
775
  $updatedAt: DateTime.now().toISO(),
774
776
  name: db.name,
775
777
  enabled: true,
778
+ type: "tablesdb",
776
779
  }));
777
780
  }
778
781
  /**
package/dist/main.js CHANGED
@@ -18,9 +18,9 @@ import { logger } from "./shared/logging.js";
18
18
  import path from "path";
19
19
  import fs from "fs";
20
20
  import { createRequire } from "node:module";
21
- import { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig } from "./utils/projectConfig.js";
22
- import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus } from "./utils/sessionAuth.js";
23
- import { findYamlConfig, loadYamlConfigWithSession } from "./config/yamlConfig.js";
21
+ import { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig, } from "./utils/projectConfig.js";
22
+ import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus, } from "./utils/sessionAuth.js";
23
+ import { findYamlConfig, loadYamlConfigWithSession, } from "./config/yamlConfig.js";
24
24
  const require = createRequire(import.meta.url);
25
25
  if (!globalThis.require) {
26
26
  globalThis.require = require;
@@ -94,9 +94,9 @@ async function performEnhancedSync(controller, parsedArgv) {
94
94
  const allowNewOnly = !syncExisting;
95
95
  // Select databases
96
96
  const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, {
97
- showSelectAll: true,
97
+ showSelectAll: false,
98
98
  allowNewOnly,
99
- defaultSelected: syncExisting ? configuredDatabases.map(db => db.$id) : []
99
+ defaultSelected: []
100
100
  });
101
101
  if (selectedDatabaseIds.length === 0) {
102
102
  MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
@@ -116,9 +116,9 @@ async function performEnhancedSync(controller, parsedArgv) {
116
116
  const configuredTables = controller.config.collections || [];
117
117
  // Select tables for this database
118
118
  const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(databaseId, database.name, availableTables, configuredTables, {
119
- showSelectAll: true,
119
+ showSelectAll: false,
120
120
  allowNewOnly,
121
- defaultSelected: syncExisting ? configuredTables.map((t) => t.$id) : []
121
+ defaultSelected: []
122
122
  });
123
123
  tableSelectionsMap.set(databaseId, selectedTableIds);
124
124
  if (selectedTableIds.length === 0) {
@@ -137,10 +137,10 @@ async function performEnhancedSync(controller, parsedArgv) {
137
137
  // you'd fetch this from the Appwrite API
138
138
  const availableBuckets = configuredBuckets; // Placeholder
139
139
  selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, {
140
- showSelectAll: true,
140
+ showSelectAll: false,
141
141
  allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
142
142
  groupByDatabase: true,
143
- defaultSelected: syncExisting ? configuredBuckets.map(b => b.$id) : []
143
+ defaultSelected: []
144
144
  });
145
145
  }
146
146
  catch (error) {
@@ -185,24 +185,28 @@ function checkMigrationConditions(configPath) {
185
185
  if (!fs.existsSync(collectionsPath)) {
186
186
  return {
187
187
  allowed: false,
188
- reason: "No collections/ folder found. Migration requires existing collections to migrate."
188
+ reason: "No collections/ folder found. Migration requires existing collections to migrate.",
189
189
  };
190
190
  }
191
191
  // Check if collections/ folder has YAML files
192
- const collectionFiles = fs.readdirSync(collectionsPath).filter(file => file.endsWith(".yaml") || file.endsWith(".yml"));
192
+ const collectionFiles = fs
193
+ .readdirSync(collectionsPath)
194
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
193
195
  if (collectionFiles.length === 0) {
194
196
  return {
195
197
  allowed: false,
196
- reason: "No YAML files found in collections/ folder. Migration requires existing collection YAML files."
198
+ reason: "No YAML files found in collections/ folder. Migration requires existing collection YAML files.",
197
199
  };
198
200
  }
199
201
  // Check if tables/ folder exists and has YAML files
200
202
  if (fs.existsSync(tablesPath)) {
201
- const tableFiles = fs.readdirSync(tablesPath).filter(file => file.endsWith(".yaml") || file.endsWith(".yml"));
203
+ const tableFiles = fs
204
+ .readdirSync(tablesPath)
205
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
202
206
  if (tableFiles.length > 0) {
203
207
  return {
204
208
  allowed: false,
205
- reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`
209
+ reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`,
206
210
  };
207
211
  }
208
212
  }
@@ -433,16 +437,23 @@ async function main() {
433
437
  // Enhanced config creation with session and project file support
434
438
  let directConfig = undefined;
435
439
  // Show authentication status on startup if no config provided
436
- if (!argv.config && !argv.endpoint && !argv.projectId && !argv.apiKey && !argv.useSession && !argv.sessionCookie) {
440
+ if (!argv.config &&
441
+ !argv.endpoint &&
442
+ !argv.projectId &&
443
+ !argv.apiKey &&
444
+ !argv.useSession &&
445
+ !argv.sessionCookie) {
437
446
  if (hasAnyValidSessions) {
438
447
  MessageFormatter.info(`Found ${availableSessions.length} available session(s)`, { prefix: "Auth" });
439
- availableSessions.forEach(session => {
440
- MessageFormatter.info(` \u2022 ${session.projectId} (${session.email || 'unknown'}) at ${session.endpoint}`, { prefix: "Auth" });
448
+ availableSessions.forEach((session) => {
449
+ MessageFormatter.info(` \u2022 ${session.projectId} (${session.email || "unknown"}) at ${session.endpoint}`, { prefix: "Auth" });
441
450
  });
442
451
  MessageFormatter.info("Use --session to enable session authentication", { prefix: "Auth" });
443
452
  }
444
453
  else {
445
- MessageFormatter.info("No active Appwrite sessions found", { prefix: "Auth" });
454
+ MessageFormatter.info("No active Appwrite sessions found", {
455
+ prefix: "Auth",
456
+ });
446
457
  MessageFormatter.info("\u2022 Run 'appwrite login' to authenticate with session", { prefix: "Auth" });
447
458
  MessageFormatter.info("\u2022 Or provide --apiKey for API key authentication", { prefix: "Auth" });
448
459
  }
@@ -457,7 +468,11 @@ async function main() {
457
468
  }
458
469
  }
459
470
  // Priority 2: CLI arguments override project config
460
- if (argv.endpoint || argv.projectId || argv.apiKey || argv.useSession || argv.sessionCookie) {
471
+ if (argv.endpoint ||
472
+ argv.projectId ||
473
+ argv.apiKey ||
474
+ argv.useSession ||
475
+ argv.sessionCookie) {
461
476
  directConfig = {
462
477
  ...directConfig,
463
478
  appwriteEndpoint: argv.endpoint || directConfig?.appwriteEndpoint,
@@ -482,7 +497,9 @@ async function main() {
482
497
  MessageFormatter.warning("Session authentication requested but no valid session found", { prefix: "Auth" });
483
498
  const availableSessions = getAvailableSessions();
484
499
  if (availableSessions.length > 0) {
485
- MessageFormatter.info(`Available sessions: ${availableSessions.map(s => `${s.projectId} (${s.email || 'unknown'})`).join(", ")}`, { prefix: "Auth" });
500
+ MessageFormatter.info(`Available sessions: ${availableSessions
501
+ .map((s) => `${s.projectId} (${s.email || "unknown"})`)
502
+ .join(", ")}`, { prefix: "Auth" });
486
503
  MessageFormatter.info("Use --session flag to enable session authentication", { prefix: "Auth" });
487
504
  }
488
505
  else {
@@ -503,11 +520,16 @@ async function main() {
503
520
  // 3. Auto-detect session authentication when possible
504
521
  let finalDirectConfig = directConfig;
505
522
  if ((argv.useSession || argv.sessionCookie) &&
506
- (!directConfig || !directConfig.appwriteEndpoint || !directConfig.appwriteProject)) {
523
+ (!directConfig ||
524
+ !directConfig.appwriteEndpoint ||
525
+ !directConfig.appwriteProject)) {
507
526
  // Don't pass incomplete directConfig - let UtilsController load YAML config normally
508
527
  finalDirectConfig = null;
509
528
  }
510
- else if (finalDirectConfig && !finalDirectConfig.appwriteKey && !argv.useSession && !argv.sessionCookie) {
529
+ else if (finalDirectConfig &&
530
+ !finalDirectConfig.appwriteKey &&
531
+ !argv.useSession &&
532
+ !argv.sessionCookie) {
511
533
  // Auto-detect session authentication when no API key provided
512
534
  if (sessionAuthAvailable) {
513
535
  MessageFormatter.info("No API key provided, but session authentication is available", { prefix: "Auth" });
@@ -539,10 +561,14 @@ async function main() {
539
561
  if (argv.generateConstants) {
540
562
  const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
541
563
  if (!controller.config) {
542
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Constants" });
564
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
565
+ prefix: "Constants",
566
+ });
543
567
  return;
544
568
  }
545
- const languages = argv.constantsLanguages.split(",").map(l => l.trim());
569
+ const languages = argv
570
+ .constantsLanguages.split(",")
571
+ .map((l) => l.trim());
546
572
  // Determine output directory - use config folder/constants by default, or custom path if specified
547
573
  let outputDir;
548
574
  if (argv.constantsOutput === "auto") {
@@ -560,13 +586,17 @@ async function main() {
560
586
  const generator = new ConstantsGenerator(controller.config);
561
587
  await generator.generateFiles(languages, outputDir);
562
588
  operationStats.generatedConstants = languages.length;
563
- MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
589
+ MessageFormatter.success(`Constants generated in ${outputDir}`, {
590
+ prefix: "Constants",
591
+ });
564
592
  return;
565
593
  }
566
594
  if (argv.migrateCollectionsToTables) {
567
595
  try {
568
596
  if (!controller.config) {
569
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Migration" });
597
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
598
+ prefix: "Migration",
599
+ });
570
600
  return;
571
601
  }
572
602
  // Get the config path from the controller or use .appwrite in current directory
@@ -587,18 +617,22 @@ async function main() {
587
617
  const migrationCheck = checkMigrationConditions(configPath);
588
618
  if (!migrationCheck.allowed) {
589
619
  MessageFormatter.error(`Migration not allowed: ${migrationCheck.reason}`, undefined, { prefix: "Migration" });
590
- MessageFormatter.info("Migration requirements:", { prefix: "Migration" });
620
+ MessageFormatter.info("Migration requirements:", {
621
+ prefix: "Migration",
622
+ });
591
623
  MessageFormatter.info(" • Configuration must be loaded (use --config or have .appwrite/ folder)", { prefix: "Migration" });
592
624
  MessageFormatter.info(" • collections/ folder must exist with YAML files", { prefix: "Migration" });
593
625
  MessageFormatter.info(" • tables/ folder must not exist or be empty", { prefix: "Migration" });
594
626
  return;
595
627
  }
596
628
  const { migrateCollectionsToTables } = await import("./config/configMigration.js");
597
- MessageFormatter.info("Starting collections to tables migration...", { prefix: "Migration" });
629
+ MessageFormatter.info("Starting collections to tables migration...", {
630
+ prefix: "Migration",
631
+ });
598
632
  const result = migrateCollectionsToTables(controller.config, {
599
633
  strategy: "full_migration",
600
634
  validateResult: true,
601
- dryRun: false
635
+ dryRun: false,
602
636
  });
603
637
  if (result.success) {
604
638
  operationStats.migratedCollections = result.changes.length;
@@ -619,17 +653,29 @@ async function main() {
619
653
  // Provide better guidance based on available authentication methods
620
654
  const availableSessions = getAvailableSessions();
621
655
  if (availableSessions.length > 0) {
622
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "CLI" });
623
- MessageFormatter.info("Available authentication options:", { prefix: "Auth" });
624
- MessageFormatter.info("• Session authentication: Add --session flag", { prefix: "Auth" });
656
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
657
+ prefix: "CLI",
658
+ });
659
+ MessageFormatter.info("Available authentication options:", {
660
+ prefix: "Auth",
661
+ });
662
+ MessageFormatter.info("• Session authentication: Add --session flag", {
663
+ prefix: "Auth",
664
+ });
625
665
  MessageFormatter.info("• API key authentication: Add --apiKey YOUR_API_KEY", { prefix: "Auth" });
626
- MessageFormatter.info(`• Available sessions: ${availableSessions.map(s => `${s.projectId} (${s.email || 'unknown'})`).join(", ")}`, { prefix: "Auth" });
666
+ MessageFormatter.info(`• Available sessions: ${availableSessions
667
+ .map((s) => `${s.projectId} (${s.email || "unknown"})`)
668
+ .join(", ")}`, { prefix: "Auth" });
627
669
  }
628
670
  else {
629
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "CLI" });
671
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
672
+ prefix: "CLI",
673
+ });
630
674
  MessageFormatter.info("Authentication options:", { prefix: "Auth" });
631
675
  MessageFormatter.info("• Login with Appwrite CLI: Run 'appwrite login' then use --session flag", { prefix: "Auth" });
632
- MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", { prefix: "Auth" });
676
+ MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", {
677
+ prefix: "Auth",
678
+ });
633
679
  MessageFormatter.info("• Create config file: Run with --setup to initialize project configuration", { prefix: "Auth" });
634
680
  }
635
681
  return;
@@ -640,13 +686,15 @@ async function main() {
640
686
  const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
641
687
  const { listBackups } = await import("./shared/backupTracking.js");
642
688
  if (!controller.config) {
643
- MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Backups" });
689
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
690
+ prefix: "Backups",
691
+ });
644
692
  return;
645
693
  }
646
694
  const { adapter } = await AdapterFactory.create({
647
695
  appwriteEndpoint: controller.config.appwriteEndpoint,
648
696
  appwriteProject: controller.config.appwriteProject,
649
- appwriteKey: controller.config.appwriteKey
697
+ appwriteKey: controller.config.appwriteKey,
650
698
  });
651
699
  const databases = parsedArgv.dbIds
652
700
  ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
@@ -699,11 +747,16 @@ async function main() {
699
747
  await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification);
700
748
  }
701
749
  // Add default databases if not specified (only if we need them for operations)
702
- const needsDatabases = options.doBackup || options.wipeDatabase ||
703
- options.wipeDocumentStorage || options.wipeUsers ||
704
- options.wipeCollections || options.importData ||
705
- parsedArgv.sync || parsedArgv.transfer;
706
- if (needsDatabases && (!options.databases || options.databases.length === 0)) {
750
+ const needsDatabases = options.doBackup ||
751
+ options.wipeDatabase ||
752
+ options.wipeDocumentStorage ||
753
+ options.wipeUsers ||
754
+ options.wipeCollections ||
755
+ options.importData ||
756
+ parsedArgv.sync ||
757
+ parsedArgv.transfer;
758
+ if (needsDatabases &&
759
+ (!options.databases || options.databases.length === 0)) {
707
760
  const allDatabases = await fetchAllDatabases(controller.database);
708
761
  options.databases = allDatabases;
709
762
  }
@@ -736,58 +789,62 @@ async function main() {
736
789
  else {
737
790
  // Interactive selection
738
791
  const inquirer = (await import("inquirer")).default;
739
- const answer = await inquirer.prompt([{
740
- type: 'list',
741
- name: 'trackingDb',
742
- message: 'Select database to store backup tracking metadata:',
743
- choices: allDatabases.map(db => ({
792
+ const answer = await inquirer.prompt([
793
+ {
794
+ type: "list",
795
+ name: "trackingDb",
796
+ message: "Select database to store backup tracking metadata:",
797
+ choices: allDatabases.map((db) => ({
744
798
  name: `${db.name} (${db.$id})`,
745
- value: db.$id
746
- }))
747
- }]);
799
+ value: db.$id,
800
+ })),
801
+ },
802
+ ]);
748
803
  trackingDatabaseId = answer.trackingDb;
749
804
  }
750
805
  }
751
806
  // Ensure trackingDatabaseId is defined before proceeding
752
807
  if (!trackingDatabaseId) {
753
- throw new Error('Tracking database ID is required for comprehensive backup');
808
+ throw new Error("Tracking database ID is required for comprehensive backup");
754
809
  }
755
- MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, { prefix: "Backup" });
810
+ MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, {
811
+ prefix: "Backup",
812
+ });
756
813
  // Create adapter for backup tracking
757
814
  const { adapter } = await AdapterFactory.create({
758
815
  appwriteEndpoint: controller.config.appwriteEndpoint,
759
816
  appwriteProject: controller.config.appwriteProject,
760
817
  appwriteKey: controller.config.appwriteKey,
761
- sessionCookie: controller.config.sessionCookie
818
+ sessionCookie: controller.config.sessionCookie,
762
819
  });
763
820
  const result = await comprehensiveBackup(controller.config, controller.database, controller.storage, adapter, {
764
821
  trackingDatabaseId,
765
- backupFormat: parsedArgv.backupFormat || 'zip',
822
+ backupFormat: parsedArgv.backupFormat || "zip",
766
823
  parallelDownloads: parsedArgv.parallelDownloads || 10,
767
824
  onProgress: (message) => {
768
825
  MessageFormatter.info(message, { prefix: "Backup" });
769
- }
826
+ },
770
827
  });
771
828
  operationStats.comprehensiveBackup = 1;
772
829
  operationStats.databasesBackedUp = result.databaseBackups.length;
773
830
  operationStats.bucketsBackedUp = result.bucketBackups.length;
774
831
  operationStats.totalBackupSize = result.totalSizeBytes;
775
- if (result.status === 'completed') {
832
+ if (result.status === "completed") {
776
833
  MessageFormatter.success(`Comprehensive backup completed successfully (ID: ${result.backupId})`, { prefix: "Backup" });
777
834
  }
778
- else if (result.status === 'partial') {
835
+ else if (result.status === "partial") {
779
836
  MessageFormatter.warning(`Comprehensive backup completed with errors (ID: ${result.backupId})`, { prefix: "Backup" });
780
- result.errors.forEach(err => MessageFormatter.warning(err, { prefix: "Backup" }));
837
+ result.errors.forEach((err) => MessageFormatter.warning(err, { prefix: "Backup" }));
781
838
  }
782
839
  else {
783
840
  MessageFormatter.error(`Comprehensive backup failed (ID: ${result.backupId})`, undefined, { prefix: "Backup" });
784
- result.errors.forEach(err => MessageFormatter.error(err, undefined, { prefix: "Backup" }));
841
+ result.errors.forEach((err) => MessageFormatter.error(err, undefined, { prefix: "Backup" }));
785
842
  }
786
843
  }
787
844
  if (options.doBackup && options.databases) {
788
845
  MessageFormatter.info(`Creating backups for ${options.databases.length} database(s) in ${parsedArgv.backupFormat} format`, { prefix: "Backup" });
789
846
  for (const db of options.databases) {
790
- await controller.backupDatabase(db, parsedArgv.backupFormat || 'json');
847
+ await controller.backupDatabase(db, parsedArgv.backupFormat || "json");
791
848
  }
792
849
  operationStats.backups = options.databases.length;
793
850
  MessageFormatter.success(`Backup completed for ${options.databases.length} database(s)`, { prefix: "Backup" });
@@ -797,10 +854,10 @@ async function main() {
797
854
  options.wipeUsers ||
798
855
  options.wipeCollections) {
799
856
  // Confirm destructive operations
800
- const databaseNames = options.databases?.map(db => db.name) || [];
857
+ const databaseNames = options.databases?.map((db) => db.name) || [];
801
858
  const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
802
859
  includeStorage: options.wipeDocumentStorage,
803
- includeUsers: options.wipeUsers
860
+ includeUsers: options.wipeUsers,
804
861
  });
805
862
  if (!confirmed) {
806
863
  MessageFormatter.info("Operation cancelled by user", { prefix: "CLI" });
@@ -842,7 +899,7 @@ async function main() {
842
899
  const dbCollections = await fetchAllCollections(db.$id, controller.database);
843
900
  const collectionsToWipe = dbCollections.filter((c) => options.collections.includes(c.$id));
844
901
  // Confirm collection wipe
845
- const collectionNames = collectionsToWipe.map(c => c.name);
902
+ const collectionNames = collectionsToWipe.map((c) => c.name);
846
903
  const collectionConfirmed = await ConfirmationDialogs.confirmCollectionWipe(db.name, collectionNames);
847
904
  if (collectionConfirmed) {
848
905
  for (const collection of collectionsToWipe) {
@@ -853,7 +910,10 @@ async function main() {
853
910
  }
854
911
  }
855
912
  // Show wipe operation summary
856
- if (wipeStats.databases > 0 || wipeStats.collections > 0 || wipeStats.users > 0 || wipeStats.buckets > 0) {
913
+ if (wipeStats.databases > 0 ||
914
+ wipeStats.collections > 0 ||
915
+ wipeStats.users > 0 ||
916
+ wipeStats.buckets > 0) {
857
917
  operationStats.wipedDatabases = wipeStats.databases;
858
918
  operationStats.wipedCollections = wipeStats.collections;
859
919
  operationStats.wipedUsers = wipeStats.users;
@@ -861,12 +921,84 @@ async function main() {
861
921
  }
862
922
  }
863
923
  if (parsedArgv.push) {
864
- // PUSH: Use LOCAL config collections only (pass empty array to use config.collections)
865
- const databases = options.databases || (await fetchAllDatabases(controller.database));
866
- // Pass empty array - syncDb will use config.collections (local schema)
867
- await controller.syncDb(databases, []);
868
- operationStats.pushedDatabases = databases.length;
869
- operationStats.pushedCollections = controller.config?.collections?.length || 0;
924
+ await controller.init();
925
+ if (!controller.database || !controller.config) {
926
+ MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
927
+ return;
928
+ }
929
+ // Fetch available DBs
930
+ const availableDatabases = await fetchAllDatabases(controller.database);
931
+ if (availableDatabases.length === 0) {
932
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
933
+ return;
934
+ }
935
+ // Determine selected DBs
936
+ let selectedDbIds = [];
937
+ if (parsedArgv.dbIds) {
938
+ selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
939
+ }
940
+ else {
941
+ selectedDbIds = await SelectionDialogs.selectDatabases(availableDatabases, controller.config.databases || [], { showSelectAll: false, allowNewOnly: false, defaultSelected: [] });
942
+ }
943
+ if (selectedDbIds.length === 0) {
944
+ MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
945
+ return;
946
+ }
947
+ // Build DatabaseSelection[] with tableIds per DB
948
+ const databaseSelections = [];
949
+ const allConfigItems = controller.config.collections || controller.config.tables || [];
950
+ for (const dbId of selectedDbIds) {
951
+ const db = availableDatabases.find(d => d.$id === dbId);
952
+ if (!db)
953
+ continue;
954
+ // Filter config items eligible for this DB according to databaseId/databaseIds rule
955
+ const eligibleConfigItems = allConfigItems.filter(item => {
956
+ const one = item.databaseId;
957
+ const many = item.databaseIds;
958
+ if (Array.isArray(many) && many.length > 0)
959
+ return many.includes(dbId);
960
+ if (one)
961
+ return one === dbId;
962
+ return true; // eligible everywhere if unspecified
963
+ });
964
+ // Fetch available tables from remote for selection context
965
+ const availableTables = await fetchAllCollections(dbId, controller.database);
966
+ // Determine selected table IDs
967
+ let selectedTableIds = [];
968
+ if (parsedArgv.collectionIds) {
969
+ const ids = parsedArgv.collectionIds.split(/[,\s]+/).filter(Boolean);
970
+ // Only allow IDs that are in eligible config items
971
+ const eligibleIds = new Set(eligibleConfigItems.map((c) => c.$id || c.id));
972
+ selectedTableIds = ids.filter(id => eligibleIds.has(id));
973
+ }
974
+ else {
975
+ selectedTableIds = await SelectionDialogs.selectTablesForDatabase(dbId, db.name, availableTables, eligibleConfigItems, { showSelectAll: false, allowNewOnly: true, defaultSelected: [] });
976
+ }
977
+ databaseSelections.push({
978
+ databaseId: db.$id,
979
+ databaseName: db.name,
980
+ tableIds: selectedTableIds,
981
+ tableNames: [],
982
+ isNew: false,
983
+ });
984
+ }
985
+ if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
986
+ MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
987
+ return;
988
+ }
989
+ const pushSummary = {
990
+ databases: databaseSelections.length,
991
+ collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
992
+ details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
993
+ };
994
+ const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
995
+ if (!confirmed) {
996
+ MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
997
+ return;
998
+ }
999
+ await controller.selectivePush(databaseSelections, []);
1000
+ operationStats.pushedDatabases = databaseSelections.length;
1001
+ operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
870
1002
  }
871
1003
  else if (parsedArgv.sync) {
872
1004
  // Enhanced SYNC: Pull from remote with intelligent configuration detection
@@ -905,7 +1037,9 @@ async function main() {
905
1037
  MessageFormatter.info(`Starting database transfer from ${parsedArgv.fromDbId} to ${parsedArgv.toDbId}`, { prefix: "Transfer" });
906
1038
  fromDb = (await controller.getDatabasesByIds([parsedArgv.fromDbId]))?.[0];
907
1039
  if (!fromDb) {
908
- MessageFormatter.error("Source database not found", undefined, { prefix: "Transfer" });
1040
+ MessageFormatter.error("Source database not found", undefined, {
1041
+ prefix: "Transfer",
1042
+ });
909
1043
  return;
910
1044
  }
911
1045
  if (isRemote) {
@@ -920,14 +1054,18 @@ async function main() {
920
1054
  const remoteDbs = await fetchAllDatabases(targetDatabases);
921
1055
  toDb = remoteDbs.find((db) => db.$id === parsedArgv.toDbId);
922
1056
  if (!toDb) {
923
- MessageFormatter.error("Target database not found", undefined, { prefix: "Transfer" });
1057
+ MessageFormatter.error("Target database not found", undefined, {
1058
+ prefix: "Transfer",
1059
+ });
924
1060
  return;
925
1061
  }
926
1062
  }
927
1063
  else {
928
1064
  toDb = (await controller.getDatabasesByIds([parsedArgv.toDbId]))?.[0];
929
1065
  if (!toDb) {
930
- MessageFormatter.error("Target database not found", undefined, { prefix: "Transfer" });
1066
+ MessageFormatter.error("Target database not found", undefined, {
1067
+ prefix: "Transfer",
1068
+ });
931
1069
  return;
932
1070
  }
933
1071
  }