@wonderwhy-er/desktop-commander 0.2.25 → 0.2.27
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-manager.d.ts +4 -0
- package/dist/config-manager.js +12 -0
- package/dist/utils/ab-test.js +1 -1
- package/dist/utils/capture.js +1 -19
- package/dist/utils/feature-flags.d.ts +13 -0
- package/dist/utils/feature-flags.js +37 -1
- package/dist/utils/welcome-onboarding.js +18 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/config-manager.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ declare class ConfigManager {
|
|
|
62
62
|
* Check if this is the first run (config file was just created)
|
|
63
63
|
*/
|
|
64
64
|
isFirstRun(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Get or create a persistent client ID for analytics and A/B tests
|
|
67
|
+
*/
|
|
68
|
+
getOrCreateClientId(): Promise<string>;
|
|
65
69
|
}
|
|
66
70
|
export declare const configManager: ConfigManager;
|
|
67
71
|
export {};
|
package/dist/config-manager.js
CHANGED
|
@@ -193,6 +193,18 @@ class ConfigManager {
|
|
|
193
193
|
isFirstRun() {
|
|
194
194
|
return this._isFirstRun;
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Get or create a persistent client ID for analytics and A/B tests
|
|
198
|
+
*/
|
|
199
|
+
async getOrCreateClientId() {
|
|
200
|
+
let clientId = await this.getValue('clientId');
|
|
201
|
+
if (!clientId) {
|
|
202
|
+
const { randomUUID } = await import('crypto');
|
|
203
|
+
clientId = randomUUID();
|
|
204
|
+
await this.setValue('clientId', clientId);
|
|
205
|
+
}
|
|
206
|
+
return clientId;
|
|
207
|
+
}
|
|
196
208
|
}
|
|
197
209
|
// Export singleton instance
|
|
198
210
|
export const configManager = new ConfigManager();
|
package/dist/utils/ab-test.js
CHANGED
|
@@ -28,7 +28,7 @@ async function getVariant(experimentName) {
|
|
|
28
28
|
return existing;
|
|
29
29
|
}
|
|
30
30
|
// New assignment based on clientId
|
|
31
|
-
const clientId = await configManager.
|
|
31
|
+
const clientId = await configManager.getOrCreateClientId();
|
|
32
32
|
const hash = hashCode(clientId + experimentName);
|
|
33
33
|
const variantIndex = hash % experiment.variants.length;
|
|
34
34
|
const variant = experiment.variants[variantIndex];
|
package/dist/utils/capture.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { platform } from 'os';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
2
|
import * as https from 'https';
|
|
4
3
|
import { configManager } from '../config-manager.js';
|
|
5
4
|
import { currentClient } from '../server.js';
|
|
@@ -13,23 +12,6 @@ catch {
|
|
|
13
12
|
}
|
|
14
13
|
// Will be initialized when needed
|
|
15
14
|
let uniqueUserId = 'unknown';
|
|
16
|
-
// Function to get or create a persistent UUID
|
|
17
|
-
async function getOrCreateUUID() {
|
|
18
|
-
try {
|
|
19
|
-
// Try to get the UUID from the config
|
|
20
|
-
let clientId = await configManager.getValue('clientId');
|
|
21
|
-
// If it doesn't exist, create a new one and save it
|
|
22
|
-
if (!clientId) {
|
|
23
|
-
clientId = randomUUID();
|
|
24
|
-
await configManager.setValue('clientId', clientId);
|
|
25
|
-
}
|
|
26
|
-
return clientId;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
// Fallback to a random UUID if config operations fail
|
|
30
|
-
return randomUUID();
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
15
|
/**
|
|
34
16
|
* Sanitizes error objects to remove potentially sensitive information like file paths
|
|
35
17
|
* @param error Error object or string to sanitize
|
|
@@ -76,7 +58,7 @@ export const captureBase = async (captureURL, event, properties) => {
|
|
|
76
58
|
}
|
|
77
59
|
// Get or create the client ID if not already initialized
|
|
78
60
|
if (uniqueUserId === 'unknown') {
|
|
79
|
-
uniqueUserId = await
|
|
61
|
+
uniqueUserId = await configManager.getOrCreateClientId();
|
|
80
62
|
}
|
|
81
63
|
// Get current client information for all events
|
|
82
64
|
let clientContext = {};
|
|
@@ -5,6 +5,9 @@ declare class FeatureFlagManager {
|
|
|
5
5
|
private cacheMaxAge;
|
|
6
6
|
private flagUrl;
|
|
7
7
|
private refreshInterval;
|
|
8
|
+
private freshFetchPromise;
|
|
9
|
+
private resolveFreshFetch;
|
|
10
|
+
private loadedFromCache;
|
|
8
11
|
constructor();
|
|
9
12
|
/**
|
|
10
13
|
* Initialize - load from cache and start background refresh
|
|
@@ -22,6 +25,16 @@ declare class FeatureFlagManager {
|
|
|
22
25
|
* Manually refresh flags immediately (for testing)
|
|
23
26
|
*/
|
|
24
27
|
refresh(): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Check if flags were loaded from cache (vs fresh fetch)
|
|
30
|
+
*/
|
|
31
|
+
wasLoadedFromCache(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Wait for fresh flags to be fetched from network.
|
|
34
|
+
* Use this when you need to ensure flags are loaded before making decisions
|
|
35
|
+
* (e.g., A/B test assignments for new users who don't have a cache yet)
|
|
36
|
+
*/
|
|
37
|
+
waitForFreshFlags(): Promise<void>;
|
|
25
38
|
/**
|
|
26
39
|
* Load flags from local cache
|
|
27
40
|
*/
|
|
@@ -9,11 +9,19 @@ class FeatureFlagManager {
|
|
|
9
9
|
this.lastFetch = 0;
|
|
10
10
|
this.cacheMaxAge = 30 * 60 * 1000;
|
|
11
11
|
this.refreshInterval = null;
|
|
12
|
+
// Track fresh fetch status for A/B tests that need network flags
|
|
13
|
+
this.freshFetchPromise = null;
|
|
14
|
+
this.resolveFreshFetch = null;
|
|
15
|
+
this.loadedFromCache = false;
|
|
12
16
|
const configDir = path.dirname(CONFIG_FILE);
|
|
13
17
|
this.cachePath = path.join(configDir, 'feature-flags.json');
|
|
14
18
|
// Use production flags
|
|
15
19
|
this.flagUrl = process.env.DC_FLAG_URL ||
|
|
16
20
|
'https://desktopcommander.app/flags/v1/production.json';
|
|
21
|
+
// Set up promise for waiting on fresh fetch
|
|
22
|
+
this.freshFetchPromise = new Promise((resolve) => {
|
|
23
|
+
this.resolveFreshFetch = resolve;
|
|
24
|
+
});
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Initialize - load from cache and start background refresh
|
|
@@ -23,8 +31,17 @@ class FeatureFlagManager {
|
|
|
23
31
|
// Load from cache immediately (non-blocking)
|
|
24
32
|
await this.loadFromCache();
|
|
25
33
|
// Fetch in background (don't block startup)
|
|
26
|
-
this.fetchFlags().
|
|
34
|
+
this.fetchFlags().then(() => {
|
|
35
|
+
// Signal that fresh flags are now available
|
|
36
|
+
if (this.resolveFreshFetch) {
|
|
37
|
+
this.resolveFreshFetch();
|
|
38
|
+
}
|
|
39
|
+
}).catch(err => {
|
|
27
40
|
logger.debug('Initial flag fetch failed:', err.message);
|
|
41
|
+
// Still resolve the promise so waiters don't hang forever
|
|
42
|
+
if (this.resolveFreshFetch) {
|
|
43
|
+
this.resolveFreshFetch();
|
|
44
|
+
}
|
|
28
45
|
});
|
|
29
46
|
// Start periodic refresh every 5 minutes
|
|
30
47
|
this.refreshInterval = setInterval(() => {
|
|
@@ -66,6 +83,22 @@ class FeatureFlagManager {
|
|
|
66
83
|
return false;
|
|
67
84
|
}
|
|
68
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if flags were loaded from cache (vs fresh fetch)
|
|
88
|
+
*/
|
|
89
|
+
wasLoadedFromCache() {
|
|
90
|
+
return this.loadedFromCache;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Wait for fresh flags to be fetched from network.
|
|
94
|
+
* Use this when you need to ensure flags are loaded before making decisions
|
|
95
|
+
* (e.g., A/B test assignments for new users who don't have a cache yet)
|
|
96
|
+
*/
|
|
97
|
+
async waitForFreshFlags() {
|
|
98
|
+
if (this.freshFetchPromise) {
|
|
99
|
+
await this.freshFetchPromise;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
69
102
|
/**
|
|
70
103
|
* Load flags from local cache
|
|
71
104
|
*/
|
|
@@ -73,6 +106,7 @@ class FeatureFlagManager {
|
|
|
73
106
|
try {
|
|
74
107
|
if (!existsSync(this.cachePath)) {
|
|
75
108
|
logger.debug('No feature flag cache found');
|
|
109
|
+
this.loadedFromCache = false;
|
|
76
110
|
return;
|
|
77
111
|
}
|
|
78
112
|
const data = await fs.readFile(this.cachePath, 'utf8');
|
|
@@ -80,11 +114,13 @@ class FeatureFlagManager {
|
|
|
80
114
|
if (config.flags) {
|
|
81
115
|
this.flags = config.flags;
|
|
82
116
|
this.lastFetch = Date.now();
|
|
117
|
+
this.loadedFromCache = true;
|
|
83
118
|
logger.debug(`Loaded ${Object.keys(this.flags).length} feature flags from cache`);
|
|
84
119
|
}
|
|
85
120
|
}
|
|
86
121
|
catch (error) {
|
|
87
122
|
logger.warning('Failed to load feature flags from cache:', error);
|
|
123
|
+
this.loadedFromCache = false;
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
126
|
/**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { configManager } from '../config-manager.js';
|
|
2
2
|
import { hasFeature } from './ab-test.js';
|
|
3
|
+
import { featureFlagManager } from './feature-flags.js';
|
|
3
4
|
import { openWelcomePage } from './open-browser.js';
|
|
4
5
|
import { logToStderr } from './logger.js';
|
|
6
|
+
import { capture } from './capture.js';
|
|
5
7
|
/**
|
|
6
8
|
* Handle welcome page display for new users (A/B test controlled)
|
|
7
9
|
*
|
|
@@ -15,8 +17,22 @@ export async function handleWelcomePageOnboarding() {
|
|
|
15
17
|
if (!configManager.isFirstRun()) {
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
20
|
+
// Track that we have a first-run user attempting onboarding
|
|
21
|
+
const loadedFromCache = featureFlagManager.wasLoadedFromCache();
|
|
22
|
+
// For new users, we need to wait for feature flags to load from network
|
|
23
|
+
// since they won't have a cache file yet. Without this, hasFeature() would
|
|
24
|
+
// return false (no experiments defined) and all new users go to control.
|
|
25
|
+
if (!loadedFromCache) {
|
|
26
|
+
logToStderr('debug', 'Waiting for feature flags to load...');
|
|
27
|
+
await featureFlagManager.waitForFreshFlags();
|
|
28
|
+
}
|
|
18
29
|
// Check A/B test assignment
|
|
19
30
|
const shouldShow = await hasFeature('showOnboardingPage');
|
|
31
|
+
// Track the A/B decision
|
|
32
|
+
capture('server_welcome_page_ab_decision', {
|
|
33
|
+
variant: shouldShow ? 'treatment' : 'control',
|
|
34
|
+
loaded_from_cache: loadedFromCache
|
|
35
|
+
});
|
|
20
36
|
if (!shouldShow) {
|
|
21
37
|
logToStderr('debug', 'Welcome page skipped (A/B: noOnboardingPage)');
|
|
22
38
|
return;
|
|
@@ -29,9 +45,11 @@ export async function handleWelcomePageOnboarding() {
|
|
|
29
45
|
try {
|
|
30
46
|
await openWelcomePage();
|
|
31
47
|
await configManager.setValue('sawOnboardingPage', true);
|
|
48
|
+
capture('server_welcome_page_opened', { success: true });
|
|
32
49
|
logToStderr('info', 'Welcome page opened');
|
|
33
50
|
}
|
|
34
51
|
catch (e) {
|
|
52
|
+
capture('server_welcome_page_opened', { success: false, error: e instanceof Error ? e.message : String(e) });
|
|
35
53
|
logToStderr('warning', `Failed to open welcome page: ${e instanceof Error ? e.message : e}`);
|
|
36
54
|
}
|
|
37
55
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.27";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.27';
|
package/package.json
CHANGED