Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"ora": "^8.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"toml": "^3.0.0",
"smol-toml": "^1.5.2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ws": "^8.16.0",
"zod": "^3.24.2"
},
Expand Down
25 changes: 10 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import chalk from "chalk";
import colorJson from "color-json";
import { randomUUID } from "node:crypto";

import { ConfigManager } from "./services/config-manager.js";
import {
ConfigManager,
createConfigManager,
} from "./services/config-manager.js";
import { ControlApi } from "./services/control-api.js";
import { InteractiveHelper } from "./services/interactive-helper.js";
import { BaseFlags, CommandConfig, ErrorDetails } from "./types/cli.js";
Expand Down Expand Up @@ -170,7 +173,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {

constructor(argv: string[], config: CommandConfig) {
super(argv, config);
this.configManager = new ConfigManager();
this.configManager = createConfigManager();
this.interactiveHelper = new InteractiveHelper(this.configManager);
// Check if we're running in web CLI mode
this.isWebCliMode = isWebCliMode();
Expand Down
4 changes: 2 additions & 2 deletions src/commands/config/show.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as fs from "node:fs";
import * as toml from "toml";
import { parse } from "smol-toml";

import { AblyBaseCommand } from "../../base-command.js";

Expand Down Expand Up @@ -43,7 +43,7 @@ export default class ConfigShow extends AblyBaseCommand {
if (this.shouldOutputJson(flags)) {
// Parse the TOML and output as JSON
try {
const config = toml.parse(contents);
const config = parse(contents);
this.log(
this.formatJsonOutput(
{ exists: true, path: configPath, config },
Expand Down
4 changes: 2 additions & 2 deletions src/commands/mcp/start-server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AblyBaseCommand } from "../../base-command.js";
import { AblyMcpServer } from "../../mcp/index.js";
import { ConfigManager } from "../../services/config-manager.js";
import { createConfigManager } from "../../services/config-manager.js";

export default class StartMcpServer extends AblyBaseCommand {
static description =
Expand All @@ -20,7 +20,7 @@ export default class StartMcpServer extends AblyBaseCommand {
const { flags } = await this.parse(StartMcpServer);

// Initialize Config Manager
const configManager = new ConfigManager();
const configManager = createConfigManager();

try {
// Start the server, write to stderr only
Expand Down
7 changes: 5 additions & 2 deletions src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { Command, Help, Config, Interfaces } from "@oclif/core";
import chalk from "chalk";
import stripAnsi from "strip-ansi";

import { ConfigManager } from "./services/config-manager.js";
import {
ConfigManager,
createConfigManager,
} from "./services/config-manager.js";
import { displayLogo } from "./utils/logo.js";
import { formatReleaseStatus } from "./utils/version.js";

Expand All @@ -27,7 +30,7 @@ export default class CustomHelp extends Help {
this.webCliMode = isWebCliMode();
this.interactiveMode = process.env.ABLY_INTERACTIVE_MODE === "true";
this.anonymousMode = process.env.ABLY_ANONYMOUS_USER_MODE === "true";
this.configManager = new ConfigManager();
this.configManager = createConfigManager();
}

// Override formatHelpOutput to apply stripAnsi when necessary
Expand Down
182 changes: 88 additions & 94 deletions src/services/config-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import toml from "toml";
import { parse, stringify } from "smol-toml";
import isTestMode from "../utils/test-mode.js";

// Updated to include key and app metadata
export interface AppConfig {
Expand Down Expand Up @@ -38,7 +39,89 @@ export interface AblyConfig {
};
}

export class ConfigManager {
export interface ConfigManager {
// Account management
getAccessToken(alias?: string): string | undefined;
getCurrentAccount(): AccountConfig | undefined;
getCurrentAccountAlias(): string | undefined;
listAccounts(): { account: AccountConfig; alias: string }[];
storeAccount(
accessToken: string,
alias?: string,
accountInfo?: {
accountId?: string;
accountName?: string;
tokenId?: string;
userEmail?: string;
},
): void;
switchAccount(alias: string): boolean;
removeAccount(alias: string): boolean;

// App management
getApiKey(appId?: string): string | undefined;
getAppName(appId: string): string | undefined;
getAppConfig(appId: string): AppConfig | undefined;
getCurrentAppId(): string | undefined;
getKeyId(appId?: string): string | undefined;
getKeyName(appId?: string): string | undefined;
setCurrentApp(appId: string): void;
storeAppInfo(
appId: string,
appInfo: { appName: string },
accountAlias?: string,
): void;
storeAppKey(
appId: string,
apiKey: string,
metadata?: { appName?: string; keyId?: string; keyName?: string },
accountAlias?: string,
): void;
removeApiKey(appId: string): boolean;

// Help context (AI conversation)
getHelpContext():
| {
conversation: {
messages: {
content: string;
role: "assistant" | "user";
}[];
};
}
| undefined;
storeHelpContext(question: string, answer: string): void;
clearHelpContext(): void;

// Config file
getConfigPath(): string;
saveConfig(): void;
}

// Type declaration for test mocks available on globalThis
declare global {
var __TEST_MOCKS__:
| { configManager?: ConfigManager; [key: string]: unknown }
| undefined;
}

/**
* Factory function to create a ConfigManager instance.
* In test mode (when ABLY_CLI_TEST_MODE is set and mock is available),
* returns the MockConfigManager from globals.
* Otherwise returns a new TomlConfigManager.
*/
export function createConfigManager(): ConfigManager {
// Check if we're in test mode and have a mock available
if (isTestMode() && globalThis.__TEST_MOCKS__?.configManager) {
return globalThis.__TEST_MOCKS__.configManager;
}

// Default to TomlConfigManager for production use
return new TomlConfigManager();
}

export class TomlConfigManager implements ConfigManager {
private config: AblyConfig = {
accounts: {},
};
Expand Down Expand Up @@ -225,8 +308,8 @@ export class ConfigManager {

public saveConfig(): void {
try {
// Format the config as TOML
const tomlContent = this.formatToToml(this.config);
// Format the config as TOML using smol-toml stringify
const tomlContent = stringify(this.config);

// Write the config to disk
fs.writeFileSync(this.configPath, tomlContent, { mode: 0o600 }); // Secure file permissions
Expand Down Expand Up @@ -382,100 +465,11 @@ export class ConfigManager {
}
}

// Updated formatToToml method to include app and key metadata
private formatToToml(config: AblyConfig): string {
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;
}

private loadConfig(): void {
if (fs.existsSync(this.configPath)) {
try {
const configContent = fs.readFileSync(this.configPath, "utf8");
this.config = toml.parse(configContent) as AblyConfig;
this.config = parse(configContent) as unknown as AblyConfig;

// Ensure config has the expected structure
if (!this.config.accounts) {
Expand Down
Loading
Loading