UNPKG

@ably/cli

Version:

Ably CLI for Pub/Sub, Chat and Spaces

362 lines (361 loc) 13.8 kB
import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import toml from "toml"; export class ConfigManager { config = { accounts: {}, }; configDir; configPath; constructor() { // Determine config directory: Use ABLY_CLI_CONFIG_DIR env var if set, otherwise default const customConfigDir = process.env.ABLY_CLI_CONFIG_DIR; this.configDir = customConfigDir || path.join(os.homedir(), ".ably"); // Define the config file path within the determined directory this.configPath = path.join(this.configDir, "config"); // Ensure the directory exists and load the configuration this.ensureConfigDirExists(); this.loadConfig(); } // Clear conversation context clearHelpContext() { delete this.config.helpContext; this.saveConfig(); } // Get access token for the current account or specific alias getAccessToken(alias) { if (alias) { return this.config.accounts[alias]?.accessToken; } const currentAccount = this.getCurrentAccount(); return currentAccount?.accessToken; } // Get API key for current app or specific app ID getApiKey(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) { // Fallback to environment variable if no config available return process.env.ABLY_API_KEY; } const targetAppId = appId || this.getCurrentAppId(); if (!targetAppId) { // Fallback to environment variable if no current app return process.env.ABLY_API_KEY; } // Return configured API key or fallback to environment variable return currentAccount.apps[targetAppId]?.apiKey || process.env.ABLY_API_KEY; } // Get app name for specific app ID getAppName(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) return undefined; return currentAccount.apps[appId]?.appName; } // Get full app configuration for specific app ID getAppConfig(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) return undefined; const cfg = currentAccount.apps[appId]; return cfg ? { ...cfg } : undefined; } // Get path to config file getConfigPath() { return this.configPath; } // Get the current account configuration getCurrentAccount() { const currentAlias = this.getCurrentAccountAlias(); if (!currentAlias) return undefined; return this.config.accounts[currentAlias]; } // Get the current account alias getCurrentAccountAlias() { return this.config.current?.account; } // Get current app ID for the current account getCurrentAppId() { const currentAccount = this.getCurrentAccount(); if (!currentAccount) return undefined; return currentAccount.currentAppId; } // Get conversation context for AI help getHelpContext() { return this.config.helpContext; } // Get key ID for the current app or specific app ID getKeyId(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) return undefined; const targetAppId = appId || this.getCurrentAppId(); if (!targetAppId) return undefined; // Get from specific metadata field or extract from API key const appConfig = currentAccount.apps[targetAppId]; if (!appConfig) return undefined; if (appConfig.keyId) { return appConfig.keyId; } if (appConfig.apiKey) { return appConfig.apiKey.split(":")[0]; } return undefined; } // Get key name for the current app or specific app ID getKeyName(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) return undefined; const targetAppId = appId || this.getCurrentAppId(); if (!targetAppId) return undefined; return currentAccount.apps[targetAppId]?.keyName; } // List all accounts listAccounts() { return Object.entries(this.config.accounts).map(([alias, account]) => ({ account, alias, })); } // Remove an account removeAccount(alias) { if (!this.config.accounts[alias]) { return false; } delete this.config.accounts[alias]; // If the removed account was the current one, clear the current account selection if (this.config.current?.account === alias) { delete this.config.current.account; } this.saveConfig(); return true; } // Remove API key for an app removeApiKey(appId) { const currentAccount = this.getCurrentAccount(); if (!currentAccount || !currentAccount.apps) return false; if (currentAccount.apps[appId]) { delete currentAccount.apps[appId].apiKey; this.saveConfig(); return true; } return false; } saveConfig() { try { // Format the config as TOML const tomlContent = this.formatToToml(this.config); // Write the config to disk fs.writeFileSync(this.configPath, tomlContent, { mode: 0o600 }); // Secure file permissions } catch (error) { throw new Error(`Failed to save Ably config: ${error}`); } } // Set current app for the current account setCurrentApp(appId) { const currentAccount = this.getCurrentAccount(); const currentAlias = this.getCurrentAccountAlias(); if (!currentAccount || !currentAlias) { throw new Error("No current account selected"); } // Set the current app for this account this.config.accounts[currentAlias].currentAppId = appId; this.saveConfig(); } // Store account information with an optional alias storeAccount(accessToken, alias = "default", accountInfo) { // Create or update the account entry this.config.accounts[alias] = { accessToken, ...accountInfo, apps: this.config.accounts[alias]?.apps || {}, currentAppId: this.config.accounts[alias]?.currentAppId, }; // Set as current account if it's the first one or no current account is set if (!this.config.current || !this.config.current.account) { this.config.current = { account: alias }; } this.saveConfig(); } // Store app information (like name) in the config storeAppInfo(appId, appInfo, accountAlias) { const alias = accountAlias || this.getCurrentAccountAlias() || "default"; // Ensure the account and apps structure exists if (!this.config.accounts[alias]) { throw new Error(`Account "${alias}" not found`); } if (!this.config.accounts[alias].apps) { this.config.accounts[alias].apps = {}; } // Store the app info this.config.accounts[alias].apps[appId] = { ...this.config.accounts[alias].apps[appId], ...appInfo, }; this.saveConfig(); } // Updated storeAppKey to include key metadata storeAppKey(appId, apiKey, metadata, accountAlias) { const alias = accountAlias || this.getCurrentAccountAlias() || "default"; // Ensure the account and apps structure exists if (!this.config.accounts[alias]) { throw new Error(`Account "${alias}" not found`); } if (!this.config.accounts[alias].apps) { this.config.accounts[alias].apps = {}; } // Store the API key and metadata this.config.accounts[alias].apps[appId] = { ...this.config.accounts[alias].apps[appId], apiKey, appName: metadata?.appName, keyId: metadata?.keyId || apiKey.split(":")[0], // Extract key ID if not provided keyName: metadata?.keyName, }; this.saveConfig(); } // Store conversation context for AI help storeHelpContext(question, answer) { if (!this.config.helpContext) { this.config.helpContext = { conversation: { messages: [], }, }; } // Add the user's question this.config.helpContext.conversation.messages.push({ content: question, role: "user", }, { content: answer, role: "assistant", }); this.saveConfig(); } // Switch to a different account switchAccount(alias) { if (!this.config.accounts[alias]) { return false; } if (!this.config.current) { this.config.current = {}; } this.config.current.account = alias; this.saveConfig(); return true; } ensureConfigDirExists() { if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { mode: 0o700 }); // Secure permissions } } // Updated formatToToml method to include app and key metadata formatToToml(config) { let result = ""; // Write current section if (config.current) { result += "[current]\n"; if (config.current.account) { result += `account = "${config.current.account}"\n`; } result += "\n"; } // Write help context if it exists if (config.helpContext) { result += "[helpContext]\n"; // Format the conversation as TOML array of tables if (config.helpContext.conversation.messages.length > 0) { result += "\n[[helpContext.conversation.messages]]\n"; const { messages } = config.helpContext.conversation; for (const [i, message] of messages.entries()) { if (i > 0) result += "\n[[helpContext.conversation.messages]]\n"; result += `role = "${message.role}"\n`; result += `content = """${message.content}"""\n`; } result += "\n"; } } // Write accounts section for (const [alias, account] of Object.entries(config.accounts)) { result += `[accounts.${alias}]\n`; result += `accessToken = "${account.accessToken}"\n`; if (account.tokenId) { result += `tokenId = "${account.tokenId}"\n`; } if (account.userEmail) { result += `userEmail = "${account.userEmail}"\n`; } if (account.accountId) { result += `accountId = "${account.accountId}"\n`; } if (account.accountName) { result += `accountName = "${account.accountName}"\n`; } if (account.currentAppId) { result += `currentAppId = "${account.currentAppId}"\n`; } // Write apps section for this account if (account.apps && Object.keys(account.apps).length > 0) { for (const [appId, appConfig] of Object.entries(account.apps)) { result += `[accounts.${alias}.apps.${appId}]\n`; if (appConfig.apiKey) { result += `apiKey = "${appConfig.apiKey}"\n`; } if (appConfig.keyId) { result += `keyId = "${appConfig.keyId}"\n`; } if (appConfig.keyName) { result += `keyName = "${appConfig.keyName}"\n`; } if (appConfig.appName) { result += `appName = "${appConfig.appName}"\n`; } result += "\n"; } } else { result += "\n"; } } return result; } loadConfig() { if (fs.existsSync(this.configPath)) { try { const configContent = fs.readFileSync(this.configPath, "utf8"); this.config = toml.parse(configContent); // Ensure config has the expected structure if (!this.config.accounts) { this.config.accounts = {}; } // Migrate old config format if needed - move app from current to account.currentAppId // eslint-disable-next-line @typescript-eslint/no-explicit-any const oldConfig = this.config; // Use 'any' to safely access potential pre-migration properties if (oldConfig.current?.app) { const currentAccountAlias = this.config.current?.account; if (currentAccountAlias && this.config.accounts[currentAccountAlias]) { this.config.accounts[currentAccountAlias].currentAppId = oldConfig.current.app; delete oldConfig.current.app; // Remove from current section this.saveConfig(); // Save the migrated config } } } catch (error) { throw new Error(`Failed to load Ably config: ${error}`); } } } }