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
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FilteredStdioServerTransport } from './custom-stdio.js';
import { server, flushDeferredMessages } from './server.js';
import { commandManager } from './command-manager.js';
import { configManager } from './config-manager.js';
import { featureFlagManager } from './utils/feature-flags.js';
import { runSetup } from './npm-scripts/setup.js';
import { runUninstall } from './npm-scripts/uninstall.js';
import { capture } from './utils/capture.js';
Expand Down Expand Up @@ -42,6 +43,10 @@ async function runServer() {
deferLog('info', 'Loading configuration...');
await configManager.loadConfig();
deferLog('info', 'Configuration loaded successfully');

// Initialize feature flags (non-blocking)
deferLog('info', 'Initializing feature flags...');
await featureFlagManager.initialize();
} catch (configError) {
deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
if (configError instanceof Error && configError.stack) {
Expand Down
2 changes: 2 additions & 0 deletions src/tools/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { configManager, ServerConfig } from '../config-manager.js';
import { SetConfigValueArgsSchema } from './schemas.js';
import { getSystemInfo } from '../utils/system-info.js';
import { currentClient } from '../server.js';
import { featureFlagManager } from '../utils/feature-flags.js';

/**
* Get the entire config including system information
Expand All @@ -27,6 +28,7 @@ export async function getConfig() {
const configWithSystemInfo = {
...config,
currentClient,
featureFlags: featureFlagManager.getAll(),
systemInfo: {
...systemInfo,
memory
Expand Down
4 changes: 2 additions & 2 deletions src/tools/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function giveFeedbackToDesktopCommander(params: FeedbackParams = {}
content: [{
type: "text",
text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
`You can still access our feedback form directly at: https://tally.so/r/mYB6av\n\n` +
`You can still access our feedback form directly at: https://tally.so/r/mKqoKg\n\n` +
`We appreciate your willingness to provide feedback!`
}],
isError: true
Expand All @@ -100,7 +100,7 @@ export async function giveFeedbackToDesktopCommander(params: FeedbackParams = {}
* Build Tally.so URL with pre-filled parameters
*/
async function buildTallyUrl(params: FeedbackParams, stats: any): Promise<string> {
const baseUrl = 'https://tally.so/r/mYB6av';
const baseUrl = 'https://tally.so/r/mKqoKg';
const urlParams = new URLSearchParams();

// Only auto-filled hidden fields remain
Expand Down
175 changes: 175 additions & 0 deletions src/utils/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import fs from 'fs/promises';
import path from 'path';
import { existsSync } from 'fs';
import { CONFIG_FILE } from '../config.js';
import { logger } from './logger.js';

interface FeatureFlags {
version?: string;
flags?: Record<string, any>;
}

class FeatureFlagManager {
private flags: Record<string, any> = {};
private lastFetch: number = 0;
private cachePath: string;
private cacheMaxAge: number = 30 * 60 * 1000; // 5 minutes - hardcoded refresh interval
private flagUrl: string;
private refreshInterval: NodeJS.Timeout | null = null;

constructor() {
const configDir = path.dirname(CONFIG_FILE);
this.cachePath = path.join(configDir, 'feature-flags.json');

// Use production flags
this.flagUrl = process.env.DC_FLAG_URL ||
'https://desktopcommander.app/flags/v1/production.json';
}

/**
* Initialize - load from cache and start background refresh
*/
async initialize(): Promise<void> {
try {
// Load from cache immediately (non-blocking)
await this.loadFromCache();

// Fetch in background (don't block startup)
this.fetchFlags().catch(err => {
logger.debug('Initial flag fetch failed:', err.message);
});

// Start periodic refresh every 5 minutes
this.refreshInterval = setInterval(() => {
this.fetchFlags().catch(err => {
logger.debug('Periodic flag fetch failed:', err.message);
});
}, this.cacheMaxAge);

logger.info(`Feature flags initialized (refresh every ${this.cacheMaxAge / 1000}s)`);
} catch (error) {
logger.warning('Failed to initialize feature flags:', error);
}
}

/**
* Get a flag value
*/
get(flagName: string, defaultValue: any = false): any {
return this.flags[flagName] !== undefined ? this.flags[flagName] : defaultValue;
}

/**
* Get all flags for debugging
*/
getAll(): Record<string, any> {
return { ...this.flags };
}

/**
* Manually refresh flags immediately (for testing)
*/
async refresh(): Promise<boolean> {
try {
await this.fetchFlags();
return true;
} catch (error) {
logger.error('Manual refresh failed:', error);
return false;
}
}

/**
* Load flags from local cache
*/
private async loadFromCache(): Promise<void> {
try {
if (!existsSync(this.cachePath)) {
logger.debug('No feature flag cache found');
return;
}

const data = await fs.readFile(this.cachePath, 'utf8');
const config: FeatureFlags = JSON.parse(data);

if (config.flags) {
this.flags = config.flags;
this.lastFetch = Date.now();
logger.debug(`Loaded ${Object.keys(this.flags).length} feature flags from cache`);
}
} catch (error) {
logger.warning('Failed to load feature flags from cache:', error);
}
}

/**
* Fetch flags from remote URL
*/
private async fetchFlags(): Promise<void> {
try {
logger.debug('Fetching feature flags from:', this.flagUrl);

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

const response = await fetch(this.flagUrl, {
signal: controller.signal,
headers: {
'Cache-Control': 'no-cache',
}
});

clearTimeout(timeout);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const config: FeatureFlags = await response.json();

// Update flags
if (config.flags) {
this.flags = config.flags;
this.lastFetch = Date.now();

// Save to cache
await this.saveToCache(config);

logger.info(`Feature flags updated: ${Object.keys(this.flags).length} flags`);
}
} catch (error: any) {
logger.debug('Failed to fetch feature flags:', error.message);
// Continue with cached values
}
}

/**
* Save flags to local cache
*/
private async saveToCache(config: FeatureFlags): Promise<void> {
try {
const configDir = path.dirname(this.cachePath);
if (!existsSync(configDir)) {
await fs.mkdir(configDir, { recursive: true });
}

await fs.writeFile(this.cachePath, JSON.stringify(config, null, 2), 'utf8');
logger.debug('Saved feature flags to cache');
} catch (error) {
logger.warning('Failed to save feature flags to cache:', error);
}
}

/**
* Cleanup on shutdown
*/
destroy(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
}

// Export singleton instance
export const featureFlagManager = new FeatureFlagManager();
Loading