Skip to content

Commit cc89540

Browse files
authored
Add feature switches and new survey (#265)
* Add feature switches and new survey
1 parent 252a00d commit cc89540

File tree

5 files changed

+271
-53
lines changed

5 files changed

+271
-53
lines changed

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FilteredStdioServerTransport } from './custom-stdio.js';
44
import { server, flushDeferredMessages } from './server.js';
55
import { commandManager } from './command-manager.js';
66
import { configManager } from './config-manager.js';
7+
import { featureFlagManager } from './utils/feature-flags.js';
78
import { runSetup } from './npm-scripts/setup.js';
89
import { runUninstall } from './npm-scripts/uninstall.js';
910
import { capture } from './utils/capture.js';
@@ -42,6 +43,10 @@ async function runServer() {
4243
deferLog('info', 'Loading configuration...');
4344
await configManager.loadConfig();
4445
deferLog('info', 'Configuration loaded successfully');
46+
47+
// Initialize feature flags (non-blocking)
48+
deferLog('info', 'Initializing feature flags...');
49+
await featureFlagManager.initialize();
4550
} catch (configError) {
4651
deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
4752
if (configError instanceof Error && configError.stack) {

src/tools/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { configManager, ServerConfig } from '../config-manager.js';
22
import { SetConfigValueArgsSchema } from './schemas.js';
33
import { getSystemInfo } from '../utils/system-info.js';
44
import { currentClient } from '../server.js';
5+
import { featureFlagManager } from '../utils/feature-flags.js';
56

67
/**
78
* Get the entire config including system information
@@ -27,6 +28,7 @@ export async function getConfig() {
2728
const configWithSystemInfo = {
2829
...config,
2930
currentClient,
31+
featureFlags: featureFlagManager.getAll(),
3032
systemInfo: {
3133
...systemInfo,
3234
memory

src/tools/feedback.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export async function giveFeedbackToDesktopCommander(params: FeedbackParams = {}
8888
content: [{
8989
type: "text",
9090
text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
91-
`You can still access our feedback form directly at: https://tally.so/r/mYB6av\n\n` +
91+
`You can still access our feedback form directly at: https://tally.so/r/mKqoKg\n\n` +
9292
`We appreciate your willingness to provide feedback!`
9393
}],
9494
isError: true
@@ -100,7 +100,7 @@ export async function giveFeedbackToDesktopCommander(params: FeedbackParams = {}
100100
* Build Tally.so URL with pre-filled parameters
101101
*/
102102
async function buildTallyUrl(params: FeedbackParams, stats: any): Promise<string> {
103-
const baseUrl = 'https://tally.so/r/mYB6av';
103+
const baseUrl = 'https://tally.so/r/mKqoKg';
104104
const urlParams = new URLSearchParams();
105105

106106
// Only auto-filled hidden fields remain

src/utils/feature-flags.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
import { existsSync } from 'fs';
4+
import { CONFIG_FILE } from '../config.js';
5+
import { logger } from './logger.js';
6+
7+
interface FeatureFlags {
8+
version?: string;
9+
flags?: Record<string, any>;
10+
}
11+
12+
class FeatureFlagManager {
13+
private flags: Record<string, any> = {};
14+
private lastFetch: number = 0;
15+
private cachePath: string;
16+
private cacheMaxAge: number = 30 * 60 * 1000; // 5 minutes - hardcoded refresh interval
17+
private flagUrl: string;
18+
private refreshInterval: NodeJS.Timeout | null = null;
19+
20+
constructor() {
21+
const configDir = path.dirname(CONFIG_FILE);
22+
this.cachePath = path.join(configDir, 'feature-flags.json');
23+
24+
// Use production flags
25+
this.flagUrl = process.env.DC_FLAG_URL ||
26+
'https://desktopcommander.app/flags/v1/production.json';
27+
}
28+
29+
/**
30+
* Initialize - load from cache and start background refresh
31+
*/
32+
async initialize(): Promise<void> {
33+
try {
34+
// Load from cache immediately (non-blocking)
35+
await this.loadFromCache();
36+
37+
// Fetch in background (don't block startup)
38+
this.fetchFlags().catch(err => {
39+
logger.debug('Initial flag fetch failed:', err.message);
40+
});
41+
42+
// Start periodic refresh every 5 minutes
43+
this.refreshInterval = setInterval(() => {
44+
this.fetchFlags().catch(err => {
45+
logger.debug('Periodic flag fetch failed:', err.message);
46+
});
47+
}, this.cacheMaxAge);
48+
49+
logger.info(`Feature flags initialized (refresh every ${this.cacheMaxAge / 1000}s)`);
50+
} catch (error) {
51+
logger.warning('Failed to initialize feature flags:', error);
52+
}
53+
}
54+
55+
/**
56+
* Get a flag value
57+
*/
58+
get(flagName: string, defaultValue: any = false): any {
59+
return this.flags[flagName] !== undefined ? this.flags[flagName] : defaultValue;
60+
}
61+
62+
/**
63+
* Get all flags for debugging
64+
*/
65+
getAll(): Record<string, any> {
66+
return { ...this.flags };
67+
}
68+
69+
/**
70+
* Manually refresh flags immediately (for testing)
71+
*/
72+
async refresh(): Promise<boolean> {
73+
try {
74+
await this.fetchFlags();
75+
return true;
76+
} catch (error) {
77+
logger.error('Manual refresh failed:', error);
78+
return false;
79+
}
80+
}
81+
82+
/**
83+
* Load flags from local cache
84+
*/
85+
private async loadFromCache(): Promise<void> {
86+
try {
87+
if (!existsSync(this.cachePath)) {
88+
logger.debug('No feature flag cache found');
89+
return;
90+
}
91+
92+
const data = await fs.readFile(this.cachePath, 'utf8');
93+
const config: FeatureFlags = JSON.parse(data);
94+
95+
if (config.flags) {
96+
this.flags = config.flags;
97+
this.lastFetch = Date.now();
98+
logger.debug(`Loaded ${Object.keys(this.flags).length} feature flags from cache`);
99+
}
100+
} catch (error) {
101+
logger.warning('Failed to load feature flags from cache:', error);
102+
}
103+
}
104+
105+
/**
106+
* Fetch flags from remote URL
107+
*/
108+
private async fetchFlags(): Promise<void> {
109+
try {
110+
logger.debug('Fetching feature flags from:', this.flagUrl);
111+
112+
const controller = new AbortController();
113+
const timeout = setTimeout(() => controller.abort(), 5000);
114+
115+
const response = await fetch(this.flagUrl, {
116+
signal: controller.signal,
117+
headers: {
118+
'Cache-Control': 'no-cache',
119+
}
120+
});
121+
122+
clearTimeout(timeout);
123+
124+
if (!response.ok) {
125+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
126+
}
127+
128+
const config: FeatureFlags = await response.json();
129+
130+
// Update flags
131+
if (config.flags) {
132+
this.flags = config.flags;
133+
this.lastFetch = Date.now();
134+
135+
// Save to cache
136+
await this.saveToCache(config);
137+
138+
logger.info(`Feature flags updated: ${Object.keys(this.flags).length} flags`);
139+
}
140+
} catch (error: any) {
141+
logger.debug('Failed to fetch feature flags:', error.message);
142+
// Continue with cached values
143+
}
144+
}
145+
146+
/**
147+
* Save flags to local cache
148+
*/
149+
private async saveToCache(config: FeatureFlags): Promise<void> {
150+
try {
151+
const configDir = path.dirname(this.cachePath);
152+
if (!existsSync(configDir)) {
153+
await fs.mkdir(configDir, { recursive: true });
154+
}
155+
156+
await fs.writeFile(this.cachePath, JSON.stringify(config, null, 2), 'utf8');
157+
logger.debug('Saved feature flags to cache');
158+
} catch (error) {
159+
logger.warning('Failed to save feature flags to cache:', error);
160+
}
161+
}
162+
163+
/**
164+
* Cleanup on shutdown
165+
*/
166+
destroy(): void {
167+
if (this.refreshInterval) {
168+
clearInterval(this.refreshInterval);
169+
this.refreshInterval = null;
170+
}
171+
}
172+
}
173+
174+
// Export singleton instance
175+
export const featureFlagManager = new FeatureFlagManager();

0 commit comments

Comments
 (0)