appwrite-utils-cli 1.7.7 → 1.7.8

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.
package/src/main.ts CHANGED
@@ -1,32 +1,35 @@
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 path from "path";
19
- import fs from "fs";
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";
24
-
25
- const require = createRequire(import.meta.url);
26
- if (!(globalThis as any).require) {
27
- (globalThis as any).require = require;
28
- }
29
-
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 { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig } from "./utils/projectConfig.js";
25
+ import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus } from "./utils/sessionAuth.js";
26
+ import { findYamlConfig, loadYamlConfigWithSession } from "./config/yamlConfig.js";
27
+
28
+ const require = createRequire(import.meta.url);
29
+ if (!(globalThis as any).require) {
30
+ (globalThis as any).require = require;
31
+ }
32
+
30
33
 
31
34
  interface CliOptions {
32
35
  config?: string;
@@ -72,10 +75,228 @@ interface CliOptions {
72
75
  useSession?: boolean;
73
76
  session?: string;
74
77
  listBackups?: boolean;
78
+ autoSync?: boolean;
79
+ selectBuckets?: boolean;
75
80
  }
76
81
 
77
82
  type ParsedArgv = ArgumentsCamelCase<CliOptions>;
78
83
 
84
+ /**
85
+ * Enhanced sync function with intelligent configuration detection and selection dialogs
86
+ */
87
+ async function performEnhancedSync(
88
+ controller: UtilsController,
89
+ parsedArgv: ParsedArgv
90
+ ): Promise<SyncSelectionSummary | null> {
91
+ try {
92
+ MessageFormatter.banner("Enhanced Sync", "Intelligent configuration detection and selection");
93
+
94
+ if (!controller.config) {
95
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Sync" });
96
+ return null;
97
+ }
98
+
99
+ // Get all available databases from remote
100
+ const availableDatabases = await fetchAllDatabases(controller.database!);
101
+ if (availableDatabases.length === 0) {
102
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Sync" });
103
+ return null;
104
+ }
105
+
106
+ // Get existing configuration
107
+ const configuredDatabases = controller.config.databases || [];
108
+ const configuredBuckets = controller.config.buckets || [];
109
+
110
+ // Check if we have existing configuration
111
+ const hasExistingConfig = configuredDatabases.length > 0 || configuredBuckets.length > 0;
112
+
113
+ let syncExisting = false;
114
+ let modifyConfiguration = true;
115
+
116
+ if (hasExistingConfig) {
117
+ // Prompt about existing configuration
118
+ const response = await SelectionDialogs.promptForExistingConfig([
119
+ ...configuredDatabases,
120
+ ...configuredBuckets
121
+ ]);
122
+ syncExisting = response.syncExisting;
123
+ modifyConfiguration = response.modifyConfiguration;
124
+
125
+ if (syncExisting && !modifyConfiguration) {
126
+ // Just sync existing configuration without changes
127
+ MessageFormatter.info("Syncing existing configuration without modifications", { prefix: "Sync" });
128
+
129
+ // Convert configured databases to DatabaseSelection format
130
+ const databaseSelections: DatabaseSelection[] = configuredDatabases.map(db => ({
131
+ databaseId: db.$id,
132
+ databaseName: db.name,
133
+ tableIds: [], // Tables will be populated from collections config
134
+ tableNames: [],
135
+ isNew: false
136
+ }));
137
+
138
+ // Convert configured buckets to BucketSelection format
139
+ const bucketSelections: BucketSelection[] = configuredBuckets.map(bucket => ({
140
+ bucketId: bucket.$id,
141
+ bucketName: bucket.name,
142
+ databaseId: undefined,
143
+ databaseName: undefined,
144
+ isNew: false
145
+ }));
146
+
147
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
148
+ databaseSelections,
149
+ bucketSelections
150
+ );
151
+
152
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
153
+ if (!confirmed) {
154
+ MessageFormatter.info("Sync operation cancelled by user", { prefix: "Sync" });
155
+ return null;
156
+ }
157
+
158
+ // Perform sync with existing configuration
159
+ await controller.selectiveSync(databaseSelections, bucketSelections);
160
+ return selectionSummary;
161
+ }
162
+ }
163
+
164
+ if (!modifyConfiguration) {
165
+ MessageFormatter.info("No configuration changes requested", { prefix: "Sync" });
166
+ return null;
167
+ }
168
+
169
+ // Allow new items selection based on user choice
170
+ const allowNewOnly = !syncExisting;
171
+
172
+ // Select databases
173
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
174
+ availableDatabases,
175
+ configuredDatabases,
176
+ {
177
+ showSelectAll: true,
178
+ allowNewOnly,
179
+ defaultSelected: syncExisting ? configuredDatabases.map(db => db.$id) : []
180
+ }
181
+ );
182
+
183
+ if (selectedDatabaseIds.length === 0) {
184
+ MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
185
+ return null;
186
+ }
187
+
188
+ // For each selected database, get available tables and select them
189
+ const tableSelectionsMap = new Map<string, string[]>();
190
+ const availableTablesMap = new Map<string, any[]>();
191
+
192
+ for (const databaseId of selectedDatabaseIds) {
193
+ const database = availableDatabases.find(db => db.$id === databaseId)!;
194
+
195
+ SelectionDialogs.showProgress(`Fetching tables for database: ${database.name}`);
196
+
197
+ // Get available tables from remote
198
+ const availableTables = await fetchAllCollections(databaseId, controller.database!);
199
+ availableTablesMap.set(databaseId, availableTables);
200
+
201
+ // Get configured tables for this database
202
+ // Note: Collections are stored globally in the config, not per database
203
+ const configuredTables = controller.config.collections || [];
204
+
205
+ // Select tables for this database
206
+ const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
207
+ databaseId,
208
+ database.name,
209
+ availableTables,
210
+ configuredTables,
211
+ {
212
+ showSelectAll: true,
213
+ allowNewOnly,
214
+ defaultSelected: syncExisting ? configuredTables.map((t: any) => t.$id) : []
215
+ }
216
+ );
217
+
218
+ tableSelectionsMap.set(databaseId, selectedTableIds);
219
+
220
+ if (selectedTableIds.length === 0) {
221
+ MessageFormatter.warning(`No tables selected for database: ${database.name}`, { prefix: "Sync" });
222
+ }
223
+ }
224
+
225
+ // Select buckets
226
+ let selectedBucketIds: string[] = [];
227
+
228
+ // Get available buckets from remote
229
+ if (controller.storage) {
230
+ try {
231
+ // Note: We need to implement fetchAllBuckets or use storage.listBuckets
232
+ // For now, we'll use configured buckets as available
233
+ SelectionDialogs.showProgress("Fetching storage buckets...");
234
+
235
+ // Create a mock availableBuckets array - in real implementation,
236
+ // you'd fetch this from the Appwrite API
237
+ const availableBuckets = configuredBuckets; // Placeholder
238
+
239
+ selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
240
+ selectedDatabaseIds,
241
+ availableBuckets,
242
+ configuredBuckets,
243
+ {
244
+ showSelectAll: true,
245
+ allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
246
+ groupByDatabase: true,
247
+ defaultSelected: syncExisting ? configuredBuckets.map(b => b.$id) : []
248
+ }
249
+ );
250
+ } catch (error) {
251
+ MessageFormatter.warning("Could not fetch storage buckets", { prefix: "Sync" });
252
+ logger.warn("Failed to fetch buckets during sync", { error });
253
+ }
254
+ }
255
+
256
+ // Create selection objects
257
+ const databaseSelections = SelectionDialogs.createDatabaseSelection(
258
+ selectedDatabaseIds,
259
+ availableDatabases,
260
+ tableSelectionsMap,
261
+ configuredDatabases,
262
+ availableTablesMap
263
+ );
264
+
265
+ const bucketSelections = SelectionDialogs.createBucketSelection(
266
+ selectedBucketIds,
267
+ [], // availableBuckets - would be populated from API
268
+ configuredBuckets,
269
+ availableDatabases
270
+ );
271
+
272
+ // Show final confirmation
273
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
274
+ databaseSelections,
275
+ bucketSelections
276
+ );
277
+
278
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
279
+ if (!confirmed) {
280
+ MessageFormatter.info("Sync operation cancelled by user", { prefix: "Sync" });
281
+ return null;
282
+ }
283
+
284
+ // Perform the selective sync
285
+ await controller.selectiveSync(databaseSelections, bucketSelections);
286
+
287
+ MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
288
+ return selectionSummary;
289
+
290
+ } catch (error) {
291
+ SelectionDialogs.showError("Enhanced sync failed", error instanceof Error ? error : new Error(String(error)));
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Performs selective sync with the given database and bucket selections
298
+ */
299
+
79
300
  /**
80
301
  * Checks if the migration from collections to tables should be allowed
81
302
  * Returns an object with:
@@ -212,6 +433,15 @@ const argv = yargs(hideBin(process.argv))
212
433
  description:
213
434
  "Pull and synchronize your local config with the remote Appwrite project schema",
214
435
  })
436
+ .option("autoSync", {
437
+ alias: ["auto"],
438
+ type: "boolean",
439
+ description: "Skip prompts and sync all databases, tables, and buckets (current behavior)"
440
+ })
441
+ .option("selectBuckets", {
442
+ type: "boolean",
443
+ description: "Force bucket selection dialog even if buckets are already configured"
444
+ })
215
445
  .option("endpoint", {
216
446
  type: "string",
217
447
  description: "Set the Appwrite endpoint",
@@ -867,11 +1097,23 @@ async function main() {
867
1097
  operationStats.pushedDatabases = databases.length;
868
1098
  operationStats.pushedCollections = controller.config?.collections?.length || 0;
869
1099
  } else if (parsedArgv.sync) {
870
- // SYNC: Pull from remote
871
- const databases =
872
- options.databases || (await fetchAllDatabases(controller.database!));
873
- await controller.synchronizeConfigurations(databases);
874
- operationStats.syncedDatabases = databases.length;
1100
+ // Enhanced SYNC: Pull from remote with intelligent configuration detection
1101
+ if (parsedArgv.autoSync) {
1102
+ // Legacy behavior: sync everything without prompts
1103
+ MessageFormatter.info("Using auto-sync mode (legacy behavior)", { prefix: "Sync" });
1104
+ const databases =
1105
+ options.databases || (await fetchAllDatabases(controller.database!));
1106
+ await controller.synchronizeConfigurations(databases);
1107
+ operationStats.syncedDatabases = databases.length;
1108
+ } else {
1109
+ // Enhanced sync flow with selection dialogs
1110
+ const syncResult = await performEnhancedSync(controller, parsedArgv);
1111
+ if (syncResult) {
1112
+ operationStats.syncedDatabases = syncResult.databases.length;
1113
+ operationStats.syncedCollections = syncResult.totalTables;
1114
+ operationStats.syncedBuckets = syncResult.buckets.length;
1115
+ }
1116
+ }
875
1117
  }
876
1118
 
877
1119
  if (options.generateSchemas) {