carto-cli 0.1.0-rc.1

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.
@@ -0,0 +1,1022 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapsList = mapsList;
4
+ exports.mapsGet = mapsGet;
5
+ exports.mapsDelete = mapsDelete;
6
+ exports.mapsCopy = mapsCopy;
7
+ exports.mapsCreate = mapsCreate;
8
+ exports.mapsUpdate = mapsUpdate;
9
+ exports.mapsClone = mapsClone;
10
+ const api_1 = require("../api");
11
+ const colors_1 = require("../colors");
12
+ const fs_1 = require("fs");
13
+ const prompt_1 = require("../prompt");
14
+ async function mapsList(options, token, baseUrl, jsonOutput, debug = false, profile) {
15
+ try {
16
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
17
+ let result;
18
+ let userId;
19
+ // If --mine flag is set, get current user ID
20
+ if (options.mine) {
21
+ const userInfo = await client.getAccounts('/users/me', true);
22
+ userId = userInfo.user_id;
23
+ }
24
+ if (options.all) {
25
+ // Fetch all pages automatically using Workspace API
26
+ const baseParams = {};
27
+ if (options.orderBy)
28
+ baseParams['order_by'] = options.orderBy;
29
+ if (options.orderDirection)
30
+ baseParams['order_direction'] = options.orderDirection;
31
+ if (options.search)
32
+ baseParams['search'] = options.search;
33
+ if (options.privacy)
34
+ baseParams['privacy'] = options.privacy;
35
+ if (options.tags)
36
+ baseParams['tags'] = JSON.stringify(options.tags);
37
+ if (userId)
38
+ baseParams['user_id'] = userId;
39
+ const paginatedResult = await client.getAllPaginatedWorkspace('/maps', baseParams);
40
+ result = {
41
+ data: paginatedResult.data,
42
+ meta: {
43
+ page: {
44
+ total: paginatedResult.total,
45
+ current: 1,
46
+ pageSize: paginatedResult.total,
47
+ totalPages: 1
48
+ }
49
+ }
50
+ };
51
+ }
52
+ else {
53
+ // Fetch single page using Workspace API
54
+ const params = new URLSearchParams();
55
+ if (options.orderBy)
56
+ params.append('order_by', options.orderBy);
57
+ if (options.orderDirection)
58
+ params.append('order_direction', options.orderDirection);
59
+ if (options.pageSize)
60
+ params.append('page_size', options.pageSize);
61
+ if (options.page)
62
+ params.append('page', options.page);
63
+ if (options.search)
64
+ params.append('search', options.search);
65
+ if (options.privacy)
66
+ params.append('privacy', options.privacy);
67
+ if (options.tags)
68
+ params.append('tags', JSON.stringify(options.tags));
69
+ if (userId)
70
+ params.append('user_id', userId);
71
+ const queryString = params.toString();
72
+ const path = `/maps${queryString ? '?' + queryString : ''}`;
73
+ result = await client.getWorkspace(path);
74
+ }
75
+ if (jsonOutput) {
76
+ console.log(JSON.stringify(result));
77
+ }
78
+ else {
79
+ const maps = result.data || result.results || result;
80
+ const total = result.meta?.page?.total || result.total || maps.length;
81
+ const currentPage = result.meta?.page?.current || result.page || 1;
82
+ const pageSize = result.meta?.page?.pageSize || result.page_size || 12;
83
+ const totalPages = result.meta?.page?.totalPages || Math.ceil(total / pageSize);
84
+ // Display header with search query if present
85
+ const header = options.search
86
+ ? `\n${total} maps found for '${options.search}':\n`
87
+ : `\nMaps (${total}):\n`;
88
+ console.log((0, colors_1.bold)(header));
89
+ maps.forEach((map) => {
90
+ console.log((0, colors_1.bold)('Map ID: ') + map.id);
91
+ console.log(' Name: ' + (map.title || '(Untitled)'));
92
+ console.log(' Owner: ' + (map.ownerEmail || 'N/A'));
93
+ console.log(' Privacy: ' + (map.privacy || 'N/A'));
94
+ console.log(' Views: ' + (map.views !== undefined ? map.views : 'N/A'));
95
+ if (map.collaborative)
96
+ console.log(' Collaborative: Yes');
97
+ if (map.tags && map.tags.length > 0) {
98
+ console.log(' Tags: ' + map.tags.join(', '));
99
+ }
100
+ console.log(' Created: ' + new Date(map.createdAt || map.created_at).toLocaleString());
101
+ console.log(' Updated: ' + new Date(map.updatedAt || map.updated_at).toLocaleString());
102
+ console.log('');
103
+ });
104
+ if (options.all) {
105
+ console.log((0, colors_1.dim)(`Fetched all ${total} maps`));
106
+ }
107
+ else if (totalPages > 1) {
108
+ console.log((0, colors_1.dim)(`Page ${currentPage} of ${totalPages} (use --all to fetch all pages)`));
109
+ }
110
+ }
111
+ }
112
+ catch (err) {
113
+ if (jsonOutput) {
114
+ console.log(JSON.stringify({ success: false, error: err.message }));
115
+ }
116
+ else {
117
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
118
+ console.log((0, colors_1.error)('✗ Authentication required'));
119
+ console.log('Please run: carto auth login');
120
+ }
121
+ else {
122
+ console.log((0, colors_1.error)('✗ Failed to list maps: ' + err.message));
123
+ }
124
+ }
125
+ process.exit(1);
126
+ }
127
+ }
128
+ async function mapsGet(mapId, token, baseUrl, jsonOutput, debug = false, profile) {
129
+ try {
130
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
131
+ // Fetch map details and datasets from Workspace API
132
+ const map = await client.getWorkspace(`/maps/${mapId}`);
133
+ const datasets = await client.getWorkspace(`/maps/${mapId}/datasets`);
134
+ if (jsonOutput) {
135
+ console.log(JSON.stringify({ map, datasets }));
136
+ }
137
+ else {
138
+ // Display map metadata
139
+ console.log((0, colors_1.bold)('\n=== Map Details ===\n'));
140
+ console.log((0, colors_1.bold)('ID: ') + map.id);
141
+ console.log((0, colors_1.bold)('Title: ') + (map.title || '(Untitled)'));
142
+ if (map.description)
143
+ console.log((0, colors_1.bold)('Description: ') + map.description);
144
+ console.log((0, colors_1.bold)('Owner: ') + (map.ownerEmail || 'N/A'));
145
+ console.log((0, colors_1.bold)('Privacy: ') + (map.privacy || 'N/A'));
146
+ console.log((0, colors_1.bold)('Views: ') + (map.views !== undefined ? map.views : 'N/A'));
147
+ console.log((0, colors_1.bold)('Collaborative: ') + (map.collaborative ? 'Yes' : 'No'));
148
+ console.log((0, colors_1.bold)('Agent Enabled: ') + (map.agentEnabled || map.agent?.enabledForViewer ? 'Yes' : 'No'));
149
+ if (map.publishedWithPassword)
150
+ console.log((0, colors_1.bold)('Password Protected: ') + 'Yes');
151
+ if (map.sharingScope)
152
+ console.log((0, colors_1.bold)('Sharing Scope: ') + map.sharingScope);
153
+ // Tags
154
+ if (map.tags && map.tags.length > 0) {
155
+ console.log((0, colors_1.bold)('Tags: ') + map.tags.join(', '));
156
+ }
157
+ // Timestamps
158
+ console.log((0, colors_1.bold)('Created: ') + new Date(map.createdAt).toLocaleString());
159
+ console.log((0, colors_1.bold)('Updated: ') + new Date(map.updatedAt).toLocaleString());
160
+ // Datasets and Connections
161
+ if (datasets && datasets.length > 0) {
162
+ console.log((0, colors_1.bold)(`\n=== Datasets (${datasets.length}) ===\n`));
163
+ // Extract unique connections
164
+ const connectionsMap = new Map();
165
+ datasets.forEach((ds) => {
166
+ if (ds.connectionId && ds.connectionName) {
167
+ connectionsMap.set(ds.connectionId, {
168
+ id: ds.connectionId,
169
+ name: ds.connectionName
170
+ });
171
+ }
172
+ });
173
+ // Display connections
174
+ if (connectionsMap.size > 0) {
175
+ console.log((0, colors_1.bold)('Connections used:'));
176
+ connectionsMap.forEach((conn) => {
177
+ console.log(` • ${conn.name} (${conn.id})`);
178
+ });
179
+ console.log('');
180
+ }
181
+ // Display datasets
182
+ console.log((0, colors_1.bold)('Datasets:'));
183
+ datasets.forEach((ds, idx) => {
184
+ console.log(` ${idx + 1}. ${(0, colors_1.bold)(ds.label || ds.name || ds.id)}`);
185
+ console.log(` Connection: ${ds.connectionName || 'N/A'}`);
186
+ if (ds.source)
187
+ console.log(` Source: ${ds.source}`);
188
+ if (ds.type)
189
+ console.log(` Type: ${ds.type}`);
190
+ console.log('');
191
+ });
192
+ }
193
+ else {
194
+ console.log((0, colors_1.dim)('\nNo datasets found for this map'));
195
+ }
196
+ // Map URL
197
+ const mapUrl = `${client.workspaceUrl}/map/${mapId}`;
198
+ console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
199
+ }
200
+ }
201
+ catch (err) {
202
+ if (jsonOutput) {
203
+ console.log(JSON.stringify({ success: false, error: err.message }));
204
+ }
205
+ else {
206
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
207
+ console.log((0, colors_1.error)('✗ Authentication required'));
208
+ console.log('Please run: carto auth login');
209
+ }
210
+ else if (err.message.includes('404')) {
211
+ console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
212
+ }
213
+ else {
214
+ console.log((0, colors_1.error)('✗ Failed to get map: ' + err.message));
215
+ }
216
+ }
217
+ process.exit(1);
218
+ }
219
+ }
220
+ async function mapsDelete(mapId, token, baseUrl, jsonOutput, debug = false, profile, yes) {
221
+ try {
222
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
223
+ // Skip confirmation if --yes or --json flag is set
224
+ const skipConfirmation = yes || jsonOutput;
225
+ if (!skipConfirmation) {
226
+ // Fetch map details to show in confirmation prompt
227
+ let mapDetails;
228
+ try {
229
+ mapDetails = await client.getWorkspace(`/maps/${mapId}`);
230
+ }
231
+ catch (err) {
232
+ // If we can't fetch details, proceed with generic message
233
+ console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete this map.'));
234
+ console.log((0, colors_1.warning)(` Map ID: ${mapId}`));
235
+ }
236
+ if (mapDetails) {
237
+ console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete:'));
238
+ console.log(` Map: "${mapDetails.title || '(Untitled)'}"`);
239
+ console.log(` ID: ${mapId}`);
240
+ if (mapDetails.ownerEmail) {
241
+ console.log(` Owner: ${mapDetails.ownerEmail}`);
242
+ }
243
+ if (mapDetails.privacy) {
244
+ console.log(` Privacy: ${mapDetails.privacy}`);
245
+ }
246
+ }
247
+ console.log('');
248
+ const confirmed = await (0, prompt_1.promptForConfirmation)("Type 'delete' to confirm: ", 'delete');
249
+ if (!confirmed) {
250
+ console.log('\nDeletion cancelled');
251
+ process.exit(0);
252
+ }
253
+ console.log('');
254
+ }
255
+ await client.delete(`/v3/organization-maps/${mapId}`);
256
+ if (jsonOutput) {
257
+ console.log(JSON.stringify({ success: true, message: 'Map deleted successfully', id: mapId }));
258
+ }
259
+ else {
260
+ console.log((0, colors_1.success)(`✓ Map ${mapId} deleted successfully`));
261
+ }
262
+ }
263
+ catch (err) {
264
+ if (jsonOutput) {
265
+ console.log(JSON.stringify({ success: false, error: err.message }));
266
+ }
267
+ else {
268
+ console.log((0, colors_1.error)('✗ Failed to delete map: ' + err.message));
269
+ }
270
+ process.exit(1);
271
+ }
272
+ }
273
+ /**
274
+ * Check if CartoAI is enabled in the destination account
275
+ * Returns true if enabled, false otherwise
276
+ */
277
+ async function checkCartoAIEnabled(client, jsonOutput) {
278
+ try {
279
+ const settings = await client.getWorkspace('/settings/carto-ai');
280
+ return settings?.enabled === true;
281
+ }
282
+ catch (err) {
283
+ // If endpoint doesn't exist or returns error, assume not enabled
284
+ if (!jsonOutput) {
285
+ // Only log in debug mode, don't pollute normal output
286
+ }
287
+ return false;
288
+ }
289
+ }
290
+ /**
291
+ * Copy agent configuration from source map to destination map
292
+ * Uses PATCH /maps/:id endpoint which properly checks CartoAI settings
293
+ */
294
+ async function copyMapAgent(sourceClient, destClient, sourceMapId, destMapId, datasetsMapping, jsonOutput) {
295
+ try {
296
+ // Get source map to extract agent field
297
+ const sourceMap = await sourceClient.getWorkspace(`/maps/${sourceMapId}`);
298
+ if (!sourceMap.agent || !sourceMap.agent.config) {
299
+ // No agent configured in source
300
+ return false;
301
+ }
302
+ // Clone agent configuration
303
+ const agentConfig = JSON.parse(JSON.stringify(sourceMap.agent));
304
+ // Replace dataset IDs in agent config if needed (some agent configs may reference datasets)
305
+ let agentConfigStr = JSON.stringify(agentConfig);
306
+ for (const [oldId, newId] of Object.entries(datasetsMapping)) {
307
+ agentConfigStr = agentConfigStr.replace(new RegExp(oldId, 'g'), newId);
308
+ }
309
+ const updatedAgentConfig = JSON.parse(agentConfigStr);
310
+ // Update destination map with agent configuration
311
+ // Backend will handle:
312
+ // - CartoAI settings validation via ModelsService
313
+ // - Agent token creation
314
+ // - Workflow tool filtering
315
+ await destClient.patchWorkspace(`/maps/${destMapId}`, {
316
+ agent: updatedAgentConfig
317
+ });
318
+ return true;
319
+ }
320
+ catch (err) {
321
+ throw new Error(`Failed to copy agent: ${err.message}`);
322
+ }
323
+ }
324
+ /**
325
+ * Validate that a dataset source is accessible in the destination connection
326
+ * Uses dry-run SQL queries with WHERE 1=0 to test without transferring data
327
+ */
328
+ async function validateDatasetSource(client, connectionName, dataset, jsonOutput) {
329
+ try {
330
+ let validationSQL;
331
+ // Build dry-run query based on dataset type
332
+ if (dataset.type === 'table') {
333
+ // For tables: SELECT * FROM table WHERE 1=0 LIMIT 1
334
+ validationSQL = `SELECT * FROM ${dataset.source} WHERE 1=0 LIMIT 1`;
335
+ }
336
+ else if (dataset.type === 'query') {
337
+ // For queries: wrap in subquery with WHERE 1=0
338
+ validationSQL = `SELECT * FROM (${dataset.source}) AS _validate WHERE 1=0`;
339
+ }
340
+ else {
341
+ // For tilesets or other types, treat as table
342
+ validationSQL = `SELECT * FROM ${dataset.source} WHERE 1=0 LIMIT 1`;
343
+ }
344
+ if (!jsonOutput) {
345
+ const displaySource = dataset.source.length > 60
346
+ ? dataset.source.substring(0, 57) + '...'
347
+ : dataset.source;
348
+ console.log((0, colors_1.dim)(` ✓ ${dataset.label || dataset.name || 'Unnamed'} → ${displaySource}`));
349
+ }
350
+ // Execute validation query via SQL API
351
+ await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/query`, {
352
+ q: validationSQL,
353
+ queryParameters: {}
354
+ });
355
+ // Success - source is accessible
356
+ return { accessible: true };
357
+ }
358
+ catch (err) {
359
+ // Extract meaningful error message from API response
360
+ let errorMsg = 'Unknown error';
361
+ if (err.message) {
362
+ errorMsg = err.message;
363
+ // Try to extract detail from API error JSON
364
+ try {
365
+ const match = err.message.match(/"detail":"([^"]+)"/);
366
+ if (match) {
367
+ errorMsg = match[1];
368
+ }
369
+ else {
370
+ // Try to extract just the main error message
371
+ const errorMatch = err.message.match(/"error":"([^"]+)"/);
372
+ if (errorMatch) {
373
+ errorMsg = errorMatch[1];
374
+ }
375
+ }
376
+ }
377
+ catch {
378
+ // Keep original error message if parsing fails
379
+ }
380
+ }
381
+ return {
382
+ accessible: false,
383
+ error: errorMsg
384
+ };
385
+ }
386
+ }
387
+ async function mapsCopy(mapId, connectionName, sourceProfile, destProfile, options, token, baseUrl, jsonOutput, debug = false) {
388
+ try {
389
+ // Step 1: Create API clients for source and destination
390
+ const sourceClient = await api_1.ApiClient.create(token, baseUrl, debug, sourceProfile);
391
+ const destClient = await api_1.ApiClient.create(token, baseUrl, debug, destProfile);
392
+ if (!jsonOutput) {
393
+ console.log((0, colors_1.dim)(`Copying map ${mapId} from ${sourceProfile} to ${destProfile}...`));
394
+ }
395
+ // Step 2: Fetch source map config and datasets
396
+ if (!jsonOutput) {
397
+ console.log((0, colors_1.dim)('→ Fetching source map configuration...'));
398
+ }
399
+ const mapConfig = await sourceClient.getWorkspace(`/maps/${mapId}`);
400
+ const datasets = await sourceClient.getWorkspace(`/maps/${mapId}/datasets`);
401
+ if (!jsonOutput) {
402
+ console.log((0, colors_1.dim)(`→ Found ${datasets.length} dataset(s) in source map`));
403
+ }
404
+ // Step 3: Parse connection mapping if provided
405
+ const manualMapping = {};
406
+ if (options.connectionMapping) {
407
+ const pairs = options.connectionMapping.split(',');
408
+ for (const pair of pairs) {
409
+ const [source, dest] = pair.split('=').map((s) => s.trim());
410
+ if (!source || !dest) {
411
+ throw new Error(`Invalid connection mapping format: "${pair}". Use format: "source1=dest1,source2=dest2"`);
412
+ }
413
+ if (manualMapping[source]) {
414
+ throw new Error(`Duplicate source connection in mapping: "${source}"`);
415
+ }
416
+ manualMapping[source] = dest;
417
+ }
418
+ // Check for circular mappings
419
+ const destValues = Object.values(manualMapping);
420
+ const duplicateDests = destValues.filter((item, index) => destValues.indexOf(item) !== index);
421
+ if (duplicateDests.length > 0) {
422
+ throw new Error(`Circular or duplicate destination mappings detected: ${duplicateDests.join(', ')}`);
423
+ }
424
+ if (!jsonOutput) {
425
+ console.log((0, colors_1.dim)(`→ Manual connection mapping: ${JSON.stringify(manualMapping)}`));
426
+ }
427
+ }
428
+ // Step 4: Collect unique source connections from datasets
429
+ const sourceConnectionsMap = new Map();
430
+ datasets.forEach((ds) => {
431
+ if (ds.connectionName) {
432
+ const existing = sourceConnectionsMap.get(ds.connectionName);
433
+ if (existing) {
434
+ existing.datasetCount++;
435
+ }
436
+ else {
437
+ sourceConnectionsMap.set(ds.connectionName, {
438
+ id: ds.connectionId,
439
+ name: ds.connectionName,
440
+ datasetCount: 1
441
+ });
442
+ }
443
+ }
444
+ });
445
+ if (!jsonOutput && sourceConnectionsMap.size > 0) {
446
+ console.log((0, colors_1.dim)(`→ Source map uses ${sourceConnectionsMap.size} connection(s):`));
447
+ sourceConnectionsMap.forEach((conn) => {
448
+ console.log((0, colors_1.dim)(` • ${conn.name} (${conn.datasetCount} dataset${conn.datasetCount > 1 ? 's' : ''})`));
449
+ });
450
+ }
451
+ // Warn about mappings that reference non-existent source connections
452
+ if (Object.keys(manualMapping).length > 0) {
453
+ const sourceConnectionNames = Array.from(sourceConnectionsMap.keys());
454
+ for (const mappedSource of Object.keys(manualMapping)) {
455
+ if (!sourceConnectionNames.includes(mappedSource)) {
456
+ if (!jsonOutput) {
457
+ console.log((0, colors_1.error)(`⚠️ Warning: Mapping references "${mappedSource}" which is not used in source map`));
458
+ }
459
+ }
460
+ }
461
+ }
462
+ // Step 5: Fetch all connections from destination
463
+ if (!jsonOutput) {
464
+ console.log((0, colors_1.dim)('→ Fetching connections from destination...'));
465
+ }
466
+ const destConnections = await destClient.getWorkspace('/connections');
467
+ const destConnectionsByName = new Map();
468
+ destConnections.forEach((c) => {
469
+ destConnectionsByName.set(c.name, { id: c.id, name: c.name });
470
+ });
471
+ // Step 6: Resolve connections (manual mapping → auto-map by name → legacy single connection)
472
+ const connectionResolution = {};
473
+ const missingConnections = [];
474
+ for (const [sourceName, sourceInfo] of sourceConnectionsMap.entries()) {
475
+ let resolved = false;
476
+ // Try manual mapping first
477
+ if (manualMapping[sourceName]) {
478
+ const mappedDestName = manualMapping[sourceName];
479
+ const destConn = destConnectionsByName.get(mappedDestName);
480
+ if (destConn) {
481
+ connectionResolution[sourceName] = {
482
+ destName: mappedDestName,
483
+ destId: destConn.id,
484
+ method: 'manual'
485
+ };
486
+ resolved = true;
487
+ }
488
+ else {
489
+ // Mapped connection doesn't exist in destination
490
+ connectionResolution[sourceName] = {
491
+ destName: mappedDestName,
492
+ destId: null,
493
+ method: 'missing'
494
+ };
495
+ missingConnections.push({ sourceName, datasetCount: sourceInfo.datasetCount });
496
+ resolved = true;
497
+ }
498
+ }
499
+ // Try auto-map by name
500
+ if (!resolved) {
501
+ const destConn = destConnectionsByName.get(sourceName);
502
+ if (destConn) {
503
+ connectionResolution[sourceName] = {
504
+ destName: sourceName,
505
+ destId: destConn.id,
506
+ method: 'auto'
507
+ };
508
+ resolved = true;
509
+ }
510
+ }
511
+ // Legacy: use single --connection flag for ALL connections
512
+ if (!resolved && connectionName) {
513
+ const destConn = destConnectionsByName.get(connectionName);
514
+ if (destConn) {
515
+ connectionResolution[sourceName] = {
516
+ destName: connectionName,
517
+ destId: destConn.id,
518
+ method: 'legacy'
519
+ };
520
+ resolved = true;
521
+ }
522
+ else {
523
+ throw new Error(`Legacy connection "${connectionName}" not found in destination organization`);
524
+ }
525
+ }
526
+ // Mark as missing if still not resolved
527
+ if (!resolved) {
528
+ connectionResolution[sourceName] = {
529
+ destName: sourceName,
530
+ destId: null,
531
+ method: 'missing'
532
+ };
533
+ missingConnections.push({ sourceName, datasetCount: sourceInfo.datasetCount });
534
+ }
535
+ }
536
+ // Step 7: Display connection resolution results
537
+ if (!jsonOutput) {
538
+ console.log((0, colors_1.dim)('→ Connection resolution:'));
539
+ for (const [sourceName, resolution] of Object.entries(connectionResolution)) {
540
+ if (resolution.method === 'manual') {
541
+ console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (manual mapping)`));
542
+ }
543
+ else if (resolution.method === 'auto') {
544
+ console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (auto-mapped)`));
545
+ }
546
+ else if (resolution.method === 'legacy') {
547
+ console.log((0, colors_1.dim)(` • ${sourceName} → ${resolution.destName} (legacy --connection)`));
548
+ }
549
+ else if (resolution.method === 'missing') {
550
+ console.log((0, colors_1.error)(` ✗ ${sourceName} → NOT FOUND`));
551
+ }
552
+ }
553
+ }
554
+ // Step 8: Check for missing connections and fail
555
+ if (missingConnections.length > 0) {
556
+ const errorMsg = [
557
+ 'Missing connections in destination organization:',
558
+ ...missingConnections.map(mc => ` • "${mc.sourceName}" (used by ${mc.datasetCount} dataset${mc.datasetCount > 1 ? 's' : ''})`),
559
+ '',
560
+ 'Solutions:',
561
+ ' 1. Create missing connections in destination organization',
562
+ ' 2. Use --connection-mapping to map to different connection names'
563
+ ].join('\n');
564
+ throw new Error(errorMsg);
565
+ }
566
+ // Step 8.5: Validate source accessibility (unless --skip-source-validation)
567
+ if (!options.skipSourceValidation) {
568
+ if (!jsonOutput) {
569
+ console.log((0, colors_1.dim)(`→ Validating ${datasets.length} dataset source(s)...`));
570
+ }
571
+ const inaccessibleDatasets = [];
572
+ for (const dataset of datasets) {
573
+ const sourceConnectionName = dataset.connectionName;
574
+ const resolution = connectionResolution[sourceConnectionName];
575
+ // Use the destination connection name for the SQL query
576
+ const destConnectionName = resolution.destName;
577
+ const result = await validateDatasetSource(destClient, destConnectionName, dataset, jsonOutput);
578
+ if (!result.accessible) {
579
+ inaccessibleDatasets.push({
580
+ label: dataset.label || dataset.name || 'Unnamed',
581
+ source: dataset.source,
582
+ error: result.error
583
+ });
584
+ }
585
+ }
586
+ if (inaccessibleDatasets.length > 0) {
587
+ const errorMsg = [
588
+ 'Source validation failed - datasets cannot access their data sources:',
589
+ ...inaccessibleDatasets.map(ds => ` • "${ds.label}" → ${ds.source}\n Error: ${ds.error}`),
590
+ '',
591
+ 'Solutions:',
592
+ ' 1. Grant access to these tables/views in destination connection',
593
+ ' 2. Ensure tables/views exist in destination data warehouse',
594
+ ' 3. Use --skip-source-validation to create map anyway (datasets will not load data)'
595
+ ].join('\n');
596
+ throw new Error(errorMsg);
597
+ }
598
+ if (!jsonOutput) {
599
+ console.log((0, colors_1.dim)(`→ All source(s) validated successfully`));
600
+ }
601
+ }
602
+ // Step 9: Create new map in destination
603
+ const newTitle = options.title || mapConfig.title;
604
+ if (!jsonOutput) {
605
+ console.log((0, colors_1.dim)(`→ Creating new map "${newTitle}" in destination...`));
606
+ }
607
+ const newMap = await destClient.postWorkspace('/maps', {
608
+ title: newTitle,
609
+ });
610
+ const newMapId = newMap.id;
611
+ if (!newMapId) {
612
+ throw new Error('Failed to create map in destination');
613
+ }
614
+ // Step 10: Create datasets in destination with resolved connections
615
+ if (!jsonOutput) {
616
+ console.log((0, colors_1.dim)(`→ Creating ${datasets.length} dataset(s) in destination...`));
617
+ }
618
+ const datasetsMapping = {};
619
+ for (const dataset of datasets) {
620
+ const sourceConnectionName = dataset.connectionName;
621
+ const resolution = connectionResolution[sourceConnectionName];
622
+ const newDataset = {
623
+ ...dataset,
624
+ connectionId: resolution.destId, // All connections are validated at this point
625
+ mapId: newMapId,
626
+ };
627
+ // Remove fields that shouldn't be copied
628
+ delete newDataset.id;
629
+ delete newDataset.connectionName;
630
+ delete newDataset.providerId;
631
+ delete newDataset.sourceWorkflowId;
632
+ delete newDataset.sourceWorkflowNodeId;
633
+ const createdDataset = await destClient.postWorkspace(`/maps/${newMapId}/datasets`, newDataset);
634
+ if (!createdDataset.id) {
635
+ throw new Error(`Failed to create dataset: ${dataset.name || dataset.label || 'unnamed'}`);
636
+ }
637
+ datasetsMapping[dataset.id] = createdDataset.id;
638
+ }
639
+ // Step 11: Update map configuration with new dataset IDs
640
+ if (!jsonOutput) {
641
+ console.log((0, colors_1.dim)('→ Updating map configuration with new dataset IDs...'));
642
+ }
643
+ let updatedConfig = JSON.stringify(mapConfig.keplerMapConfig || {});
644
+ // Replace all old dataset IDs with new ones
645
+ for (const [oldId, newId] of Object.entries(datasetsMapping)) {
646
+ updatedConfig = updatedConfig.replace(new RegExp(oldId, 'g'), newId);
647
+ }
648
+ await destClient.patchWorkspace(`/maps/${newMapId}`, {
649
+ keplerMapConfig: JSON.parse(updatedConfig),
650
+ });
651
+ // Step 12: Set privacy if needed
652
+ if (options.keepPrivacy !== false && mapConfig.privacy === 'public') {
653
+ if (!jsonOutput) {
654
+ console.log((0, colors_1.dim)('→ Setting map privacy to public...'));
655
+ }
656
+ await destClient.postWorkspace(`/maps/${newMapId}/privacy`, {
657
+ privacy: 'public',
658
+ });
659
+ }
660
+ // Step 13: Copy agent configuration (if enabled and not skipped)
661
+ if (!options.skipAgent) {
662
+ // Check if carto-ai is enabled in destination
663
+ if (!jsonOutput) {
664
+ console.log((0, colors_1.dim)('→ Checking AI capabilities in destination...'));
665
+ }
666
+ const cartoAIEnabled = await checkCartoAIEnabled(destClient, jsonOutput);
667
+ if (!cartoAIEnabled) {
668
+ // CartoAI not enabled - skip gracefully with warning
669
+ if (!jsonOutput) {
670
+ console.log((0, colors_1.warning)('⚠️ CartoAI not enabled in destination - skipping agent copy'));
671
+ console.log((0, colors_1.dim)(' Enable CartoAI in destination account settings to copy AI agents'));
672
+ }
673
+ }
674
+ else {
675
+ // CartoAI is enabled, proceed with agent copy
676
+ if (!jsonOutput) {
677
+ console.log((0, colors_1.dim)('→ Copying AI agent configuration...'));
678
+ }
679
+ try {
680
+ const agentCopied = await copyMapAgent(sourceClient, destClient, mapId, newMapId, datasetsMapping, jsonOutput);
681
+ if (agentCopied) {
682
+ if (!jsonOutput) {
683
+ console.log((0, colors_1.dim)('→ Agent copied successfully'));
684
+ }
685
+ }
686
+ else {
687
+ if (!jsonOutput) {
688
+ console.log((0, colors_1.dim)('→ No agent configuration found'));
689
+ }
690
+ }
691
+ }
692
+ catch (err) {
693
+ // Log agent copy failure as warning, but don't fail the whole operation
694
+ if (!jsonOutput) {
695
+ console.log((0, colors_1.warning)(`⚠️ Could not copy agent: ${err.message}`));
696
+ }
697
+ }
698
+ }
699
+ }
700
+ else {
701
+ if (!jsonOutput) {
702
+ console.log((0, colors_1.dim)('→ Skipping agent configuration copy'));
703
+ }
704
+ }
705
+ // Get destination workspace URL for constructing map URL
706
+ const mapUrl = `${destClient.workspaceUrl}/map/${newMapId}`;
707
+ if (jsonOutput) {
708
+ console.log(JSON.stringify({
709
+ success: true,
710
+ sourceMapId: mapId,
711
+ newMapId: newMapId,
712
+ mapUrl: mapUrl,
713
+ sourceProfile: sourceProfile,
714
+ destProfile: destProfile,
715
+ datasetsCreated: datasets.length,
716
+ connectionResolution: connectionResolution,
717
+ }));
718
+ }
719
+ else {
720
+ console.log((0, colors_1.success)(`\n✓ Map copied successfully!`));
721
+ console.log((0, colors_1.bold)('New Map ID: ') + newMapId);
722
+ console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
723
+ console.log((0, colors_1.dim)(`Datasets created: ${datasets.length}`));
724
+ }
725
+ }
726
+ catch (err) {
727
+ if (jsonOutput) {
728
+ console.log(JSON.stringify({ success: false, error: err.message }));
729
+ }
730
+ else {
731
+ console.log((0, colors_1.error)('✗ Failed to copy map: ' + err.message));
732
+ }
733
+ process.exit(1);
734
+ }
735
+ }
736
+ /**
737
+ * Get JSON from various input sources (arg, file, stdin)
738
+ */
739
+ async function getJsonFromInput(jsonString, options) {
740
+ let json = jsonString;
741
+ // Priority 1: --file flag
742
+ if (options.file) {
743
+ try {
744
+ json = (0, fs_1.readFileSync)(options.file, 'utf-8').trim();
745
+ }
746
+ catch (err) {
747
+ throw new Error(`Failed to read file: ${err.message}`);
748
+ }
749
+ }
750
+ // Priority 2: stdin
751
+ if (!json && !process.stdin.isTTY) {
752
+ const chunks = [];
753
+ for await (const chunk of process.stdin) {
754
+ chunks.push(chunk);
755
+ }
756
+ json = Buffer.concat(chunks).toString('utf-8').trim();
757
+ }
758
+ // Parse JSON if we have it
759
+ if (!json) {
760
+ return undefined;
761
+ }
762
+ try {
763
+ return JSON.parse(json);
764
+ }
765
+ catch (err) {
766
+ throw new Error(`Invalid JSON: ${err.message}`);
767
+ }
768
+ }
769
+ async function mapsCreate(jsonString, options, token, baseUrl, jsonOutput, debug = false, profile) {
770
+ try {
771
+ if (!jsonOutput) {
772
+ console.error((0, colors_1.dim)('⚠️ Note: maps create is experimental and subject to change\n'));
773
+ }
774
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
775
+ // Get map config from various sources
776
+ const config = await getJsonFromInput(jsonString, options);
777
+ if (!config) {
778
+ throw new Error('No map config provided. Use: carto maps create <json> or --file <path> or pipe via stdin');
779
+ }
780
+ if (!jsonOutput) {
781
+ console.log((0, colors_1.dim)('Creating map...'));
782
+ }
783
+ // Step 1: Create the map with basic metadata
784
+ const mapPayload = {};
785
+ if (config.title)
786
+ mapPayload.title = config.title;
787
+ if (config.description)
788
+ mapPayload.description = config.description;
789
+ if (config.collaborative !== undefined)
790
+ mapPayload.collaborative = config.collaborative;
791
+ const newMap = await client.postWorkspace('/maps', mapPayload);
792
+ const newMapId = newMap.id;
793
+ if (!newMapId) {
794
+ throw new Error('Failed to create map');
795
+ }
796
+ if (!jsonOutput) {
797
+ console.log((0, colors_1.dim)(`→ Map created: ${newMapId}`));
798
+ }
799
+ // Step 2: Create datasets if provided
800
+ let datasetsCreated = 0;
801
+ if (config.datasets && Array.isArray(config.datasets)) {
802
+ if (!jsonOutput) {
803
+ console.log((0, colors_1.dim)(`→ Creating ${config.datasets.length} dataset(s)...`));
804
+ }
805
+ for (const dataset of config.datasets) {
806
+ // Get connection ID if connectionName is provided
807
+ let connectionId = dataset.connectionId;
808
+ if (!connectionId && dataset.connectionName) {
809
+ const connections = await client.getWorkspace('/connections');
810
+ const connection = connections.find((c) => c.name === dataset.connectionName);
811
+ if (!connection) {
812
+ throw new Error(`Connection "${dataset.connectionName}" not found`);
813
+ }
814
+ connectionId = connection.id;
815
+ }
816
+ const datasetPayload = {
817
+ ...dataset,
818
+ connectionId: connectionId,
819
+ mapId: newMapId,
820
+ };
821
+ // Remove fields that shouldn't be sent
822
+ delete datasetPayload.connectionName;
823
+ delete datasetPayload.id;
824
+ await client.postWorkspace(`/maps/${newMapId}/datasets`, datasetPayload);
825
+ datasetsCreated++;
826
+ }
827
+ }
828
+ // Step 3: Update map with keplerMapConfig and agent if provided
829
+ const updatePayload = {};
830
+ if (config.keplerMapConfig)
831
+ updatePayload.keplerMapConfig = config.keplerMapConfig;
832
+ if (config.agent)
833
+ updatePayload.agent = config.agent;
834
+ if (config.tags)
835
+ updatePayload.tags = config.tags;
836
+ if (Object.keys(updatePayload).length > 0) {
837
+ if (!jsonOutput) {
838
+ console.log((0, colors_1.dim)('→ Updating map configuration...'));
839
+ }
840
+ await client.patchWorkspace(`/maps/${newMapId}`, updatePayload);
841
+ }
842
+ // Step 4: Set privacy if provided
843
+ if (config.privacy) {
844
+ if (!jsonOutput) {
845
+ console.log((0, colors_1.dim)(`→ Setting privacy to ${config.privacy}...`));
846
+ }
847
+ const privacyPayload = { privacy: config.privacy };
848
+ if (config.sharingScope) {
849
+ privacyPayload.sharingScope = config.sharingScope;
850
+ }
851
+ await client.postWorkspace(`/maps/${newMapId}/privacy`, privacyPayload);
852
+ }
853
+ // Get map URL
854
+ const mapUrl = `${client.workspaceUrl}/map/${newMapId}`;
855
+ if (jsonOutput) {
856
+ console.log(JSON.stringify({
857
+ success: true,
858
+ mapId: newMapId,
859
+ mapUrl: mapUrl,
860
+ datasetsCreated: datasetsCreated,
861
+ }));
862
+ }
863
+ else {
864
+ console.log((0, colors_1.success)('\n✓ Map created successfully!'));
865
+ console.log((0, colors_1.bold)('Map ID: ') + newMapId);
866
+ console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
867
+ if (datasetsCreated > 0) {
868
+ console.log((0, colors_1.dim)(`Datasets created: ${datasetsCreated}`));
869
+ }
870
+ if (config.agent?.enabledForViewer) {
871
+ console.log((0, colors_1.dim)('Agent: enabled'));
872
+ }
873
+ }
874
+ }
875
+ catch (err) {
876
+ if (jsonOutput) {
877
+ console.log(JSON.stringify({ success: false, error: err.message }));
878
+ }
879
+ else {
880
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
881
+ console.log((0, colors_1.error)('✗ Authentication required'));
882
+ console.log('Please run: carto auth login');
883
+ }
884
+ else {
885
+ console.log((0, colors_1.error)('✗ Failed to create map: ' + err.message));
886
+ }
887
+ }
888
+ process.exit(1);
889
+ }
890
+ }
891
+ async function mapsUpdate(mapId, jsonString, options, token, baseUrl, jsonOutput, debug = false, profile) {
892
+ try {
893
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
894
+ // Get update config from various sources
895
+ const config = await getJsonFromInput(jsonString, options);
896
+ if (!config) {
897
+ throw new Error('No update config provided. Use: carto maps update <id> <json> or --file <path> or pipe via stdin');
898
+ }
899
+ if (!jsonOutput) {
900
+ console.log((0, colors_1.dim)(`Updating map ${mapId}...`));
901
+ }
902
+ // Build update payload (can be partial)
903
+ const updatePayload = {};
904
+ if (config.title !== undefined)
905
+ updatePayload.title = config.title;
906
+ if (config.description !== undefined)
907
+ updatePayload.description = config.description;
908
+ if (config.collaborative !== undefined)
909
+ updatePayload.collaborative = config.collaborative;
910
+ if (config.keplerMapConfig)
911
+ updatePayload.keplerMapConfig = config.keplerMapConfig;
912
+ if (config.agent)
913
+ updatePayload.agent = config.agent;
914
+ if (config.tags)
915
+ updatePayload.tags = config.tags;
916
+ // Update the map
917
+ if (Object.keys(updatePayload).length > 0) {
918
+ await client.patchWorkspace(`/maps/${mapId}`, updatePayload);
919
+ }
920
+ // Update privacy if provided (separate endpoint)
921
+ if (config.privacy) {
922
+ if (!jsonOutput) {
923
+ console.log((0, colors_1.dim)(`→ Updating privacy to ${config.privacy}...`));
924
+ }
925
+ const privacyPayload = { privacy: config.privacy };
926
+ if (config.sharingScope) {
927
+ privacyPayload.sharingScope = config.sharingScope;
928
+ }
929
+ await client.postWorkspace(`/maps/${mapId}/privacy`, privacyPayload);
930
+ }
931
+ // Get map URL
932
+ const mapUrl = `${client.workspaceUrl}/map/${mapId}`;
933
+ if (jsonOutput) {
934
+ console.log(JSON.stringify({
935
+ success: true,
936
+ mapId: mapId,
937
+ mapUrl: mapUrl,
938
+ updated: Object.keys(updatePayload).concat(config.privacy ? ['privacy'] : []),
939
+ }));
940
+ }
941
+ else {
942
+ console.log((0, colors_1.success)('✓ Map updated successfully!'));
943
+ console.log((0, colors_1.bold)('Map ID: ') + mapId);
944
+ console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
945
+ const updatedFields = Object.keys(updatePayload).concat(config.privacy ? ['privacy'] : []);
946
+ if (updatedFields.length > 0) {
947
+ console.log((0, colors_1.dim)(`Updated: ${updatedFields.join(', ')}`));
948
+ }
949
+ }
950
+ }
951
+ catch (err) {
952
+ if (jsonOutput) {
953
+ console.log(JSON.stringify({ success: false, error: err.message }));
954
+ }
955
+ else {
956
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
957
+ console.log((0, colors_1.error)('✗ Authentication required'));
958
+ console.log('Please run: carto auth login');
959
+ }
960
+ else if (err.message.includes('404')) {
961
+ console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
962
+ }
963
+ else {
964
+ console.log((0, colors_1.error)('✗ Failed to update map: ' + err.message));
965
+ }
966
+ }
967
+ process.exit(1);
968
+ }
969
+ }
970
+ async function mapsClone(mapId, options, token, baseUrl, jsonOutput, debug = false, profile) {
971
+ try {
972
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
973
+ if (!jsonOutput) {
974
+ console.log((0, colors_1.dim)(`Cloning map ${mapId}...`));
975
+ }
976
+ // Call the clone endpoint
977
+ const clonedMap = await client.postWorkspace(`/maps/${mapId}/clone`, {});
978
+ // Optionally rename the cloned map
979
+ if (options.title) {
980
+ if (!jsonOutput) {
981
+ console.log((0, colors_1.dim)(`→ Renaming cloned map to "${options.title}"...`));
982
+ }
983
+ await client.patchWorkspace(`/maps/${clonedMap.id}`, {
984
+ title: options.title,
985
+ });
986
+ }
987
+ // Get map URL
988
+ const mapUrl = `${client.workspaceUrl}/map/${clonedMap.id}`;
989
+ if (jsonOutput) {
990
+ console.log(JSON.stringify({
991
+ success: true,
992
+ sourceMapId: mapId,
993
+ clonedMapId: clonedMap.id,
994
+ mapUrl: mapUrl,
995
+ }));
996
+ }
997
+ else {
998
+ console.log((0, colors_1.success)('\n✓ Map cloned successfully!'));
999
+ console.log((0, colors_1.bold)('Source Map ID: ') + mapId);
1000
+ console.log((0, colors_1.bold)('Cloned Map ID: ') + clonedMap.id);
1001
+ console.log((0, colors_1.bold)('Map URL: ') + mapUrl);
1002
+ }
1003
+ }
1004
+ catch (err) {
1005
+ if (jsonOutput) {
1006
+ console.log(JSON.stringify({ success: false, error: err.message }));
1007
+ }
1008
+ else {
1009
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
1010
+ console.log((0, colors_1.error)('✗ Authentication required'));
1011
+ console.log('Please run: carto auth login');
1012
+ }
1013
+ else if (err.message.includes('404')) {
1014
+ console.log((0, colors_1.error)('✗ Map not found: ' + mapId));
1015
+ }
1016
+ else {
1017
+ console.log((0, colors_1.error)('✗ Failed to clone map: ' + err.message));
1018
+ }
1019
+ }
1020
+ process.exit(1);
1021
+ }
1022
+ }