Files
codetyper.cli/src/services/upgrade.ts
Carlos Gutierrez 0062e5d9d9 Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features:
  - Interactive TUI with React/Ink
  - Autonomous agent with tool calls (bash, read, write, edit, glob, grep)
  - Permission system with pattern-based rules
  - Session management with auto-compaction
  - Dual providers: GitHub Copilot and Ollama
  - MCP server integration
  - Todo panel and theme system
  - Streaming responses
  - GitHub-compatible project context
2026-01-27 23:33:06 -05:00

324 lines
8.2 KiB
TypeScript

/**
* Upgrade Service
*
* Handles self-update functionality for the CLI
*/
import { exec } from "child_process";
import { promisify } from "util";
import chalk from "chalk";
import appVersion from "@/version.json";
const execAsync = promisify(exec);
const REPO_URL = "git@github.com:CarGDev/codetyper.cli.git";
const REPO_API_URL = "https://api.github.com/repos/CarGDev/codetyper.cli";
interface VersionInfo {
current: string;
latest: string;
hasUpdate: boolean;
}
interface UpgradeOptions {
check?: boolean;
version?: string;
}
interface UpgradeResult {
success: boolean;
previousVersion: string;
newVersion: string;
message: string;
}
/**
* Parse semantic version string
*/
const parseVersion = (
version: string,
): { major: number; minor: number; patch: number } => {
const cleaned = version.replace(/^v/, "");
const [major = 0, minor = 0, patch = 0] = cleaned.split(".").map(Number);
return { major, minor, patch };
};
/**
* Compare two semantic versions
* Returns: 1 if a > b, -1 if a < b, 0 if equal
*/
const compareVersions = (a: string, b: string): number => {
const vA = parseVersion(a);
const vB = parseVersion(b);
if (vA.major !== vB.major) return vA.major > vB.major ? 1 : -1;
if (vA.minor !== vB.minor) return vA.minor > vB.minor ? 1 : -1;
if (vA.patch !== vB.patch) return vA.patch > vB.patch ? 1 : -1;
return 0;
};
/**
* Get the current installed version
*/
export const getCurrentVersion = (): string => {
return appVersion.version;
};
interface GitHubRelease {
tag_name?: string;
}
interface GitHubTag {
name?: string;
}
/**
* Get the latest version from GitHub
*/
export const getLatestVersion = async (): Promise<string> => {
try {
// Try to get latest release from GitHub API
const response = await fetch(`${REPO_API_URL}/releases/latest`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "codetyper-cli",
},
});
if (response.ok) {
const data = (await response.json()) as GitHubRelease;
return data.tag_name?.replace(/^v/, "") || getCurrentVersion();
}
// Fallback: get latest tag
const tagsResponse = await fetch(`${REPO_API_URL}/tags`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "codetyper-cli",
},
});
if (tagsResponse.ok) {
const tags = (await tagsResponse.json()) as GitHubTag[];
if (tags.length > 0) {
return tags[0].name?.replace(/^v/, "") || getCurrentVersion();
}
}
// Fallback: get latest commit info
const commitsResponse = await fetch(`${REPO_API_URL}/commits/master`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "codetyper-cli",
},
});
if (commitsResponse.ok) {
// Can't determine version from commits, return current
return getCurrentVersion();
}
return getCurrentVersion();
} catch {
// If we can't reach GitHub, return current version
return getCurrentVersion();
}
};
/**
* Check for available updates
*/
export const checkForUpdates = async (): Promise<VersionInfo> => {
const current = getCurrentVersion();
const latest = await getLatestVersion();
const hasUpdate = compareVersions(latest, current) > 0;
return { current, latest, hasUpdate };
};
/**
* Display spinner animation
*/
const createSpinner = (
message: string,
): { stop: (success: boolean, finalMessage?: string) => void } => {
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let frameIndex = 0;
let isRunning = true;
const interval = setInterval(() => {
if (isRunning) {
process.stdout.write(`\r${chalk.cyan(frames[frameIndex])} ${message}`);
frameIndex = (frameIndex + 1) % frames.length;
}
}, 80);
return {
stop: (success: boolean, finalMessage?: string) => {
isRunning = false;
clearInterval(interval);
const icon = success ? chalk.green("✓") : chalk.red("✗");
const msg = finalMessage || message;
process.stdout.write(`\r${icon} ${msg}\n`);
},
};
};
/**
* Backup current installation info for rollback
*/
const backupCurrentVersion = async (): Promise<string> => {
return getCurrentVersion();
};
/**
* Perform the upgrade
*/
export const performUpgrade = async (
options: UpgradeOptions = {},
): Promise<UpgradeResult> => {
const previousVersion = getCurrentVersion();
// Check only mode
if (options.check) {
const versionInfo = await checkForUpdates();
if (versionInfo.hasUpdate) {
console.log(
chalk.yellow(
`\nUpdate available: ${versionInfo.current}${versionInfo.latest}`,
),
);
console.log(chalk.gray(`Run 'codetyper upgrade' to update\n`));
} else {
console.log(
chalk.green(
`\nYou're on the latest version (${versionInfo.current})\n`,
),
);
}
return {
success: true,
previousVersion,
newVersion: versionInfo.latest,
message: versionInfo.hasUpdate
? "Update available"
: "Already up to date",
};
}
// Display current version
console.log(chalk.cyan(`\nCurrent version: ${previousVersion}`));
// Check for updates first
const spinner1 = createSpinner("Checking for updates...");
const versionInfo = await checkForUpdates();
spinner1.stop(true, "Checked for updates");
if (!options.version && !versionInfo.hasUpdate) {
console.log(
chalk.green(`\nAlready on the latest version (${versionInfo.current})\n`),
);
return {
success: true,
previousVersion,
newVersion: versionInfo.current,
message: "Already up to date",
};
}
const targetVersion = options.version || versionInfo.latest;
console.log(chalk.cyan(`Target version: ${targetVersion}\n`));
// Backup current version info
const backupVersion = await backupCurrentVersion();
// Perform upgrade
const spinner2 = createSpinner("Downloading and installing update...");
try {
const installCommand = options.version
? `npm install -g ${REPO_URL}#v${options.version}`
: `npm install -g ${REPO_URL}`;
await execAsync(installCommand, {
timeout: 120000, // 2 minute timeout
});
spinner2.stop(true, "Update installed successfully");
// Verify new version
const newVersion = options.version || targetVersion;
console.log(
chalk.green(
`\n✓ Successfully upgraded from ${previousVersion} to ${newVersion}`,
),
);
console.log(
chalk.gray(
"Restart your terminal or run 'codetyper --version' to verify\n",
),
);
return {
success: true,
previousVersion,
newVersion,
message: `Upgraded from ${previousVersion} to ${newVersion}`,
};
} catch (error) {
spinner2.stop(false, "Update failed");
// Attempt rollback
console.log(chalk.yellow("\nAttempting rollback..."));
const rollbackSpinner = createSpinner(
`Rolling back to ${backupVersion}...`,
);
try {
await execAsync(`npm install -g ${REPO_URL}#v${backupVersion}`, {
timeout: 120000,
});
rollbackSpinner.stop(true, `Rolled back to ${backupVersion}`);
console.log(
chalk.yellow("Rollback successful. Previous version restored.\n"),
);
} catch {
rollbackSpinner.stop(false, "Rollback failed");
console.log(
chalk.red("Rollback failed. Manual intervention may be required."),
);
console.log(chalk.gray(`Run: npm install -g ${REPO_URL}`));
}
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
previousVersion,
newVersion: previousVersion,
message: `Upgrade failed: ${errorMessage}`,
};
}
};
/**
* Display upgrade help
*/
export const displayUpgradeHelp = (): void => {
console.log(chalk.bold("\nCodeTyper Upgrade\n"));
console.log("Usage:");
console.log(
chalk.gray(" codetyper upgrade # Update to latest version"),
);
console.log(
chalk.gray(" codetyper upgrade --check # Check for updates only"),
);
console.log(
chalk.gray(
" codetyper upgrade --version 1.0.0 # Install specific version\n",
),
);
};