appwrite-utils-cli 1.8.5 → 1.8.7

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.
@@ -182,11 +182,34 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
182
182
  }
183
183
  }
184
184
  }
185
- // Find existing table by name
186
- const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
187
- const items = list.tables || [];
188
- let table = items[0];
185
+ // Find existing table — prefer lookup by ID (if provided), then by name
186
+ let table;
189
187
  let tableId;
188
+ // 1) Try by explicit $id first (handles rename scenarios)
189
+ if (collectionData.$id) {
190
+ try {
191
+ const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
192
+ table = byId.data || byId.tables?.[0];
193
+ if (table?.$id) {
194
+ MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
195
+ }
196
+ }
197
+ catch {
198
+ // Not found by ID; fall back to name lookup
199
+ }
200
+ }
201
+ // 2) If not found by ID, try by name
202
+ if (!table) {
203
+ const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
204
+ const items = list.tables || [];
205
+ table = items[0];
206
+ if (table?.$id) {
207
+ // If local has $id that differs from remote, prefer remote (IDs are immutable)
208
+ if (collectionData.$id && collectionData.$id !== table.$id) {
209
+ MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
210
+ }
211
+ }
212
+ }
190
213
  if (!table) {
191
214
  // Determine ID (prefer provided $id or re-use deleted one)
192
215
  let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", ""));
@@ -235,6 +258,11 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
235
258
  const plusminus = plan.toUpdate.map((u) => u.attribute.key);
236
259
  const minus = plan.toRecreate.map((r) => r.newAttribute.key);
237
260
  const skip = plan.unchanged;
261
+ // Compute deletions (remote extras not present locally)
262
+ const desiredKeysForDelete = new Set((attributes || []).map((a) => a.key));
263
+ const extraRemoteKeys = (existingCols || [])
264
+ .map((c) => c?.key)
265
+ .filter((k) => !!k && !desiredKeysForDelete.has(k));
238
266
  const parts = [];
239
267
  if (plus.length)
240
268
  parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
@@ -244,6 +272,7 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
244
272
  parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
245
273
  if (skip.length)
246
274
  parts.push(`⏭️ ${skip.length}`);
275
+ parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
247
276
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
248
277
  // Execute
249
278
  const colResults = await executeColumnOperations(adapter, databaseId, tableId, plan);
@@ -458,6 +487,121 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
458
487
  catch (e) {
459
488
  MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
460
489
  }
490
+ // Deletions for indexes: remove remote indexes not declared in YAML/config
491
+ try {
492
+ const desiredIndexKeys = new Set((indexes || []).map((i) => i.key));
493
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
494
+ const existingIdx = idxRes.data || idxRes.indexes || [];
495
+ const extraIdx = existingIdx
496
+ .filter((i) => i?.key && !desiredIndexKeys.has(i.key))
497
+ .map((i) => i.key);
498
+ if (extraIdx.length > 0) {
499
+ MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
500
+ const deleted = [];
501
+ const errors = [];
502
+ for (const key of extraIdx) {
503
+ try {
504
+ await adapter.deleteIndex({ databaseId, tableId, key });
505
+ // Optionally wait for index to disappear
506
+ const start = Date.now();
507
+ const maxWait = 30000;
508
+ while (Date.now() - start < maxWait) {
509
+ try {
510
+ const li = await adapter.listIndexes({ databaseId, tableId });
511
+ const list = li.data || li.indexes || [];
512
+ if (!list.find((ix) => ix.key === key))
513
+ break;
514
+ }
515
+ catch { }
516
+ await delay(1000);
517
+ }
518
+ deleted.push(key);
519
+ }
520
+ catch (e) {
521
+ errors.push({ key, error: e?.message || String(e) });
522
+ }
523
+ }
524
+ if (deleted.length) {
525
+ MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
526
+ }
527
+ if (errors.length) {
528
+ MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
529
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
530
+ }
531
+ }
532
+ else {
533
+ MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
534
+ }
535
+ }
536
+ catch (e) {
537
+ MessageFormatter.warning(`Could not evaluate index deletions: ${e?.message || e}`, { prefix: 'Indexes' });
538
+ }
539
+ // Deletions: remove columns/attributes that are present remotely but not in desired config
540
+ try {
541
+ const desiredKeys = new Set((attributes || []).map((a) => a.key));
542
+ const tableInfo3 = await adapter.getTable({ databaseId, tableId });
543
+ const existingCols3 = tableInfo3.data?.columns || tableInfo3.data?.attributes || [];
544
+ const toDelete = existingCols3
545
+ .filter((col) => col?.key && !desiredKeys.has(col.key))
546
+ .map((col) => col.key);
547
+ if (toDelete.length > 0) {
548
+ MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
549
+ const deleted = [];
550
+ const errors = [];
551
+ for (const key of toDelete) {
552
+ try {
553
+ // Drop any indexes that reference this attribute to avoid server errors
554
+ try {
555
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
556
+ const ilist = idxRes.data || idxRes.indexes || [];
557
+ for (const idx of ilist) {
558
+ const attrs = Array.isArray(idx.attributes) ? idx.attributes : [];
559
+ if (attrs.includes(key)) {
560
+ MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
561
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
562
+ await delay(500);
563
+ }
564
+ }
565
+ }
566
+ catch { }
567
+ await adapter.deleteAttribute({ databaseId, tableId, key });
568
+ // Wait briefly for deletion to settle
569
+ const start = Date.now();
570
+ const maxWaitMs = 60000;
571
+ while (Date.now() - start < maxWaitMs) {
572
+ try {
573
+ const tinfo = await adapter.getTable({ databaseId, tableId });
574
+ const cols = tinfo.data?.columns || tinfo.data?.attributes || [];
575
+ const found = cols.find((c) => c.key === key);
576
+ if (!found)
577
+ break;
578
+ if (found.status && found.status !== 'deleting')
579
+ break;
580
+ }
581
+ catch { }
582
+ await delay(1000);
583
+ }
584
+ deleted.push(key);
585
+ }
586
+ catch (e) {
587
+ errors.push({ key, error: e?.message || String(e) });
588
+ }
589
+ }
590
+ if (deleted.length) {
591
+ MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
592
+ }
593
+ if (errors.length) {
594
+ MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
595
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
596
+ }
597
+ }
598
+ else {
599
+ MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
600
+ }
601
+ }
602
+ catch (e) {
603
+ MessageFormatter.warning(`Could not evaluate deletions: ${e?.message || e}`, { prefix: 'Attributes' });
604
+ }
461
605
  // Mark this table as fully processed to prevent re-processing
462
606
  markCollectionProcessed(tableId, collectionData.name);
463
607
  }
@@ -619,6 +619,15 @@ export class UtilsController {
619
619
  MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
620
620
  return;
621
621
  }
622
+ // Always reload config from disk so pushes use current local YAML/Ts definitions
623
+ try {
624
+ await this.reloadConfig();
625
+ MessageFormatter.info("Reloaded config from disk for push", { prefix: "Controller" });
626
+ }
627
+ catch (e) {
628
+ // Non-fatal; continue with existing config
629
+ MessageFormatter.warning("Could not reload config; continuing with current in-memory config", { prefix: "Controller" });
630
+ }
622
631
  MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
623
632
  // Convert database selections to Models.Database format
624
633
  const selectedDatabases = [];
@@ -665,7 +674,8 @@ export class UtilsController {
665
674
  // Check if this collection was selected for THIS database
666
675
  if (dbSelection.tableIds.includes(collectionId)) {
667
676
  collectionsForDatabase.push(collection);
668
- MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId}`, { prefix: "Controller" });
677
+ const source = collection._isFromTablesDir ? 'tables/' : 'collections/';
678
+ MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId} [source: ${source}]`, { prefix: "Controller" });
669
679
  }
670
680
  }
671
681
  databaseCollectionsMap.set(dbSelection.databaseId, collectionsForDatabase);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.8.5",
4
+ "version": "1.8.7",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -262,45 +262,69 @@ export const createOrUpdateCollectionsViaAdapter = async (
262
262
  }
263
263
  }
264
264
 
265
- // Find existing table by name
266
- const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
267
- const items: any[] = (list as any).tables || [];
268
- let table = items[0];
269
- let tableId: string;
270
-
271
- if (!table) {
272
- // Determine ID (prefer provided $id or re-use deleted one)
273
- let foundColl = deletedCollections?.find(
274
- (coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
275
- );
276
- if (collectionData.$id) tableId = collectionData.$id;
277
- else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
278
- else tableId = ID.unique();
279
- usedIds.add(tableId);
280
-
281
- const res = await adapter.createTable({
282
- databaseId,
283
- id: tableId,
284
- name: collectionData.name,
285
- permissions,
286
- documentSecurity: !!collectionData.documentSecurity,
287
- enabled: collectionData.enabled !== false
288
- });
289
- table = (res as any).data || res;
290
- nameToIdMapping.set(collectionData.name, tableId);
291
- } else {
292
- tableId = table.$id;
293
- await adapter.updateTable({
294
- databaseId,
295
- id: tableId,
296
- name: collectionData.name,
297
- permissions,
298
- documentSecurity: !!collectionData.documentSecurity,
299
- enabled: collectionData.enabled !== false
300
- });
301
- // Cache the existing table ID
302
- nameToIdMapping.set(collectionData.name, tableId);
303
- }
265
+ // Find existing table — prefer lookup by ID (if provided), then by name
266
+ let table: any | undefined;
267
+ let tableId: string;
268
+
269
+ // 1) Try by explicit $id first (handles rename scenarios)
270
+ if (collectionData.$id) {
271
+ try {
272
+ const byId = await adapter.getTable({ databaseId, tableId: collectionData.$id });
273
+ table = (byId as any).data || (byId as any).tables?.[0];
274
+ if (table?.$id) {
275
+ MessageFormatter.info(`Found existing table by ID: ${table.$id}`, { prefix: 'Tables' });
276
+ }
277
+ } catch {
278
+ // Not found by ID; fall back to name lookup
279
+ }
280
+ }
281
+
282
+ // 2) If not found by ID, try by name
283
+ if (!table) {
284
+ const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
285
+ const items: any[] = (list as any).tables || [];
286
+ table = items[0];
287
+ if (table?.$id) {
288
+ // If local has $id that differs from remote, prefer remote (IDs are immutable)
289
+ if (collectionData.$id && collectionData.$id !== table.$id) {
290
+ MessageFormatter.warning(`Config $id '${collectionData.$id}' differs from existing table ID '${table.$id}'. Using existing table.`, { prefix: 'Tables' });
291
+ }
292
+ }
293
+ }
294
+
295
+ if (!table) {
296
+ // Determine ID (prefer provided $id or re-use deleted one)
297
+ let foundColl = deletedCollections?.find(
298
+ (coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
299
+ );
300
+ if (collectionData.$id) tableId = collectionData.$id;
301
+ else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
302
+ else tableId = ID.unique();
303
+ usedIds.add(tableId);
304
+
305
+ const res = await adapter.createTable({
306
+ databaseId,
307
+ id: tableId,
308
+ name: collectionData.name,
309
+ permissions,
310
+ documentSecurity: !!collectionData.documentSecurity,
311
+ enabled: collectionData.enabled !== false
312
+ });
313
+ table = (res as any).data || res;
314
+ nameToIdMapping.set(collectionData.name, tableId);
315
+ } else {
316
+ tableId = table.$id;
317
+ await adapter.updateTable({
318
+ databaseId,
319
+ id: tableId,
320
+ name: collectionData.name,
321
+ permissions,
322
+ documentSecurity: !!collectionData.documentSecurity,
323
+ enabled: collectionData.enabled !== false
324
+ });
325
+ // Cache the existing table ID
326
+ nameToIdMapping.set(collectionData.name, tableId);
327
+ }
304
328
 
305
329
  // Add small delay after table create/update
306
330
  await delay(250);
@@ -319,11 +343,18 @@ export const createOrUpdateCollectionsViaAdapter = async (
319
343
  const minus = plan.toRecreate.map((r: any) => (r.newAttribute as any).key);
320
344
  const skip = plan.unchanged;
321
345
 
346
+ // Compute deletions (remote extras not present locally)
347
+ const desiredKeysForDelete = new Set((attributes || []).map((a: any) => a.key));
348
+ const extraRemoteKeys = (existingCols || [])
349
+ .map((c: any) => c?.key)
350
+ .filter((k: any): k is string => !!k && !desiredKeysForDelete.has(k));
351
+
322
352
  const parts: string[] = [];
323
353
  if (plus.length) parts.push(`➕ ${plus.length} (${plus.join(', ')})`);
324
354
  if (plusminus.length) parts.push(`🔧 ${plusminus.length} (${plusminus.join(', ')})`);
325
355
  if (minus.length) parts.push(`♻️ ${minus.length} (${minus.join(', ')})`);
326
356
  if (skip.length) parts.push(`⏭️ ${skip.length}`);
357
+ parts.push(`🗑️ ${extraRemoteKeys.length}${extraRemoteKeys.length ? ` (${extraRemoteKeys.join(', ')})` : ''}`);
327
358
  MessageFormatter.info(`Plan → ${parts.join(' | ') || 'no changes'}`, { prefix: 'Attributes' });
328
359
 
329
360
  // Execute
@@ -510,10 +541,117 @@ export const createOrUpdateCollectionsViaAdapter = async (
510
541
  } catch (e) {
511
542
  MessageFormatter.error(`Failed to list/create indexes`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
512
543
  }
513
-
514
- // Mark this table as fully processed to prevent re-processing
515
- markCollectionProcessed(tableId, collectionData.name);
516
- }
544
+
545
+ // Deletions for indexes: remove remote indexes not declared in YAML/config
546
+ try {
547
+ const desiredIndexKeys = new Set((indexes || []).map((i: any) => i.key));
548
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
549
+ const existingIdx: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
550
+ const extraIdx = existingIdx
551
+ .filter((i: any) => i?.key && !desiredIndexKeys.has(i.key))
552
+ .map((i: any) => i.key as string);
553
+ if (extraIdx.length > 0) {
554
+ MessageFormatter.info(`Plan → 🗑️ ${extraIdx.length} indexes (${extraIdx.join(', ')})`, { prefix: 'Indexes' });
555
+ const deleted: string[] = [];
556
+ const errors: Array<{ key: string; error: string }> = [];
557
+ for (const key of extraIdx) {
558
+ try {
559
+ await adapter.deleteIndex({ databaseId, tableId, key });
560
+ // Optionally wait for index to disappear
561
+ const start = Date.now();
562
+ const maxWait = 30000;
563
+ while (Date.now() - start < maxWait) {
564
+ try {
565
+ const li = await adapter.listIndexes({ databaseId, tableId });
566
+ const list: any[] = (li as any).data || (li as any).indexes || [];
567
+ if (!list.find((ix: any) => ix.key === key)) break;
568
+ } catch {}
569
+ await delay(1000);
570
+ }
571
+ deleted.push(key);
572
+ } catch (e: any) {
573
+ errors.push({ key, error: e?.message || String(e) });
574
+ }
575
+ }
576
+ if (deleted.length) {
577
+ MessageFormatter.success(`Deleted ${deleted.length} indexes: ${deleted.join(', ')}`, { prefix: 'Indexes' });
578
+ }
579
+ if (errors.length) {
580
+ MessageFormatter.error(`${errors.length} index deletions failed`, undefined, { prefix: 'Indexes' });
581
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Indexes' }));
582
+ }
583
+ } else {
584
+ MessageFormatter.info(`Plan → 🗑️ 0 indexes`, { prefix: 'Indexes' });
585
+ }
586
+ } catch (e) {
587
+ MessageFormatter.warning(`Could not evaluate index deletions: ${(e as Error)?.message || e}`, { prefix: 'Indexes' });
588
+ }
589
+
590
+ // Deletions: remove columns/attributes that are present remotely but not in desired config
591
+ try {
592
+ const desiredKeys = new Set((attributes || []).map((a: any) => a.key));
593
+ const tableInfo3 = await adapter.getTable({ databaseId, tableId });
594
+ const existingCols3: any[] = (tableInfo3 as any).data?.columns || (tableInfo3 as any).data?.attributes || [];
595
+ const toDelete = existingCols3
596
+ .filter((col: any) => col?.key && !desiredKeys.has(col.key))
597
+ .map((col: any) => col.key as string);
598
+
599
+ if (toDelete.length > 0) {
600
+ MessageFormatter.info(`Plan → 🗑️ ${toDelete.length} (${toDelete.join(', ')})`, { prefix: 'Attributes' });
601
+ const deleted: string[] = [];
602
+ const errors: Array<{ key: string; error: string }> = [];
603
+ for (const key of toDelete) {
604
+ try {
605
+ // Drop any indexes that reference this attribute to avoid server errors
606
+ try {
607
+ const idxRes = await adapter.listIndexes({ databaseId, tableId });
608
+ const ilist: any[] = (idxRes as any).data || (idxRes as any).indexes || [];
609
+ for (const idx of ilist) {
610
+ const attrs: string[] = Array.isArray(idx.attributes) ? idx.attributes : [];
611
+ if (attrs.includes(key)) {
612
+ MessageFormatter.info(`🗑️ Deleting index '${idx.key}' referencing '${key}'`, { prefix: 'Indexes' });
613
+ await adapter.deleteIndex({ databaseId, tableId, key: idx.key });
614
+ await delay(500);
615
+ }
616
+ }
617
+ } catch {}
618
+
619
+ await adapter.deleteAttribute({ databaseId, tableId, key });
620
+ // Wait briefly for deletion to settle
621
+ const start = Date.now();
622
+ const maxWaitMs = 60000;
623
+ while (Date.now() - start < maxWaitMs) {
624
+ try {
625
+ const tinfo = await adapter.getTable({ databaseId, tableId });
626
+ const cols = (tinfo as any).data?.columns || (tinfo as any).data?.attributes || [];
627
+ const found = cols.find((c: any) => c.key === key);
628
+ if (!found) break;
629
+ if (found.status && found.status !== 'deleting') break;
630
+ } catch {}
631
+ await delay(1000);
632
+ }
633
+ deleted.push(key);
634
+ } catch (e: any) {
635
+ errors.push({ key, error: e?.message || String(e) });
636
+ }
637
+ }
638
+ if (deleted.length) {
639
+ MessageFormatter.success(`Deleted ${deleted.length} attributes: ${deleted.join(', ')}`, { prefix: 'Attributes' });
640
+ }
641
+ if (errors.length) {
642
+ MessageFormatter.error(`${errors.length} deletions failed`, undefined, { prefix: 'Attributes' });
643
+ errors.forEach(er => MessageFormatter.error(` ${er.key}: ${er.error}`, undefined, { prefix: 'Attributes' }));
644
+ }
645
+ } else {
646
+ MessageFormatter.info(`Plan → 🗑️ 0`, { prefix: 'Attributes' });
647
+ }
648
+ } catch (e) {
649
+ MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
650
+ }
651
+
652
+ // Mark this table as fully processed to prevent re-processing
653
+ markCollectionProcessed(tableId, collectionData.name);
654
+ }
517
655
 
518
656
  // Process queued relationships once mapping likely populated
519
657
  if (relQueue.length > 0) {
@@ -848,15 +848,24 @@ export class UtilsController {
848
848
  MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
849
849
  }
850
850
 
851
- async selectivePush(
852
- databaseSelections: DatabaseSelection[],
853
- bucketSelections: BucketSelection[]
854
- ): Promise<void> {
855
- await this.init();
856
- if (!this.database) {
857
- MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
858
- return;
859
- }
851
+ async selectivePush(
852
+ databaseSelections: DatabaseSelection[],
853
+ bucketSelections: BucketSelection[]
854
+ ): Promise<void> {
855
+ await this.init();
856
+ if (!this.database) {
857
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
858
+ return;
859
+ }
860
+
861
+ // Always reload config from disk so pushes use current local YAML/Ts definitions
862
+ try {
863
+ await this.reloadConfig();
864
+ MessageFormatter.info("Reloaded config from disk for push", { prefix: "Controller" });
865
+ } catch (e) {
866
+ // Non-fatal; continue with existing config
867
+ MessageFormatter.warning("Could not reload config; continuing with current in-memory config", { prefix: "Controller" });
868
+ }
860
869
 
861
870
  MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
862
871
 
@@ -900,7 +909,7 @@ export class UtilsController {
900
909
  const databaseCollectionsMap = new Map<string, any[]>();
901
910
 
902
911
  // Get all collections/tables from config (they're at the root level, not nested in databases)
903
- const allCollections = this.config?.collections || this.config?.tables || [];
912
+ const allCollections = this.config?.collections || this.config?.tables || [];
904
913
 
905
914
  // Create database-specific collection mapping to preserve relationships
906
915
  for (const dbSelection of databaseSelections) {
@@ -908,16 +917,17 @@ export class UtilsController {
908
917
 
909
918
  MessageFormatter.info(`Processing collections for database: ${dbSelection.databaseId}`, { prefix: "Controller" });
910
919
 
911
- // Filter collections that were selected for THIS specific database
912
- for (const collection of allCollections) {
913
- const collectionId = collection.$id || (collection as any).id;
914
-
915
- // Check if this collection was selected for THIS database
916
- if (dbSelection.tableIds.includes(collectionId)) {
917
- collectionsForDatabase.push(collection);
918
- MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId}`, { prefix: "Controller" });
919
- }
920
- }
920
+ // Filter collections that were selected for THIS specific database
921
+ for (const collection of allCollections) {
922
+ const collectionId = collection.$id || (collection as any).id;
923
+
924
+ // Check if this collection was selected for THIS database
925
+ if (dbSelection.tableIds.includes(collectionId)) {
926
+ collectionsForDatabase.push(collection);
927
+ const source = (collection as any)._isFromTablesDir ? 'tables/' : 'collections/';
928
+ MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId} [source: ${source}]`, { prefix: "Controller" });
929
+ }
930
+ }
921
931
 
922
932
  databaseCollectionsMap.set(dbSelection.databaseId, collectionsForDatabase);
923
933
  MessageFormatter.info(`Database ${dbSelection.databaseId}: ${collectionsForDatabase.length} collections selected`, { prefix: "Controller" });