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
31 changes: 6 additions & 25 deletions core/util/GlobalContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ export type GlobalContextType = {
isSupportedLanceDbCpuTargetForLinux: boolean;
sharedConfig: SharedConfigSchema;
failedDocs: SiteIndexingConfig[];
shownDeprecatedProviderWarnings: {
[providerTitle: string]: boolean;
};
shownDeprecatedProviderWarnings: { [providerTitle: string]: boolean };
autoUpdateCli: boolean;
mcpOauthStorage: {
[serverUrl: string]: {
clientInformation?: OAuthClientInformationFull;
Expand All @@ -66,16 +65,7 @@ export class GlobalContext {
) {
const filepath = getGlobalContextFilePath();
if (!fs.existsSync(filepath)) {
fs.writeFileSync(
filepath,
JSON.stringify(
{
[key]: value,
},
null,
2,
),
);
fs.writeFileSync(filepath, JSON.stringify({ [key]: value }, null, 2));
} else {
const data = fs.readFileSync(filepath, "utf-8");

Expand Down Expand Up @@ -113,10 +103,7 @@ export class GlobalContext {
}

// Recreate the file with salvaged values plus the new value
const newData = {
...salvaged,
[key]: value,
};
const newData = { ...salvaged, [key]: value };

fs.writeFileSync(filepath, JSON.stringify(newData, null, 2));
return;
Expand Down Expand Up @@ -174,10 +161,7 @@ export class GlobalContext {
newValues: Partial<SharedConfigSchema>,
): SharedConfigSchema {
const currentSharedConfig = this.getSharedConfig();
const updatedSharedConfig = {
...currentSharedConfig,
...newValues,
};
const updatedSharedConfig = { ...currentSharedConfig, ...newValues };
this.update("sharedConfig", updatedSharedConfig);
return updatedSharedConfig;
}
Expand All @@ -189,10 +173,7 @@ export class GlobalContext {
): GlobalContextModelSelections {
const currentSelections = this.get("selectedModelsByProfileId") ?? {};
const forProfile = currentSelections[profileId] ?? {};
const newSelections = {
...forProfile,
[role]: title,
};
const newSelections = { ...forProfile, [role]: title };

this.update("selectedModelsByProfileId", {
...currentSelections,
Expand Down
5 changes: 5 additions & 0 deletions extensions/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export const SYSTEM_SLASH_COMMANDS: SystemCommand[] = [
description: "Sign out of your current session",
category: "system",
},
{
name: "update",
description: "Update the Continue CLI",
category: "system",
},
{
name: "whoami",
description: "Check who you're currently logged in as",
Expand Down
241 changes: 241 additions & 0 deletions extensions/cli/src/services/UpdateService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { exec, spawn } from "child_process";
import { promisify } from "util";

import { GlobalContext } from "core/util/GlobalContext.js";

import { logger } from "src/util/logger.js";

import { compareVersions, getLatestVersion, getVersion } from "../version.js";

import { BaseService } from "./BaseService.js";
import { serviceContainer } from "./ServiceContainer.js";
import { UpdateServiceState, UpdateStatus } from "./types.js";
const execAsync = promisify(exec);

/**
* Service for checking and performing CLI updates
*/
export class UpdateService extends BaseService<UpdateServiceState> {
constructor() {
super("update", {
autoUpdate: true,
isAutoUpdate: true,
status: UpdateStatus.IDLE,
message: "",
error: null,
isUpdateAvailable: false,
latestVersion: null,
currentVersion: getVersion(),
});
}

/**
* Initialize the update service
*/
async doInitialize(headless?: boolean) {
// Don't automatically check in tests/headless
if (!headless && process.env.NODE_ENV !== "test") {
void this.checkAndAutoUpdate();
}

return this.currentState;
}

private async checkAndAutoUpdate() {
// First get auto update setting from global context
const globalContext = new GlobalContext();
const autoUpdate = globalContext.get("autoUpdateCli") ?? true;
this.setState({
autoUpdate,
});

try {
// Check for updates
this.setState({
status: UpdateStatus.CHECKING,
message: "Checking for updates",
});

const latestVersion = await getLatestVersion();
this.setState({
latestVersion,
});

if (!latestVersion) {
this.setState({
status: UpdateStatus.IDLE,
message: "Continue CLI",
isUpdateAvailable: false,
});
return;
}

const comparison = compareVersions(
this.currentState.currentVersion,
latestVersion,
);
const isUpdateAvailable = comparison === "older";
this.setState({
isUpdateAvailable,
});

if (this.currentState.currentVersion === "0.0.0-dev") {
this.setState({
status: UpdateStatus.IDLE,
message: `Continue CLI`,
isUpdateAvailable,
latestVersion,
});
return; // Uncomment to test auto-update behavior in dev
}

// If update is available, automatically update
if (
autoUpdate &&
isUpdateAvailable &&
this.currentState.status !== "updating" &&
!process.env.CONTINUE_CLI_AUTO_UPDATED //Already auto updated, preventing sequential auto-update
) {
await this.performUpdate(true);
} else {
this.setState({
status: UpdateStatus.IDLE,
message: isUpdateAvailable
? `Update available: v${latestVersion}`
: `Continue CLI v${this.currentState.currentVersion}`,
isUpdateAvailable,
latestVersion,
});
}
} catch (error: any) {
logger.error("Error checking for updates:", error);
this.setState({
status: UpdateStatus.ERROR,
message: `Continue CLI v${this.currentState.currentVersion}`,
error,
});
}
}

public async setAutoUpdate(value: boolean) {
const globalContext = new GlobalContext();
globalContext.update("autoUpdateCli", value);
this.setState({
autoUpdate: value,
});
}

// TODO this is a hack because our service state update code is broken
// Currently all things that need update use serviceContainer.set manually
// Rather than actually using the stateChanged event
setState(newState: Partial<UpdateServiceState>): void {
super.setState(newState);
serviceContainer.set("update", this.currentState);
}

async performUpdate(isAutoUpdate?: boolean) {
if (this.currentState.status === "updating") {
return;
}

try {
this.setState({
isAutoUpdate,
status: UpdateStatus.UPDATING,
message: `${isAutoUpdate ? "Auto-updating" : "Updating"} to v${this.currentState.latestVersion}`,
});

// Install the update
const { stdout, stderr } = await execAsync("npm i -g @continuedev/cli");
logger.debug("Update output:", { stdout, stderr });

if (stderr) {
const errLines = stderr.split("\n");
for (const line of errLines) {
const lower = line.toLowerCase().trim();
if (
!line ||
lower.includes("debugger") ||
lower.includes("npm warn")
) {
continue;
}
this.setState({
status: UpdateStatus.ERROR,
message: `Error updating to v${this.currentState.latestVersion}`,
error: new Error(stderr),
});
return;
}
}

this.setState({
status: UpdateStatus.UPDATED,
message: `${isAutoUpdate ? "Auto-updated to" : "Restart for"} v${this.currentState.latestVersion}`,
isUpdateAvailable: false,
});
if (isAutoUpdate) {
this.restartCLI();
}
} catch (error: any) {
logger.error("Error updating CLI:", error);
this.setState({
status: UpdateStatus.ERROR,
message: isAutoUpdate ? "Auto-update failed" : "Update failed",
error,
});
setTimeout(() => {
this.setState({
status: UpdateStatus.IDLE,
message: `/update to v${this.currentState.latestVersion}`,
});
}, 4000);
}
}

private restartCLI(): void {
try {
const entryPoint = process.argv[1];
const cliArgs = process.argv.slice(2);
const nodeExecutable = process.execPath;

logger.debug(
`Preparing for CLI restart with: ${nodeExecutable} ${entryPoint} ${cliArgs.join(
" ",
)}`,
);

// Halt/clean up parent cn process
try {
// Remove all input listeners
global.clearTimeout = () => {};
global.clearInterval = () => {};
process.stdin.removeAllListeners();
process.stdin.pause();
// console.clear(); // Don't want to clear things that were in console before cn started
} catch (e) {
logger.debug("Error cleaning up terminal:", e);
}

// Spawn a new detached cn process
const child = spawn(nodeExecutable, [entryPoint, ...cliArgs], {
detached: true,
stdio: "inherit",
env: {
...process.env,
CONTINUE_CLI_AUTO_UPDATED: "true",
},
});

// I did not find a way on existing to avoid a bug where next process has input glitches without leaving parent in place
// So instead of existing, parent will exit when child exits
// process.exit(0);
child.on("exit", (code) => {
process.exit(code);
});
child.unref();
} catch (error) {
logger.error("Failed to restart CLI:", error);
}
}
}
23 changes: 12 additions & 11 deletions extensions/cli/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ServiceInitOptions,
ServiceInitResult,
} from "./types.js";
import { UpdateService } from "./UpdateService.js";

// Service instances
const authService = new AuthService();
Expand All @@ -31,6 +32,7 @@ const mcpService = new MCPService();
const fileIndexService = new FileIndexService();
const resourceMonitoringService = new ResourceMonitoringService();
const chatHistoryService = new ChatHistoryService();
const updateService = new UpdateService();

/**
* Initialize all services and register them with the service container
Expand Down Expand Up @@ -132,6 +134,12 @@ export async function initializeServices(
[], // No dependencies
);

serviceContainer.register(
SERVICE_NAMES.UPDATE,
() => updateService.initialize(),
[], // No dependencies
);

serviceContainer.register(
SERVICE_NAMES.API_CLIENT,
async () => {
Expand Down Expand Up @@ -283,17 +291,9 @@ export function reloadService(serviceName: string) {
* Check if all core services are ready
*/
export function areServicesReady(): boolean {
return [
SERVICE_NAMES.TOOL_PERMISSIONS,
SERVICE_NAMES.AUTH,
SERVICE_NAMES.API_CLIENT,
SERVICE_NAMES.CONFIG,
SERVICE_NAMES.MODEL,
SERVICE_NAMES.MCP,
SERVICE_NAMES.FILE_INDEX,
SERVICE_NAMES.RESOURCE_MONITORING,
SERVICE_NAMES.CHAT_HISTORY,
].every((name) => serviceContainer.isReady(name));
return Object.values(SERVICE_NAMES).every((name) =>
serviceContainer.isReady(name),
);
}

/**
Expand All @@ -317,6 +317,7 @@ export const services = {
resourceMonitoring: resourceMonitoringService,
systemMessage: systemMessageService,
chatHistory: chatHistoryService,
updateService: updateService,
} as const;

// Export the service container for advanced usage
Expand Down
Loading
Loading