Skip to content

Commit b3f4b82

Browse files
authored
Merge pull request continuedev#7591 from continuedev/nate/cli-headless-flag
note whether headless mode / github actions in telemetry
2 parents 1c03de5 + 63a38c8 commit b3f4b82

File tree

5 files changed

+260
-24
lines changed

5 files changed

+260
-24
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { EventEmitter } from "events";
2+
import { vi } from "vitest";
3+
4+
/**
5+
* Mocked base service class for testing
6+
*/
7+
export class BaseService<TState> extends EventEmitter {
8+
protected serviceName: string;
9+
protected currentState: TState;
10+
private isInitialized: boolean = false;
11+
12+
constructor(serviceName: string, initialState: TState) {
13+
super();
14+
this.serviceName = serviceName;
15+
this.currentState = initialState;
16+
}
17+
18+
// Mock the abstract method
19+
doInitialize = vi.fn<any>();
20+
21+
async initialize(...args: any[]): Promise<TState> {
22+
this.emit("initializing");
23+
try {
24+
const state = await this.doInitialize(...args);
25+
this.currentState = state;
26+
this.isInitialized = true;
27+
this.emit("initialized", state);
28+
return state;
29+
} catch (error) {
30+
this.emit("error", error);
31+
throw error;
32+
}
33+
}
34+
35+
getState(): TState {
36+
return { ...this.currentState };
37+
}
38+
39+
protected setState(newState: Partial<TState>): void {
40+
const previousState = this.currentState;
41+
this.currentState = { ...this.currentState, ...newState };
42+
this.emit("stateChanged", this.currentState, previousState);
43+
}
44+
45+
isReady(): boolean {
46+
return this.isInitialized;
47+
}
48+
49+
async reload(...args: any[]): Promise<TState> {
50+
this.isInitialized = false;
51+
return this.initialize(...args);
52+
}
53+
54+
async cleanup(): Promise<void> {
55+
this.removeAllListeners();
56+
this.isInitialized = false;
57+
}
58+
}
59+
60+
/**
61+
* Interface for services with dependencies
62+
*/
63+
export interface ServiceWithDependencies {
64+
getDependencies(): string[];
65+
}
66+
67+
/**
68+
* Helper to check if a service has dependencies
69+
*/
70+
export function hasDependencies(
71+
service: any,
72+
): service is ServiceWithDependencies {
73+
return !!service && typeof service.getDependencies === "function";
74+
}

extensions/cli/src/services/BaseService.test.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,118 @@
1-
import { describe, expect, test, beforeEach, vi } from "vitest";
1+
import { EventEmitter } from "events";
22

3-
import {
4-
BaseService,
5-
hasDependencies,
6-
ServiceWithDependencies,
7-
} from "./BaseService.js";
3+
import { beforeEach, describe, expect, test, vi } from "vitest";
4+
5+
import { logger } from "../util/logger.js";
6+
7+
/**
8+
* Base abstract class for all services
9+
* Provides common lifecycle methods and state management
10+
*/
11+
abstract class BaseService<TState> extends EventEmitter {
12+
protected serviceName: string;
13+
protected currentState: TState;
14+
private isInitialized: boolean = false;
15+
16+
constructor(serviceName: string, initialState: TState) {
17+
super();
18+
this.serviceName = serviceName;
19+
this.currentState = initialState;
20+
}
21+
22+
/**
23+
* Initialize the service (must be implemented by subclasses)
24+
* This is automatically wrapped with initialization logic
25+
*/
26+
abstract doInitialize(...args: any[]): Promise<TState>;
27+
28+
/**
29+
* Public initialization method that wraps the implementation
30+
*/
31+
async initialize(...args: any[]): Promise<TState> {
32+
logger.debug(`Initializing ${this.serviceName}`, {
33+
wasInitialized: this.isInitialized,
34+
hadPreviousState: !!this.currentState,
35+
});
36+
this.emit("initializing");
37+
38+
try {
39+
const state = await this.doInitialize(...args);
40+
this.currentState = state;
41+
this.isInitialized = true;
42+
logger.debug(`${this.serviceName} initialized successfully`, {
43+
stateKeys: state ? Object.keys(state as any) : [],
44+
isNowInitialized: this.isInitialized,
45+
});
46+
this.emit("initialized", state);
47+
return state;
48+
} catch (error: any) {
49+
logger.debug(`Failed to initialize ${this.serviceName}:`, error);
50+
if (this.listenerCount("error") > 0) {
51+
this.emit("error", error);
52+
}
53+
throw error;
54+
}
55+
}
56+
57+
/**
58+
* Get current service state (shallow copy for immutability)
59+
*/
60+
getState(): TState {
61+
return { ...this.currentState };
62+
}
63+
64+
/**
65+
* Update service state and emit change event
66+
*/
67+
protected setState(newState: Partial<TState>): void {
68+
const previousState = this.currentState;
69+
this.currentState = { ...this.currentState, ...newState };
70+
// Only log state updates if not in production to avoid circular reference issues
71+
if (process.env.NODE_ENV !== "production") {
72+
logger.debug(`${this.serviceName} state updated`);
73+
}
74+
this.emit("stateChanged", this.currentState, previousState);
75+
}
76+
77+
/**
78+
* Check if service is ready for use
79+
*/
80+
isReady(): boolean {
81+
return this.isInitialized;
82+
}
83+
84+
/**
85+
* Reload/refresh the service
86+
*/
87+
async reload(...args: any[]): Promise<TState> {
88+
logger.debug(`Reloading ${this.serviceName}`);
89+
this.isInitialized = false;
90+
return this.initialize(...args);
91+
}
92+
93+
/**
94+
* Cleanup resources (can be overridden by subclasses)
95+
*/
96+
async cleanup(): Promise<void> {
97+
logger.debug(`Cleaning up ${this.serviceName}`);
98+
this.removeAllListeners();
99+
this.isInitialized = false;
100+
}
101+
}
102+
103+
/**
104+
* Interface for services with dependencies
105+
*/
106+
interface ServiceWithDependencies {
107+
getDependencies(): string[];
108+
}
109+
110+
/**
111+
* Helper to check if a service has dependencies
112+
*/
113+
function hasDependencies(service: any): service is ServiceWithDependencies {
114+
return !!service && typeof service.getDependencies === "function";
115+
}
8116

9117
// Test implementation of BaseService
10118
interface TestState {

extensions/cli/src/telemetry/posthogService.errorHandling.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("PosthogService Error Handling", () => {
3939
properties: expect.objectContaining({
4040
test: "data",
4141
os: expect.any(String),
42-
extensionVersion: "",
42+
extensionVersion: expect.any(String),
4343
ideName: "cn",
4444
ideType: "cli",
4545
}),

extensions/cli/src/telemetry/posthogService.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import node_machine_id from "node-machine-id";
55
import type { PostHog as PostHogType } from "posthog-node";
66

77
import { isAuthenticatedConfig, loadAuthConfig } from "../auth/workos.js";
8+
import { isGitHubActions } from "../util/git.js";
89
import { logger } from "../util/logger.js";
10+
import { getVersion } from "../version.js";
911

1012
export class PosthogService {
1113
private os: string | undefined;
@@ -36,6 +38,14 @@ export class PosthogService {
3638
return this._hasInternetConnection;
3739
}
3840

41+
/**
42+
* Check if running in headless mode (-p/--print flags)
43+
*/
44+
private isHeadlessMode(): boolean {
45+
const args = process.argv.slice(2);
46+
return args.includes("-p") || args.includes("--print");
47+
}
48+
3949
get isEnabled() {
4050
return process.env.CONTINUE_CLI_ENABLE_TELEMETRY !== "0";
4151
}
@@ -87,9 +97,11 @@ export class PosthogService {
8797
const augmentedProperties = {
8898
...properties,
8999
os: this.os,
90-
extensionVersion: "", // TODO cn version
100+
extensionVersion: getVersion(),
91101
ideName: "cn",
92102
ideType: "cli",
103+
isHeadless: this.isHeadlessMode(),
104+
isGitHubCI: isGitHubActions(),
93105
};
94106
const payload = {
95107
distinctId: this.uniqueId,

extensions/cli/src/version.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { readFileSync } from "fs";
22
import { dirname, join } from "path";
33
import { fileURLToPath } from "url";
44

5+
import node_machine_id from "node-machine-id";
6+
7+
import { isAuthenticatedConfig, loadAuthConfig } from "./auth/workos.js";
58
import { logger } from "./util/logger.js";
69

710
export function getVersion(): string {
@@ -17,29 +20,68 @@ export function getVersion(): string {
1720
}
1821
}
1922

23+
function getEventUserId(): string {
24+
const authConfig = loadAuthConfig();
25+
26+
if (isAuthenticatedConfig(authConfig)) {
27+
return authConfig.userId;
28+
}
29+
30+
// Fall back to unique machine id if not signed in
31+
return node_machine_id.machineIdSync();
32+
}
33+
34+
// Singleton to cache the latest version result
35+
let latestVersionCache: Promise<string | null> | null = null;
36+
2037
export async function getLatestVersion(
2138
signal?: AbortSignal,
2239
): Promise<string | null> {
23-
try {
24-
const response = await fetch(
25-
"https://registry.npmjs.org/@continuedev/cli/latest",
26-
{ signal },
27-
);
28-
if (!response.ok) {
29-
throw new Error(`HTTP error! status: ${response.status}`);
30-
}
31-
const data = await response.json();
32-
return data.version;
33-
} catch (error) {
34-
if (error instanceof Error && error.name === "AbortError") {
35-
// Request was aborted, don't log
40+
// Return cached promise if it exists
41+
if (latestVersionCache) {
42+
return latestVersionCache;
43+
}
44+
45+
// Create and cache the promise
46+
latestVersionCache = (async () => {
47+
try {
48+
const id = getEventUserId();
49+
const response = await fetch(
50+
`https://api.continue.dev/cn/info?id=${encodeURIComponent(id)}`,
51+
{ signal },
52+
);
53+
if (!response.ok) {
54+
throw new Error(`HTTP error! status: ${response.status}`);
55+
}
56+
const data = await response.json();
57+
return data.version;
58+
} catch (error) {
59+
if (error instanceof Error && error.name === "AbortError") {
60+
// Request was aborted, don't log
61+
return null;
62+
}
63+
logger?.debug(
64+
"Warning: Could not fetch latest version from api.continue.dev",
65+
);
3666
return null;
3767
}
38-
logger.debug("Warning: Could not fetch latest version from npm registry");
39-
return null;
40-
}
68+
})();
69+
70+
return latestVersionCache;
4171
}
4272

73+
getLatestVersion()
74+
.then((version) => {
75+
if (version) {
76+
logger?.info(`Latest version: ${version}`);
77+
}
78+
})
79+
.catch((error) => {
80+
logger?.debug(
81+
`Warning: Could not fetch latest version from api.continue.dev: ${error}`,
82+
);
83+
});
84+
4385
export function compareVersions(
4486
current: string,
4587
latest: string,

0 commit comments

Comments
 (0)