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
This commit is contained in:
323
src/services/upgrade.ts
Normal file
323
src/services/upgrade.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 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",
|
||||
),
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user