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.
package/dist/config.js ADDED
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getCurrentProfile = getCurrentProfile;
7
+ exports.setCurrentProfile = setCurrentProfile;
8
+ exports.suggestProfileName = suggestProfileName;
9
+ exports.loadCredentials = loadCredentials;
10
+ exports.loadAllCredentials = loadAllCredentials;
11
+ exports.listProfileNames = listProfileNames;
12
+ exports.saveCredentials = saveCredentials;
13
+ exports.deleteCredentials = deleteCredentials;
14
+ exports.loadConfig = loadConfig;
15
+ exports.saveConfig = saveConfig;
16
+ exports.deleteConfig = deleteConfig;
17
+ exports.getApiToken = getApiToken;
18
+ exports.fetchTenantConfig = fetchTenantConfig;
19
+ exports.getTenantId = getTenantId;
20
+ exports.getLiteLLMUrl = getLiteLLMUrl;
21
+ exports.getAllUrls = getAllUrls;
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const os_1 = __importDefault(require("os"));
25
+ const js_yaml_1 = __importDefault(require("js-yaml"));
26
+ const http_1 = require("./http");
27
+ // In-memory cache for tenant config (per process)
28
+ const tenantConfigCache = new Map();
29
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.carto');
30
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
31
+ const CREDENTIALS_FILE = path_1.default.join(os_1.default.homedir(), '.carto_credentials.json');
32
+ // Migrate old credentials file format to new format
33
+ function migrateCredentialsFile(data) {
34
+ // Check if already in new format
35
+ if (data.profiles !== undefined) {
36
+ return data;
37
+ }
38
+ // Old format: profiles are at root level
39
+ // Migrate to new format with current_profile and profiles structure
40
+ const migratedProfiles = {};
41
+ for (const [profileName, creds] of Object.entries(data)) {
42
+ // Only migrate if it looks like a profile (has token field)
43
+ if (typeof creds === 'object' && creds !== null && 'token' in creds) {
44
+ const oldCreds = creds;
45
+ migratedProfiles[profileName] = {
46
+ token: oldCreds.token,
47
+ tenant_id: oldCreds.tenant_id,
48
+ tenant_domain: oldCreds.tenant_domain,
49
+ // For old profiles, we don't have this info
50
+ organization_id: oldCreds.organization_id || '',
51
+ organization_name: oldCreds.organization_name || '',
52
+ user_email: oldCreds.user_email || '',
53
+ };
54
+ }
55
+ }
56
+ return {
57
+ current_profile: 'default', // Default to 'default' profile for backwards compatibility
58
+ profiles: migratedProfiles,
59
+ };
60
+ }
61
+ // Load all credentials with migration
62
+ function loadCredentialsFile() {
63
+ try {
64
+ if (fs_1.default.existsSync(CREDENTIALS_FILE)) {
65
+ const data = fs_1.default.readFileSync(CREDENTIALS_FILE, 'utf-8');
66
+ const parsed = JSON.parse(data);
67
+ const migrated = migrateCredentialsFile(parsed);
68
+ // Save migrated format if it was changed
69
+ if (parsed.profiles === undefined && Object.keys(migrated.profiles).length > 0) {
70
+ fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(migrated, null, 2), 'utf-8');
71
+ }
72
+ return migrated;
73
+ }
74
+ }
75
+ catch (error) {
76
+ // Ignore errors, return empty structure
77
+ }
78
+ return { profiles: {} };
79
+ }
80
+ // Get current profile name with fallbacks
81
+ function getCurrentProfile() {
82
+ // Priority: CARTO_PROFILE env var > current_profile field > 'default'
83
+ if (process.env.CARTO_PROFILE) {
84
+ return process.env.CARTO_PROFILE;
85
+ }
86
+ const file = loadCredentialsFile();
87
+ return file.current_profile || 'default';
88
+ }
89
+ // Set current profile
90
+ function setCurrentProfile(profileName) {
91
+ const file = loadCredentialsFile();
92
+ // Verify profile exists
93
+ if (!file.profiles[profileName]) {
94
+ throw new Error(`Profile '${profileName}' not found`);
95
+ }
96
+ file.current_profile = profileName;
97
+ fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(file, null, 2), 'utf-8');
98
+ }
99
+ // Generate suggested profile name
100
+ function suggestProfileName(credentials) {
101
+ const parts = [];
102
+ // Add auth environment prefix only for non-production
103
+ if (credentials.auth_environment && credentials.auth_environment !== 'production') {
104
+ parts.push(credentials.auth_environment);
105
+ }
106
+ if (credentials.tenant_id) {
107
+ parts.push(credentials.tenant_id);
108
+ }
109
+ if (credentials.organization_name) {
110
+ parts.push(credentials.organization_name);
111
+ }
112
+ if (credentials.user_email) {
113
+ parts.push(credentials.user_email);
114
+ }
115
+ return parts.join('/');
116
+ }
117
+ // Load specific profile credentials
118
+ function loadCredentials(profileName) {
119
+ try {
120
+ const file = loadCredentialsFile();
121
+ const targetProfile = profileName || getCurrentProfile();
122
+ return file.profiles[targetProfile] || null;
123
+ }
124
+ catch (error) {
125
+ return null;
126
+ }
127
+ }
128
+ // Load all credentials
129
+ function loadAllCredentials() {
130
+ return loadCredentialsFile();
131
+ }
132
+ // Get list of profile names
133
+ function listProfileNames() {
134
+ const file = loadCredentialsFile();
135
+ return Object.keys(file.profiles);
136
+ }
137
+ // Save credentials for a profile
138
+ function saveCredentials(profileName, credentials, setAsCurrent = true) {
139
+ try {
140
+ const file = loadCredentialsFile();
141
+ // Add or update the profile
142
+ file.profiles[profileName] = credentials;
143
+ // Set as current if requested or if it's the first profile
144
+ if (setAsCurrent || Object.keys(file.profiles).length === 1) {
145
+ file.current_profile = profileName;
146
+ }
147
+ fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(file, null, 2), 'utf-8');
148
+ }
149
+ catch (error) {
150
+ throw new Error(`Failed to save credentials: ${error}`);
151
+ }
152
+ }
153
+ // Delete credentials for a profile
154
+ function deleteCredentials(profileName) {
155
+ try {
156
+ const file = loadCredentialsFile();
157
+ if (!file.profiles[profileName]) {
158
+ throw new Error(`Profile '${profileName}' not found`);
159
+ }
160
+ delete file.profiles[profileName];
161
+ // If we deleted the current profile, switch to another one or clear
162
+ if (file.current_profile === profileName) {
163
+ const remainingProfiles = Object.keys(file.profiles);
164
+ file.current_profile = remainingProfiles.length > 0 ? remainingProfiles[0] : undefined;
165
+ }
166
+ // If no profiles left, delete the file
167
+ if (Object.keys(file.profiles).length === 0) {
168
+ fs_1.default.unlinkSync(CREDENTIALS_FILE);
169
+ }
170
+ else {
171
+ fs_1.default.writeFileSync(CREDENTIALS_FILE, JSON.stringify(file, null, 2), 'utf-8');
172
+ }
173
+ }
174
+ catch (error) {
175
+ throw new Error(`Failed to delete credentials: ${error}`);
176
+ }
177
+ }
178
+ // Legacy config file functions (kept for backwards compatibility)
179
+ function loadConfig() {
180
+ try {
181
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
182
+ const data = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
183
+ return JSON.parse(data);
184
+ }
185
+ }
186
+ catch (error) {
187
+ // Ignore errors, return empty config
188
+ }
189
+ return {};
190
+ }
191
+ function saveConfig(config) {
192
+ try {
193
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
194
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
195
+ }
196
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
197
+ }
198
+ catch (error) {
199
+ throw new Error(`Failed to save config: ${error}`);
200
+ }
201
+ }
202
+ function deleteConfig() {
203
+ try {
204
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
205
+ fs_1.default.unlinkSync(CONFIG_FILE);
206
+ }
207
+ }
208
+ catch (error) {
209
+ throw new Error(`Failed to delete config: ${error}`);
210
+ }
211
+ }
212
+ function getApiToken(profileName) {
213
+ // Priority: environment variable > credentials file > legacy config file
214
+ if (process.env.CARTO_API_TOKEN) {
215
+ return process.env.CARTO_API_TOKEN;
216
+ }
217
+ const credentials = loadCredentials(profileName);
218
+ if (credentials) {
219
+ return credentials.token;
220
+ }
221
+ return loadConfig().apiToken;
222
+ }
223
+ /**
224
+ * Fetch tenant configuration from config.yaml
225
+ * @param tenant_domain The tenant domain (e.g., "clausa-26.dev.app.carto.com")
226
+ * @returns Tenant configuration with API URLs
227
+ */
228
+ async function fetchTenantConfig(tenant_domain) {
229
+ // Check cache first
230
+ if (tenantConfigCache.has(tenant_domain)) {
231
+ return tenantConfigCache.get(tenant_domain);
232
+ }
233
+ const configUrl = `https://${tenant_domain}/config.yaml`;
234
+ try {
235
+ const response = await (0, http_1.request)(configUrl, { method: 'GET' });
236
+ if (response.statusCode !== 200) {
237
+ throw new Error(`HTTP ${response.statusCode}: ${response.body}`);
238
+ }
239
+ // Parse YAML
240
+ const config = js_yaml_1.default.load(response.body);
241
+ if (!config || !config.apis) {
242
+ throw new Error('Invalid config.yaml format: missing apis section');
243
+ }
244
+ const tenantConfig = {
245
+ accountsUrl: config.apis.accountsUrl,
246
+ workspaceUrl: config.apis.workspaceUrl,
247
+ baseUrl: config.apis.baseUrl,
248
+ aiApi: config.apis.aiApi,
249
+ };
250
+ // Validate required fields
251
+ if (!tenantConfig.accountsUrl || !tenantConfig.workspaceUrl || !tenantConfig.baseUrl || !tenantConfig.aiApi) {
252
+ throw new Error('Invalid config.yaml: missing required API URLs');
253
+ }
254
+ // Cache the config
255
+ tenantConfigCache.set(tenant_domain, tenantConfig);
256
+ return tenantConfig;
257
+ }
258
+ catch (error) {
259
+ throw new Error(`Failed to fetch tenant configuration from ${configUrl}: ${error.message}`);
260
+ }
261
+ }
262
+ /**
263
+ * Get validated tenant configuration
264
+ * Loads credentials, validates tenant_domain, and fetches config once
265
+ */
266
+ async function getValidatedTenantConfig(profileName) {
267
+ const credentials = loadCredentials(profileName);
268
+ if (!credentials) {
269
+ throw new Error('Not authenticated. Please run: carto auth login');
270
+ }
271
+ if (!credentials.tenant_domain) {
272
+ throw new Error('Profile configuration incomplete: tenant_domain not found. Please re-authenticate with: carto auth login');
273
+ }
274
+ return await fetchTenantConfig(credentials.tenant_domain);
275
+ }
276
+ async function getTenantId(profileName) {
277
+ const credentials = loadCredentials(profileName);
278
+ if (!credentials) {
279
+ throw new Error('Not authenticated. Please run: carto auth login');
280
+ }
281
+ return credentials.tenant_id;
282
+ }
283
+ // TODO: LiteLLM URL is not available in config.yaml
284
+ // Keep using hardcoded construction for now
285
+ function getLiteLLMUrl(profileName) {
286
+ const credentials = loadCredentials(profileName);
287
+ if (credentials) {
288
+ return `https://litellm-${credentials.tenant_id}.api.carto.com`;
289
+ }
290
+ return 'https://litellm-gcp-us-east1.api.carto.com';
291
+ }
292
+ /**
293
+ * Helper function to get all URLs at once for ApiClient
294
+ * Returns URLs from config.yaml if tenant_domain is available
295
+ */
296
+ async function getAllUrls(profileName) {
297
+ const credentials = loadCredentials(profileName);
298
+ // For M2M profiles with base_url set directly, skip tenant config lookup
299
+ if (credentials?.base_url) {
300
+ // Extract region from base_url (e.g., "gcp-us-east1" from "https://gcp-us-east1.api.carto.com")
301
+ const regionMatch = credentials.base_url.match(/https:\/\/([^.]+)\.api\.carto\.com/);
302
+ const region = regionMatch ? regionMatch[1] : 'gcp-us-east1';
303
+ return {
304
+ baseUrl: credentials.base_url,
305
+ workspaceUrl: `https://workspace-${region}.app.carto.com`,
306
+ accountsUrl: 'https://accounts.app.carto.com',
307
+ aiApiUrl: `https://ai-${region}.api.carto.com`,
308
+ litellmUrl: `https://litellm-${region}.api.carto.com`
309
+ };
310
+ }
311
+ const config = await getValidatedTenantConfig(profileName);
312
+ const litellmUrl = getLiteLLMUrl(profileName);
313
+ return {
314
+ baseUrl: config.baseUrl,
315
+ workspaceUrl: config.workspaceUrl,
316
+ accountsUrl: config.accountsUrl,
317
+ aiApiUrl: config.aiApi,
318
+ litellmUrl
319
+ };
320
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.downloadFile = downloadFile;
7
+ exports.formatBytes = formatBytes;
8
+ exports.getFilenameFromUrl = getFilenameFromUrl;
9
+ exports.sanitizeFilename = sanitizeFilename;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ /**
13
+ * Downloads a file from a URL to a local path
14
+ * @param url - The URL to download from
15
+ * @param outputPath - The local file path to save to
16
+ * @param onProgress - Optional callback for progress updates
17
+ */
18
+ async function downloadFile(url, outputPath, onProgress) {
19
+ // Ensure output directory exists
20
+ const outputDir = path_1.default.dirname(outputPath);
21
+ if (!fs_1.default.existsSync(outputDir)) {
22
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
23
+ }
24
+ return new Promise((resolve, reject) => {
25
+ const https = require('https');
26
+ const http = require('http');
27
+ const protocol = url.startsWith('https:') ? https : http;
28
+ protocol.get(url, (response) => {
29
+ if (response.statusCode === 301 || response.statusCode === 302) {
30
+ // Handle redirects
31
+ const redirectUrl = response.headers.location;
32
+ if (redirectUrl) {
33
+ downloadFile(redirectUrl, outputPath, onProgress).then(resolve).catch(reject);
34
+ return;
35
+ }
36
+ }
37
+ if (response.statusCode !== 200) {
38
+ reject(new Error(`Download failed with status ${response.statusCode}`));
39
+ return;
40
+ }
41
+ const totalSize = parseInt(response.headers['content-length'] || '0', 10);
42
+ let downloadedSize = 0;
43
+ const fileStream = fs_1.default.createWriteStream(outputPath);
44
+ response.on('data', (chunk) => {
45
+ downloadedSize += chunk.length;
46
+ if (onProgress && totalSize > 0) {
47
+ const percentage = Math.round((downloadedSize / totalSize) * 100);
48
+ onProgress({
49
+ downloaded: downloadedSize,
50
+ total: totalSize,
51
+ percentage
52
+ });
53
+ }
54
+ });
55
+ response.pipe(fileStream);
56
+ fileStream.on('finish', () => {
57
+ fileStream.close();
58
+ resolve();
59
+ });
60
+ fileStream.on('error', (err) => {
61
+ fs_1.default.unlinkSync(outputPath);
62
+ reject(err);
63
+ });
64
+ response.on('error', (err) => {
65
+ fs_1.default.unlinkSync(outputPath);
66
+ reject(err);
67
+ });
68
+ }).on('error', (err) => {
69
+ reject(err);
70
+ });
71
+ });
72
+ }
73
+ /**
74
+ * Format bytes to human-readable string
75
+ */
76
+ function formatBytes(bytes) {
77
+ if (bytes === 0)
78
+ return '0 B';
79
+ const k = 1024;
80
+ const sizes = ['B', 'KB', 'MB', 'GB'];
81
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
82
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
83
+ }
84
+ /**
85
+ * Extract filename from URL
86
+ */
87
+ function getFilenameFromUrl(url) {
88
+ try {
89
+ const urlObj = new URL(url);
90
+ const pathname = urlObj.pathname;
91
+ const filename = pathname.substring(pathname.lastIndexOf('/') + 1);
92
+ // Decode URL-encoded characters
93
+ return decodeURIComponent(filename);
94
+ }
95
+ catch (err) {
96
+ return 'downloaded_file';
97
+ }
98
+ }
99
+ /**
100
+ * Sanitize filename to make it safe for filesystem
101
+ */
102
+ function sanitizeFilename(filename) {
103
+ // Remove or replace unsafe characters
104
+ return filename
105
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
106
+ .replace(/\s+/g, '_')
107
+ .substring(0, 255); // Limit length
108
+ }