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,1025 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workflowsList = workflowsList;
4
+ exports.workflowsDelete = workflowsDelete;
5
+ exports.workflowsGet = workflowsGet;
6
+ exports.workflowsCreate = workflowsCreate;
7
+ exports.workflowsUpdate = workflowsUpdate;
8
+ exports.workflowsExtensionsInstall = workflowsExtensionsInstall;
9
+ exports.workflowScheduleAdd = workflowScheduleAdd;
10
+ exports.workflowScheduleUpdate = workflowScheduleUpdate;
11
+ exports.workflowsCopy = workflowsCopy;
12
+ exports.workflowScheduleRemove = workflowScheduleRemove;
13
+ const api_1 = require("../api");
14
+ const colors_1 = require("../colors");
15
+ const fs_1 = require("fs");
16
+ const os_1 = require("os");
17
+ const path_1 = require("path");
18
+ const child_process_1 = require("child_process");
19
+ const schedule_parser_1 = require("../schedule-parser");
20
+ const prompt_1 = require("../prompt");
21
+ async function workflowsList(options, token, baseUrl, jsonOutput, debug = false, profile) {
22
+ try {
23
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
24
+ let result;
25
+ if (options.all) {
26
+ // Fetch all pages automatically
27
+ const baseParams = {};
28
+ if (options.orderBy)
29
+ baseParams['order_by'] = options.orderBy;
30
+ if (options.orderDirection)
31
+ baseParams['order_direction'] = options.orderDirection;
32
+ if (options.search)
33
+ baseParams['search'] = options.search;
34
+ if (options.privacy)
35
+ baseParams['privacy'] = options.privacy;
36
+ if (options.tags)
37
+ baseParams['tags'] = JSON.stringify(options.tags);
38
+ const paginatedResult = await client.getAllPaginated('/v3/organization-workflows', baseParams);
39
+ result = {
40
+ data: paginatedResult.data,
41
+ meta: {
42
+ page: {
43
+ total: paginatedResult.total,
44
+ current: 1,
45
+ pageSize: paginatedResult.total,
46
+ totalPages: 1
47
+ }
48
+ }
49
+ };
50
+ }
51
+ else {
52
+ // Fetch single page
53
+ const params = new URLSearchParams();
54
+ if (options.orderBy)
55
+ params.append('order_by', options.orderBy);
56
+ if (options.orderDirection)
57
+ params.append('order_direction', options.orderDirection);
58
+ if (options.pageSize)
59
+ params.append('page_size', options.pageSize);
60
+ if (options.page)
61
+ params.append('page', options.page);
62
+ if (options.search)
63
+ params.append('search', options.search);
64
+ if (options.privacy)
65
+ params.append('privacy', options.privacy);
66
+ if (options.tags)
67
+ params.append('tags', JSON.stringify(options.tags));
68
+ const queryString = params.toString();
69
+ const path = `/v3/organization-workflows${queryString ? '?' + queryString : ''}`;
70
+ result = await client.get(path);
71
+ }
72
+ if (jsonOutput) {
73
+ console.log(JSON.stringify(result));
74
+ }
75
+ else {
76
+ const total = result.meta?.page?.total || result.data?.length || 0;
77
+ // Display header with search query if present
78
+ const header = options.search
79
+ ? `\n${total} workflows found for '${options.search}':\n`
80
+ : `\nWorkflows (${total}):\n`;
81
+ console.log((0, colors_1.bold)(header));
82
+ const workflows = result.data || [];
83
+ workflows.forEach((workflow) => {
84
+ console.log((0, colors_1.bold)('Workflow ID: ') + workflow.id);
85
+ console.log(' Name: ' + (workflow.title || workflow.name));
86
+ console.log(' Privacy: ' + (workflow.privacy || 'N/A'));
87
+ if (workflow.tags && workflow.tags.length > 0) {
88
+ console.log(' Tags: ' + workflow.tags.join(', '));
89
+ }
90
+ console.log(' Created: ' + new Date(workflow.createdAt || workflow.created_at).toLocaleString());
91
+ console.log(' Updated: ' + new Date(workflow.updatedAt || workflow.updated_at).toLocaleString());
92
+ console.log('');
93
+ });
94
+ if (options.all) {
95
+ console.log((0, colors_1.dim)(`Fetched all ${total} workflows`));
96
+ }
97
+ else if (result.meta?.page) {
98
+ const page = result.meta.page;
99
+ console.log((0, colors_1.dim)(`Page ${page.current || 1} of ${page.totalPages || 1} (use --all to fetch all pages)`));
100
+ }
101
+ }
102
+ }
103
+ catch (err) {
104
+ if (jsonOutput) {
105
+ console.log(JSON.stringify({ success: false, error: err.message }));
106
+ }
107
+ else {
108
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
109
+ console.log((0, colors_1.error)('✗ Authentication required'));
110
+ console.log('Please run: carto auth login');
111
+ }
112
+ else {
113
+ console.log((0, colors_1.error)('✗ Failed to list workflows: ' + err.message));
114
+ }
115
+ }
116
+ process.exit(1);
117
+ }
118
+ }
119
+ async function workflowsDelete(workflowId, token, baseUrl, jsonOutput, debug = false, profile, yes) {
120
+ try {
121
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
122
+ // Skip confirmation if --yes or --json flag is set
123
+ const skipConfirmation = yes || jsonOutput;
124
+ if (!skipConfirmation) {
125
+ // Fetch workflow details to show in confirmation prompt
126
+ let workflowDetails;
127
+ try {
128
+ workflowDetails = await client.get(`/v3/workflows/${workflowId}`);
129
+ }
130
+ catch (err) {
131
+ // If we can't fetch details, proceed with generic message
132
+ console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete this workflow.'));
133
+ console.log((0, colors_1.warning)(` Workflow ID: ${workflowId}`));
134
+ }
135
+ if (workflowDetails) {
136
+ const title = workflowDetails.title || workflowDetails.config?.title || '(Untitled)';
137
+ console.log((0, colors_1.warning)('\n⚠️ Warning: This will permanently delete:'));
138
+ console.log(` Workflow: "${title}"`);
139
+ console.log(` ID: ${workflowId}`);
140
+ if (workflowDetails.privacy) {
141
+ console.log(` Privacy: ${workflowDetails.privacy}`);
142
+ }
143
+ }
144
+ console.log('');
145
+ const confirmed = await (0, prompt_1.promptForConfirmation)("Type 'delete' to confirm: ", 'delete');
146
+ if (!confirmed) {
147
+ console.log('\nDeletion cancelled');
148
+ process.exit(0);
149
+ }
150
+ console.log('');
151
+ }
152
+ await client.delete(`/v3/organization-workflows/${workflowId}`);
153
+ if (jsonOutput) {
154
+ console.log(JSON.stringify({ success: true, message: 'Workflow deleted successfully', id: workflowId }));
155
+ }
156
+ else {
157
+ console.log((0, colors_1.success)(`✓ Workflow ${workflowId} deleted successfully`));
158
+ }
159
+ }
160
+ catch (err) {
161
+ if (jsonOutput) {
162
+ console.log(JSON.stringify({ success: false, error: err.message }));
163
+ }
164
+ else {
165
+ console.log((0, colors_1.error)('✗ Failed to delete workflow: ' + err.message));
166
+ }
167
+ process.exit(1);
168
+ }
169
+ }
170
+ async function workflowsGet(workflowId, options, token, baseUrl, jsonOutput, debug = false) {
171
+ try {
172
+ const client = await api_1.ApiClient.create(token, baseUrl, debug);
173
+ // Build query parameters
174
+ const params = new URLSearchParams();
175
+ if (options.client)
176
+ params.append('client', options.client);
177
+ const queryString = params.toString();
178
+ const path = `/v3/workflows/${workflowId}${queryString ? '?' + queryString : ''}`;
179
+ const result = await client.get(path);
180
+ if (jsonOutput) {
181
+ console.log(JSON.stringify(result));
182
+ }
183
+ else {
184
+ console.log((0, colors_1.bold)('\nWorkflow Details:\n'));
185
+ console.log((0, colors_1.bold)('ID: ') + result.id);
186
+ const title = result.title || result.config?.title || result.name;
187
+ console.log(' Title: ' + title);
188
+ const description = result.description || result.config?.description;
189
+ if (description)
190
+ console.log(' Description: ' + description);
191
+ console.log(' Privacy: ' + (result.privacy || 'N/A'));
192
+ if (result.tags && result.tags.length > 0) {
193
+ console.log(' Tags: ' + result.tags.join(', '));
194
+ }
195
+ if (result.connectionProvider || result.providerId) {
196
+ console.log(' Connection Provider: ' + (result.connectionProvider || result.providerId));
197
+ }
198
+ if (result.schemaVersion)
199
+ console.log(' Schema Version: ' + result.schemaVersion);
200
+ console.log(' Created: ' + new Date(result.createdAt || result.created_at).toLocaleString());
201
+ console.log(' Updated: ' + new Date(result.updatedAt || result.updated_at).toLocaleString());
202
+ if (result.config) {
203
+ console.log('\n' + (0, colors_1.bold)('Configuration:'));
204
+ console.log(' Nodes: ' + (result.config.nodes?.length || 0));
205
+ console.log(' Edges: ' + (result.config.edges?.length || 0));
206
+ if (result.config.procedure !== undefined)
207
+ console.log(' Procedure: ' + result.config.procedure);
208
+ if (result.config.useCache !== undefined)
209
+ console.log(' Use Cache: ' + result.config.useCache);
210
+ }
211
+ if (result.executionInfo) {
212
+ console.log('\n' + (0, colors_1.bold)('Execution Info:'));
213
+ if (result.executionInfo.status)
214
+ console.log(' Status: ' + result.executionInfo.status);
215
+ if (result.executionInfo.startedAt)
216
+ console.log(' Started: ' + new Date(result.executionInfo.startedAt).toLocaleString());
217
+ if (result.executionInfo.finishedAt)
218
+ console.log(' Finished: ' + new Date(result.executionInfo.finishedAt).toLocaleString());
219
+ }
220
+ }
221
+ }
222
+ catch (err) {
223
+ if (jsonOutput) {
224
+ console.log(JSON.stringify({ success: false, error: err.message }));
225
+ }
226
+ else {
227
+ console.log((0, colors_1.error)('✗ Failed to get workflow: ' + err.message));
228
+ }
229
+ process.exit(1);
230
+ }
231
+ }
232
+ /**
233
+ * Get JSON from various input sources (arg, file, stdin)
234
+ */
235
+ async function getJsonFromInput(jsonString, options) {
236
+ let json = jsonString;
237
+ // Priority 1: --file flag
238
+ if (options.file) {
239
+ try {
240
+ json = (0, fs_1.readFileSync)(options.file, 'utf-8').trim();
241
+ }
242
+ catch (err) {
243
+ throw new Error(`Failed to read file: ${err.message}`);
244
+ }
245
+ }
246
+ // Priority 2: stdin
247
+ if (!json && !process.stdin.isTTY) {
248
+ const chunks = [];
249
+ for await (const chunk of process.stdin) {
250
+ chunks.push(chunk);
251
+ }
252
+ json = Buffer.concat(chunks).toString('utf-8').trim();
253
+ }
254
+ // Parse JSON if we have it
255
+ if (!json) {
256
+ return undefined;
257
+ }
258
+ try {
259
+ return JSON.parse(json);
260
+ }
261
+ catch (err) {
262
+ throw new Error(`Invalid JSON: ${err.message}`);
263
+ }
264
+ }
265
+ async function workflowsCreate(jsonString, options, token, baseUrl, jsonOutput, debug = false) {
266
+ try {
267
+ if (!jsonOutput) {
268
+ console.error((0, colors_1.dim)('⚠️ Note: workflows create is experimental and subject to change\n'));
269
+ }
270
+ const client = await api_1.ApiClient.create(token, baseUrl, debug);
271
+ // Get workflow config from various sources
272
+ const body = await getJsonFromInput(jsonString, options);
273
+ if (!body) {
274
+ throw new Error('No workflow config provided. Use: carto workflows create <json> or --file <path> or pipe via stdin');
275
+ }
276
+ // Ensure required fields
277
+ if (!body.connectionId) {
278
+ throw new Error('connectionId is required');
279
+ }
280
+ // Set default client if not provided
281
+ if (!body.client) {
282
+ body.client = 'carto-cli';
283
+ }
284
+ const result = await client.post('/v3/workflows', body);
285
+ if (jsonOutput) {
286
+ console.log(JSON.stringify(result));
287
+ }
288
+ else {
289
+ console.log((0, colors_1.success)(`✓ Workflow created successfully`));
290
+ console.log((0, colors_1.bold)('\nWorkflow ID: ') + result.id);
291
+ if (result.title)
292
+ console.log('Title: ' + result.title);
293
+ if (result.connectionProvider)
294
+ console.log('Connection Provider: ' + result.connectionProvider);
295
+ }
296
+ }
297
+ catch (err) {
298
+ if (jsonOutput) {
299
+ console.log(JSON.stringify({ success: false, error: err.message }));
300
+ }
301
+ else {
302
+ console.log((0, colors_1.error)('✗ Failed to create workflow: ' + err.message));
303
+ }
304
+ process.exit(1);
305
+ }
306
+ }
307
+ async function workflowsUpdate(workflowId, jsonString, options, token, baseUrl, jsonOutput, debug = false) {
308
+ try {
309
+ const client = await api_1.ApiClient.create(token, baseUrl, debug);
310
+ // Get update config from various sources
311
+ const body = await getJsonFromInput(jsonString, options);
312
+ if (!body) {
313
+ throw new Error('No update config provided. Use: carto workflows update <id> <json> or --file <path> or pipe via stdin');
314
+ }
315
+ // Ensure required fields
316
+ if (!body.config) {
317
+ throw new Error('config object is required');
318
+ }
319
+ // Set default client if not provided
320
+ if (!body.client) {
321
+ body.client = 'carto-cli';
322
+ }
323
+ const result = await client.patch(`/v3/workflows/${workflowId}`, body);
324
+ if (jsonOutput) {
325
+ console.log(JSON.stringify(result));
326
+ }
327
+ else {
328
+ console.log((0, colors_1.success)(`✓ Workflow ${workflowId} updated successfully`));
329
+ if (result.title)
330
+ console.log('Title: ' + result.title);
331
+ if (result.updated_at)
332
+ console.log('Updated: ' + new Date(result.updated_at).toLocaleString());
333
+ }
334
+ }
335
+ catch (err) {
336
+ if (jsonOutput) {
337
+ console.log(JSON.stringify({ success: false, error: err.message }));
338
+ }
339
+ else {
340
+ console.log((0, colors_1.error)('✗ Failed to update workflow: ' + err.message));
341
+ }
342
+ process.exit(1);
343
+ }
344
+ }
345
+ /**
346
+ * Install a workflow extension from a local zip file
347
+ */
348
+ async function workflowsExtensionsInstall(extensionFile, connectionName, token, baseUrl, jsonOutput, debug = false, profile) {
349
+ let tempDir;
350
+ try {
351
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
352
+ if (!jsonOutput) {
353
+ console.log((0, colors_1.dim)('Installing extension...'));
354
+ }
355
+ // Create temp directory
356
+ tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-extension-'));
357
+ // Extract zip file
358
+ try {
359
+ (0, child_process_1.execSync)(`unzip -q -o "${extensionFile}" -d "${tempDir}"`, { stdio: 'pipe' });
360
+ }
361
+ catch (err) {
362
+ throw new Error(`Failed to extract extension file: ${err.message}`);
363
+ }
364
+ // Read metadata.json
365
+ const metadataPath = (0, path_1.join)(tempDir, 'metadata.json');
366
+ let metadata;
367
+ try {
368
+ const metadataContent = (0, fs_1.readFileSync)(metadataPath, 'utf-8');
369
+ metadata = JSON.parse(metadataContent);
370
+ }
371
+ catch (err) {
372
+ throw new Error(`Failed to read metadata.json: ${err.message}`);
373
+ }
374
+ if (!metadata.provider) {
375
+ throw new Error('Extension metadata missing "provider" field');
376
+ }
377
+ if (!jsonOutput) {
378
+ console.log((0, colors_1.dim)(`Extension: ${metadata.name || 'unknown'} (provider: ${metadata.provider})`));
379
+ console.log((0, colors_1.dim)(`Finding connection: ${connectionName}...`));
380
+ }
381
+ // Find connection by name
382
+ const connectionsResult = await client.getAllPaginated('/v3/connections', {});
383
+ const connectionSummary = connectionsResult.data.find((conn) => conn.name === connectionName);
384
+ if (!connectionSummary) {
385
+ throw new Error(`Connection '${connectionName}' not found`);
386
+ }
387
+ // Get full connection details (list endpoint doesn't include extensionsLocations)
388
+ const connection = await client.get(`/v3/connections/${connectionSummary.id}`);
389
+ // Validate provider compatibility
390
+ if (metadata.provider !== connection.provider_id) {
391
+ throw new Error(`Extension provider '${metadata.provider}' does not match connection provider '${connection.provider_id}'`);
392
+ }
393
+ // Get extensionsLocations FQN
394
+ if (!connection.extensionsLocations || connection.extensionsLocations.length === 0) {
395
+ throw new Error(`Connection '${connectionName}' has no extensionsLocations configured`);
396
+ }
397
+ const fqn = connection.extensionsLocations[0].fqn;
398
+ if (!jsonOutput) {
399
+ console.log((0, colors_1.dim)(`Connection found: ${connection.id} (${connection.provider_id})`));
400
+ console.log((0, colors_1.dim)(`Extensions location: ${fqn}`));
401
+ console.log((0, colors_1.dim)('Processing SQL...'));
402
+ }
403
+ // Read and process extension.sql
404
+ const sqlPath = (0, path_1.join)(tempDir, 'extension.sql');
405
+ let sql;
406
+ try {
407
+ sql = (0, fs_1.readFileSync)(sqlPath, 'utf-8');
408
+ }
409
+ catch (err) {
410
+ throw new Error(`Failed to read extension.sql: ${err.message}`);
411
+ }
412
+ // Replace placeholder
413
+ sql = sql.replace(/@@workflows_temp@@/g, fqn);
414
+ if (!jsonOutput) {
415
+ console.log((0, colors_1.dim)('Executing extension SQL...'));
416
+ }
417
+ // Execute SQL job
418
+ const jobBody = {
419
+ query: sql,
420
+ queryParameters: {}
421
+ };
422
+ const jobResult = await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/job`, jobBody);
423
+ const jobId = jobResult.externalId || jobResult.jobId;
424
+ if (!jsonOutput) {
425
+ console.log((0, colors_1.dim)(`Job created: ${jobId}`));
426
+ console.log((0, colors_1.dim)('Waiting for completion...'));
427
+ }
428
+ // Poll for job completion
429
+ const maxRetries = 600; // 10 minutes
430
+ let retries = 0;
431
+ let jobStatus;
432
+ while (retries < maxRetries) {
433
+ jobStatus = await client.get(`/v3/sql/${encodeURIComponent(connectionName)}/job/${encodeURIComponent(jobId)}`);
434
+ if (!jsonOutput && retries > 0 && retries % 5 === 0) {
435
+ console.log((0, colors_1.dim)(`Status: ${jobStatus.status}...`));
436
+ }
437
+ if (jobStatus.status === 'success' || jobStatus.status === 'failed' || jobStatus.status === 'failure') {
438
+ break;
439
+ }
440
+ await new Promise(resolve => setTimeout(resolve, 1000));
441
+ retries++;
442
+ }
443
+ if (retries >= maxRetries) {
444
+ throw new Error('Job timeout: Installation took longer than 10 minutes');
445
+ }
446
+ // Check final status
447
+ if (jobStatus.status === 'failed' || jobStatus.status === 'failure') {
448
+ const errorMsg = jobStatus.error?.msg || jobStatus.error?.message || jobStatus.error || 'Unknown error';
449
+ throw new Error(`Installation failed: ${errorMsg}`);
450
+ }
451
+ if (jsonOutput) {
452
+ console.log(JSON.stringify({
453
+ success: true,
454
+ extensionName: metadata.name,
455
+ provider: metadata.provider,
456
+ connection: connectionName,
457
+ jobId: jobId
458
+ }));
459
+ }
460
+ else {
461
+ console.log((0, colors_1.success)(`\n✓ Extension '${metadata.name}' installed successfully`));
462
+ console.log((0, colors_1.dim)(`Job ID: ${jobId}`));
463
+ }
464
+ }
465
+ catch (err) {
466
+ if (jsonOutput) {
467
+ console.log(JSON.stringify({ success: false, error: err.message }));
468
+ }
469
+ else {
470
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
471
+ console.log((0, colors_1.error)('✗ Authentication required'));
472
+ console.log('Please run: carto auth login');
473
+ }
474
+ else {
475
+ console.log((0, colors_1.error)('✗ ' + err.message));
476
+ }
477
+ }
478
+ process.exit(1);
479
+ }
480
+ finally {
481
+ // Clean up temp directory
482
+ if (tempDir) {
483
+ try {
484
+ (0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
485
+ }
486
+ catch (err) {
487
+ // Ignore cleanup errors
488
+ }
489
+ }
490
+ }
491
+ }
492
+ /**
493
+ * Add a schedule to a workflow
494
+ */
495
+ async function workflowScheduleAdd(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
496
+ let tempDir;
497
+ try {
498
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
499
+ if (!options.expression) {
500
+ throw new Error('Schedule expression is required (use --expression)');
501
+ }
502
+ if (!jsonOutput) {
503
+ console.log((0, colors_1.dim)('Fetching workflow details...'));
504
+ }
505
+ // 1. Get workflow details
506
+ const workflow = await client.get(`/v3/workflows/${workflowId}`);
507
+ if (!workflow.connectionId) {
508
+ throw new Error('Workflow does not have a connectionId');
509
+ }
510
+ if (!jsonOutput) {
511
+ console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
512
+ console.log((0, colors_1.dim)('Fetching connection details...'));
513
+ }
514
+ // 2. Get connection details
515
+ const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
516
+ const connectionName = connection.name;
517
+ const region = connection.region || 'US';
518
+ const provider = connection.provider_id || connection.providerId;
519
+ if (!jsonOutput) {
520
+ console.log((0, colors_1.dim)(`Connection: ${connectionName} (${provider}, region: ${region})`));
521
+ console.log((0, colors_1.dim)('Generating SQL from workflow...'));
522
+ }
523
+ // 3. Generate SQL from workflow config
524
+ tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-workflow-'));
525
+ const diagramPath = (0, path_1.join)(tempDir, 'workflow.json');
526
+ (0, fs_1.writeFileSync)(diagramPath, JSON.stringify(workflow.config, null, 2), 'utf-8');
527
+ let sql;
528
+ try {
529
+ sql = (0, child_process_1.execSync)(`workflows-engine diagram to-sql "${diagramPath}"`, {
530
+ encoding: 'utf-8',
531
+ stdio: ['pipe', 'pipe', 'pipe']
532
+ });
533
+ }
534
+ catch (err) {
535
+ throw new Error(`Failed to generate SQL: ${err.message}`);
536
+ }
537
+ if (!jsonOutput) {
538
+ console.log((0, colors_1.dim)('Creating schedule in data warehouse...'));
539
+ }
540
+ // 4. Create schedule via SQL API
541
+ const scheduleBody = {
542
+ schedule: options.expression,
543
+ query: sql,
544
+ region: region,
545
+ name: `workflows.${workflowId}`,
546
+ client: 'workflows-schedule'
547
+ };
548
+ await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/schedule`, scheduleBody);
549
+ if (!jsonOutput) {
550
+ console.log((0, colors_1.dim)('Parsing schedule expression...'));
551
+ }
552
+ // 5. Parse schedule expression to metadata
553
+ const scheduleMetadata = (0, schedule_parser_1.parseScheduleExpression)(options.expression, provider);
554
+ if (!jsonOutput) {
555
+ console.log((0, colors_1.dim)('Updating workflow metadata...'));
556
+ }
557
+ // 6. Update workflow with schedule metadata in config
558
+ const updatedConfig = {
559
+ ...workflow.config,
560
+ schedule: {
561
+ time: scheduleMetadata.time,
562
+ frequency: scheduleMetadata.frequency,
563
+ daysOfWeek: scheduleMetadata.daysOfWeek,
564
+ expression: options.expression,
565
+ daysOfMonth: scheduleMetadata.daysOfMonth,
566
+ repeatInterval: scheduleMetadata.repeatInterval
567
+ }
568
+ };
569
+ const updateBody = {
570
+ config: updatedConfig,
571
+ client: 'carto-cli'
572
+ };
573
+ await client.patch(`/v3/workflows/${workflowId}`, updateBody);
574
+ if (jsonOutput) {
575
+ console.log(JSON.stringify({
576
+ success: true,
577
+ workflowId,
578
+ schedule: options.expression,
579
+ metadata: scheduleMetadata
580
+ }));
581
+ }
582
+ else {
583
+ console.log((0, colors_1.success)(`\n✓ Schedule added successfully`));
584
+ console.log((0, colors_1.bold)('Workflow: ') + workflowId);
585
+ console.log((0, colors_1.bold)('Schedule: ') + options.expression);
586
+ console.log((0, colors_1.bold)('Time: ') + scheduleMetadata.time);
587
+ if (scheduleMetadata.daysOfWeek.length < 7) {
588
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
589
+ const days = scheduleMetadata.daysOfWeek.map(d => dayNames[d]).join(', ');
590
+ console.log((0, colors_1.bold)('Days: ') + days);
591
+ }
592
+ }
593
+ }
594
+ catch (err) {
595
+ if (jsonOutput) {
596
+ console.log(JSON.stringify({ success: false, error: err.message }));
597
+ }
598
+ else {
599
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
600
+ console.log((0, colors_1.error)('✗ Authentication required'));
601
+ console.log('Please run: carto auth login');
602
+ }
603
+ else {
604
+ console.log((0, colors_1.error)('✗ Failed to add schedule: ' + err.message));
605
+ }
606
+ }
607
+ process.exit(1);
608
+ }
609
+ finally {
610
+ if (tempDir) {
611
+ try {
612
+ (0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
613
+ }
614
+ catch (err) {
615
+ // Ignore cleanup errors
616
+ }
617
+ }
618
+ }
619
+ }
620
+ /**
621
+ * Update a workflow schedule
622
+ */
623
+ async function workflowScheduleUpdate(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
624
+ let tempDir;
625
+ try {
626
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
627
+ if (!options.expression) {
628
+ throw new Error('Schedule expression is required (use --expression)');
629
+ }
630
+ if (!jsonOutput) {
631
+ console.log((0, colors_1.dim)('Fetching workflow details...'));
632
+ }
633
+ // 1. Get workflow details
634
+ const workflow = await client.get(`/v3/workflows/${workflowId}`);
635
+ if (!workflow.connectionId) {
636
+ throw new Error('Workflow does not have a connectionId');
637
+ }
638
+ if (!jsonOutput) {
639
+ console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
640
+ console.log((0, colors_1.dim)('Fetching connection details...'));
641
+ }
642
+ // 2. Get connection details
643
+ const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
644
+ const connectionName = connection.name;
645
+ const region = connection.region || 'US';
646
+ const provider = connection.provider_id || connection.providerId;
647
+ if (!jsonOutput) {
648
+ console.log((0, colors_1.dim)(`Connection: ${connectionName} (${provider}, region: ${region})`));
649
+ console.log((0, colors_1.dim)('Generating SQL from workflow...'));
650
+ }
651
+ // 3. Generate SQL from workflow config
652
+ tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'carto-workflow-'));
653
+ const diagramPath = (0, path_1.join)(tempDir, 'workflow.json');
654
+ (0, fs_1.writeFileSync)(diagramPath, JSON.stringify(workflow.config, null, 2), 'utf-8');
655
+ let sql;
656
+ try {
657
+ sql = (0, child_process_1.execSync)(`workflows-engine diagram to-sql "${diagramPath}"`, {
658
+ encoding: 'utf-8',
659
+ stdio: ['pipe', 'pipe', 'pipe']
660
+ });
661
+ }
662
+ catch (err) {
663
+ throw new Error(`Failed to generate SQL: ${err.message}`);
664
+ }
665
+ if (!jsonOutput) {
666
+ console.log((0, colors_1.dim)('Updating schedule in data warehouse...'));
667
+ }
668
+ // 4. Update schedule via SQL API
669
+ const schedulePath = `/v3/sql/${encodeURIComponent(connectionName)}/schedule/workflows.${workflowId}?region=${region}`;
670
+ const scheduleBody = {
671
+ schedule: options.expression,
672
+ query: sql,
673
+ name: `workflows.${workflowId}`,
674
+ region: region,
675
+ client: 'workflows-schedule'
676
+ };
677
+ await client.patch(schedulePath, scheduleBody);
678
+ if (!jsonOutput) {
679
+ console.log((0, colors_1.dim)('Parsing schedule expression...'));
680
+ }
681
+ // 5. Parse schedule expression to metadata
682
+ const scheduleMetadata = (0, schedule_parser_1.parseScheduleExpression)(options.expression, provider);
683
+ if (!jsonOutput) {
684
+ console.log((0, colors_1.dim)('Updating workflow metadata...'));
685
+ }
686
+ // 6. Update workflow with new schedule metadata in config
687
+ const updatedConfig = {
688
+ ...workflow.config,
689
+ schedule: {
690
+ time: scheduleMetadata.time,
691
+ frequency: scheduleMetadata.frequency,
692
+ daysOfWeek: scheduleMetadata.daysOfWeek,
693
+ expression: options.expression,
694
+ daysOfMonth: scheduleMetadata.daysOfMonth,
695
+ repeatInterval: scheduleMetadata.repeatInterval
696
+ }
697
+ };
698
+ const updateBody = {
699
+ config: updatedConfig,
700
+ client: 'carto-cli'
701
+ };
702
+ await client.patch(`/v3/workflows/${workflowId}`, updateBody);
703
+ if (jsonOutput) {
704
+ console.log(JSON.stringify({
705
+ success: true,
706
+ workflowId,
707
+ schedule: options.expression,
708
+ metadata: scheduleMetadata
709
+ }));
710
+ }
711
+ else {
712
+ console.log((0, colors_1.success)(`\n✓ Schedule updated successfully`));
713
+ console.log((0, colors_1.bold)('Workflow: ') + workflowId);
714
+ console.log((0, colors_1.bold)('New schedule: ') + options.expression);
715
+ console.log((0, colors_1.bold)('Time: ') + scheduleMetadata.time);
716
+ if (scheduleMetadata.daysOfWeek.length < 7) {
717
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
718
+ const days = scheduleMetadata.daysOfWeek.map(d => dayNames[d]).join(', ');
719
+ console.log((0, colors_1.bold)('Days: ') + days);
720
+ }
721
+ }
722
+ }
723
+ catch (err) {
724
+ if (jsonOutput) {
725
+ console.log(JSON.stringify({ success: false, error: err.message }));
726
+ }
727
+ else {
728
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
729
+ console.log((0, colors_1.error)('✗ Authentication required'));
730
+ console.log('Please run: carto auth login');
731
+ }
732
+ else {
733
+ console.log((0, colors_1.error)('✗ Failed to update schedule: ' + err.message));
734
+ }
735
+ }
736
+ process.exit(1);
737
+ }
738
+ finally {
739
+ if (tempDir) {
740
+ try {
741
+ (0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
742
+ }
743
+ catch (err) {
744
+ // Ignore cleanup errors
745
+ }
746
+ }
747
+ }
748
+ }
749
+ /**
750
+ * Validate that a table source is accessible in the destination connection
751
+ * Uses dry-run SQL queries with WHERE 1=0 to test without transferring data
752
+ */
753
+ async function validateTableSource(client, connectionName, tableFqn, jsonOutput) {
754
+ try {
755
+ // Build dry-run query
756
+ const validationSQL = `SELECT * FROM ${tableFqn} WHERE 1=0 LIMIT 1`;
757
+ if (!jsonOutput) {
758
+ const displayTable = tableFqn.length > 60
759
+ ? tableFqn.substring(0, 57) + '...'
760
+ : tableFqn;
761
+ console.log((0, colors_1.dim)(` ✓ ${displayTable}`));
762
+ }
763
+ // Execute validation query via SQL API
764
+ await client.post(`/v3/sql/${encodeURIComponent(connectionName)}/query`, {
765
+ q: validationSQL,
766
+ queryParameters: {}
767
+ });
768
+ // Success - table is accessible
769
+ return { accessible: true };
770
+ }
771
+ catch (err) {
772
+ // Extract meaningful error message from API response
773
+ let errorMsg = 'Unknown error';
774
+ if (err.message) {
775
+ errorMsg = err.message;
776
+ // Try to extract detail from API error JSON
777
+ try {
778
+ const match = err.message.match(/"detail":"([^"]+)"/);
779
+ if (match) {
780
+ errorMsg = match[1];
781
+ }
782
+ else {
783
+ // Try to extract just the main error message
784
+ const errorMatch = err.message.match(/"error":"([^"]+)"/);
785
+ if (errorMatch) {
786
+ errorMsg = errorMatch[1];
787
+ }
788
+ }
789
+ }
790
+ catch {
791
+ // Keep original error message if parsing fails
792
+ }
793
+ }
794
+ return {
795
+ accessible: false,
796
+ error: errorMsg
797
+ };
798
+ }
799
+ }
800
+ /**
801
+ * Copy a workflow from one profile to another
802
+ */
803
+ async function workflowsCopy(workflowId, connectionName, sourceProfile, destProfile, options, token, baseUrl, jsonOutput, debug = false) {
804
+ try {
805
+ // Step 1: Create API clients for source and destination
806
+ const sourceClient = await api_1.ApiClient.create(token, baseUrl, debug, sourceProfile);
807
+ const destClient = await api_1.ApiClient.create(token, baseUrl, debug, destProfile);
808
+ if (!jsonOutput) {
809
+ console.log((0, colors_1.dim)(`Copying workflow ${workflowId} from ${sourceProfile} to ${destProfile}...`));
810
+ }
811
+ // Step 2: Fetch source workflow
812
+ if (!jsonOutput) {
813
+ console.log((0, colors_1.dim)('→ Fetching source workflow...'));
814
+ }
815
+ const workflow = await sourceClient.get(`/v3/workflows/${workflowId}`);
816
+ if (!workflow.connectionId) {
817
+ throw new Error('Source workflow does not have a connectionId');
818
+ }
819
+ // Step 3: Get source connection details
820
+ if (!jsonOutput) {
821
+ console.log((0, colors_1.dim)('→ Fetching source connection details...'));
822
+ }
823
+ const sourceConnection = await sourceClient.get(`/v3/connections/${workflow.connectionId}`);
824
+ const sourceConnectionName = sourceConnection.name;
825
+ if (!jsonOutput) {
826
+ console.log((0, colors_1.dim)(`→ Source connection: ${sourceConnectionName}`));
827
+ }
828
+ // Step 4: Resolve destination connection
829
+ const destConnectionName = connectionName || sourceConnectionName;
830
+ if (!jsonOutput) {
831
+ if (connectionName) {
832
+ console.log((0, colors_1.dim)(`→ Using destination connection: ${destConnectionName} (manual)`));
833
+ }
834
+ else {
835
+ console.log((0, colors_1.dim)(`→ Using destination connection: ${destConnectionName} (auto-mapped by name)`));
836
+ }
837
+ }
838
+ // Fetch destination connections
839
+ if (!jsonOutput) {
840
+ console.log((0, colors_1.dim)('→ Fetching connections from destination...'));
841
+ }
842
+ const destConnections = await destClient.getAllPaginated('/v3/connections', {});
843
+ const destConnection = destConnections.data.find((c) => c.name === destConnectionName);
844
+ if (!destConnection) {
845
+ const errorMsg = [
846
+ `Connection "${destConnectionName}" not found in destination organization.`,
847
+ '',
848
+ 'Solutions:',
849
+ ' 1. Create the connection in destination organization',
850
+ ' 2. Use --connection to specify a different destination connection name'
851
+ ].join('\n');
852
+ throw new Error(errorMsg);
853
+ }
854
+ const destConnectionId = destConnection.id;
855
+ // Step 5: Validate source tables (unless --skip-source-validation)
856
+ if (!options.skipSourceValidation) {
857
+ // Extract source tables from workflow config nodes
858
+ const sourceTables = [];
859
+ if (workflow.config && workflow.config.nodes && Array.isArray(workflow.config.nodes)) {
860
+ for (const node of workflow.config.nodes) {
861
+ // Look for nodes that reference source tables
862
+ // Common patterns: node.data.source, node.data.tableName, node.data.table
863
+ if (node.data) {
864
+ const source = node.data.source || node.data.tableName || node.data.table;
865
+ if (source && typeof source === 'string' && source.includes('.')) {
866
+ // Only include if it looks like a FQN (has dots) and not a temp table
867
+ if (!source.startsWith('temp_') && !source.startsWith('tmp_')) {
868
+ if (!sourceTables.includes(source)) {
869
+ sourceTables.push(source);
870
+ }
871
+ }
872
+ }
873
+ }
874
+ }
875
+ }
876
+ if (sourceTables.length > 0) {
877
+ if (!jsonOutput) {
878
+ console.log((0, colors_1.dim)(`→ Validating ${sourceTables.length} source table(s)...`));
879
+ }
880
+ const inaccessibleTables = [];
881
+ for (const tableFqn of sourceTables) {
882
+ const result = await validateTableSource(destClient, destConnectionName, tableFqn, jsonOutput);
883
+ if (!result.accessible) {
884
+ inaccessibleTables.push({
885
+ table: tableFqn,
886
+ error: result.error
887
+ });
888
+ }
889
+ }
890
+ if (inaccessibleTables.length > 0) {
891
+ const errorMsg = [
892
+ 'Source validation failed - workflow references inaccessible tables:',
893
+ ...inaccessibleTables.map(t => ` • ${t.table}\n Error: ${t.error}`),
894
+ '',
895
+ 'Solutions:',
896
+ ' 1. Grant access to these tables in destination connection',
897
+ ' 2. Ensure tables exist in destination data warehouse',
898
+ ' 3. Use --skip-source-validation to create workflow anyway (may fail at runtime)'
899
+ ].join('\n');
900
+ throw new Error(errorMsg);
901
+ }
902
+ if (!jsonOutput) {
903
+ console.log((0, colors_1.dim)(`→ All source table(s) validated successfully`));
904
+ }
905
+ }
906
+ else {
907
+ if (!jsonOutput) {
908
+ console.log((0, colors_1.dim)('→ No source tables found to validate'));
909
+ }
910
+ }
911
+ }
912
+ // Step 6: Create workflow in destination
913
+ const newTitle = options.title || workflow.title || workflow.config?.title;
914
+ if (!jsonOutput) {
915
+ console.log((0, colors_1.dim)(`→ Creating workflow "${newTitle}" in destination...`));
916
+ }
917
+ // Update config with new title if provided
918
+ const updatedConfig = { ...workflow.config };
919
+ if (options.title) {
920
+ updatedConfig.title = options.title;
921
+ }
922
+ const workflowPayload = {
923
+ connectionId: destConnectionId,
924
+ config: updatedConfig,
925
+ client: 'carto-cli'
926
+ };
927
+ const newWorkflow = await destClient.post('/v3/workflows', workflowPayload);
928
+ const newWorkflowId = newWorkflow.id;
929
+ if (!newWorkflowId) {
930
+ throw new Error('Failed to create workflow in destination');
931
+ }
932
+ if (jsonOutput) {
933
+ console.log(JSON.stringify({
934
+ success: true,
935
+ sourceWorkflowId: workflowId,
936
+ newWorkflowId: newWorkflowId,
937
+ sourceProfile: sourceProfile,
938
+ destProfile: destProfile,
939
+ sourceConnection: sourceConnectionName,
940
+ destConnection: destConnectionName,
941
+ }));
942
+ }
943
+ else {
944
+ console.log((0, colors_1.success)(`\n✓ Workflow copied successfully!`));
945
+ console.log((0, colors_1.bold)('New Workflow ID: ') + newWorkflowId);
946
+ console.log((0, colors_1.dim)(`Source connection: ${sourceConnectionName} → Destination connection: ${destConnectionName}`));
947
+ }
948
+ }
949
+ catch (err) {
950
+ if (jsonOutput) {
951
+ console.log(JSON.stringify({ success: false, error: err.message }));
952
+ }
953
+ else {
954
+ console.log((0, colors_1.error)('✗ Failed to copy workflow: ' + err.message));
955
+ }
956
+ process.exit(1);
957
+ }
958
+ }
959
+ /**
960
+ * Remove a workflow schedule
961
+ */
962
+ async function workflowScheduleRemove(workflowId, options, token, baseUrl, jsonOutput, debug = false, profile) {
963
+ try {
964
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
965
+ if (!jsonOutput) {
966
+ console.log((0, colors_1.dim)('Fetching workflow details...'));
967
+ }
968
+ // 1. Get workflow details
969
+ const workflow = await client.get(`/v3/workflows/${workflowId}`);
970
+ if (!workflow.connectionId) {
971
+ throw new Error('Workflow does not have a connectionId');
972
+ }
973
+ if (!jsonOutput) {
974
+ console.log((0, colors_1.dim)(`Workflow: ${workflow.title || workflow.config?.title || workflowId}`));
975
+ console.log((0, colors_1.dim)('Fetching connection details...'));
976
+ }
977
+ // 2. Get connection details
978
+ const connection = await client.get(`/v3/connections/${workflow.connectionId}`);
979
+ const connectionName = connection.name;
980
+ const region = connection.region || 'US';
981
+ if (!jsonOutput) {
982
+ console.log((0, colors_1.dim)(`Connection: ${connectionName} (region: ${region})`));
983
+ console.log((0, colors_1.dim)('Removing schedule from data warehouse...'));
984
+ }
985
+ // 3. Delete schedule via SQL API
986
+ const schedulePath = `/v3/sql/${encodeURIComponent(connectionName)}/schedule/workflows.${workflowId}?region=${region}&client=workflows-schedule`;
987
+ await client.delete(schedulePath);
988
+ if (!jsonOutput) {
989
+ console.log((0, colors_1.dim)('Updating workflow metadata...'));
990
+ }
991
+ // 4. Update workflow to remove schedule metadata from config
992
+ const updatedConfig = { ...workflow.config };
993
+ delete updatedConfig.schedule;
994
+ const updateBody = {
995
+ config: updatedConfig,
996
+ client: 'carto-cli'
997
+ };
998
+ await client.patch(`/v3/workflows/${workflowId}`, updateBody);
999
+ if (jsonOutput) {
1000
+ console.log(JSON.stringify({
1001
+ success: true,
1002
+ workflowId
1003
+ }));
1004
+ }
1005
+ else {
1006
+ console.log((0, colors_1.success)(`\n✓ Schedule removed successfully`));
1007
+ console.log((0, colors_1.bold)('Workflow: ') + workflowId);
1008
+ }
1009
+ }
1010
+ catch (err) {
1011
+ if (jsonOutput) {
1012
+ console.log(JSON.stringify({ success: false, error: err.message }));
1013
+ }
1014
+ else {
1015
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
1016
+ console.log((0, colors_1.error)('✗ Authentication required'));
1017
+ console.log('Please run: carto auth login');
1018
+ }
1019
+ else {
1020
+ console.log((0, colors_1.error)('✗ Failed to remove schedule: ' + err.message));
1021
+ }
1022
+ }
1023
+ process.exit(1);
1024
+ }
1025
+ }