mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-17 00:14:34 +00:00
fix(update): target the current CCS installation
This commit is contained in:
1 parent
033116b863
commit
2aaabb2deb
5 files changed
+1131
-91
No files matched your search
+134
-36
@@ -7,7 +7,14 @@
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { initUI, header, ok, fail, warn, info, color } from '../utils/ui';
|
||||
import { detectPackageManager } from '../utils/package-manager-detector';
|
||||
import {
|
||||
buildPackageManagerEnv,
|
||||
detectCurrentInstall,
|
||||
formatManualUpdateCommand,
|
||||
readInstalledPackageState,
|
||||
type CurrentInstall,
|
||||
type InstalledPackageState,
|
||||
} from '../utils/package-manager-detector';
|
||||
import { compareVersionsWithPrerelease } from '../utils/update-checker';
|
||||
import { getVersion } from '../utils/version';
|
||||
|
||||
@@ -22,6 +29,24 @@ export interface UpdateOptions {
|
||||
// Version (from centralized utility)
|
||||
const CCS_VERSION = getVersion();
|
||||
|
||||
async function resolveTargetVersion(
|
||||
currentVersion: string,
|
||||
targetTag: 'latest' | 'dev'
|
||||
): Promise<string | undefined> {
|
||||
const { checkForUpdates } = await import('../utils/update-checker');
|
||||
const result = await checkForUpdates(currentVersion, true, 'npm', targetTag);
|
||||
|
||||
if (result.status === 'update_available' && result.latest) {
|
||||
return result.latest;
|
||||
}
|
||||
|
||||
if (result.status === 'no_update') {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the update command
|
||||
* Checks for updates and installs the latest version
|
||||
@@ -30,6 +55,7 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<
|
||||
await initUI();
|
||||
const { force = false, beta = false } = options;
|
||||
const targetTag = beta ? 'dev' : 'latest';
|
||||
const currentInstall = detectCurrentInstall();
|
||||
|
||||
console.log('');
|
||||
console.log(header('Checking for updates...'));
|
||||
@@ -39,16 +65,16 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<
|
||||
if (force) {
|
||||
console.log(info(`Force reinstall from @${targetTag} channel...`));
|
||||
console.log('');
|
||||
await performNpmUpdate(targetTag, true);
|
||||
const expectedVersion = await resolveTargetVersion(CCS_VERSION, targetTag);
|
||||
await performNpmUpdate(currentInstall, targetTag, true, expectedVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
const { checkForUpdates } = await import('../utils/update-checker');
|
||||
|
||||
const updateResult = await checkForUpdates(CCS_VERSION, true, 'npm', targetTag);
|
||||
|
||||
if (updateResult.status === 'check_failed') {
|
||||
handleCheckFailed(updateResult.message ?? 'Update check failed', targetTag);
|
||||
handleCheckFailed(updateResult.message ?? 'Update check failed', targetTag, currentInstall);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,13 +115,17 @@ export async function handleUpdateCommand(options: UpdateOptions = {}): Promise<
|
||||
console.log('');
|
||||
}
|
||||
|
||||
await performNpmUpdate(targetTag);
|
||||
await performNpmUpdate(currentInstall, targetTag, false, updateResult.latest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed update check
|
||||
*/
|
||||
function handleCheckFailed(message: string, targetTag: string = 'latest'): void {
|
||||
function handleCheckFailed(
|
||||
message: string,
|
||||
targetTag: string = 'latest',
|
||||
currentInstall: CurrentInstall = detectCurrentInstall()
|
||||
): void {
|
||||
console.log(fail(message));
|
||||
console.log('');
|
||||
console.log(warn('Possible causes:'));
|
||||
@@ -105,27 +135,7 @@ function handleCheckFailed(message: string, targetTag: string = 'latest'): void
|
||||
console.log('');
|
||||
console.log('Try again later or update manually:');
|
||||
|
||||
const packageManager = detectPackageManager();
|
||||
let manualCommand: string;
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'yarn':
|
||||
manualCommand = `yarn global add @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'pnpm':
|
||||
manualCommand = `pnpm add -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
case 'bun':
|
||||
manualCommand = `bun add -g @kaitranntt/ccs@${targetTag}`;
|
||||
break;
|
||||
default:
|
||||
manualCommand = `npm install -g @kaitranntt/ccs@${targetTag}`;
|
||||
}
|
||||
|
||||
console.log(color(` ${manualCommand}`, 'command'));
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -157,15 +167,94 @@ function handleNoUpdate(reason: string | undefined): void {
|
||||
/**
|
||||
* Perform update via npm/yarn/pnpm/bun
|
||||
*/
|
||||
async function performNpmUpdate(
|
||||
targetTag: string = 'latest',
|
||||
function verifyCurrentInstallVersion(
|
||||
currentInstall: CurrentInstall,
|
||||
targetTag: string,
|
||||
expectedVersion?: string,
|
||||
previousState?: InstalledPackageState,
|
||||
isReinstall: boolean = false
|
||||
): void {
|
||||
const nextState = readInstalledPackageState(currentInstall);
|
||||
const installedVersion = nextState.version;
|
||||
if (!installedVersion) {
|
||||
console.log('');
|
||||
console.log(
|
||||
fail('Update finished, but CCS could not verify the current installation version.')
|
||||
);
|
||||
console.log('');
|
||||
console.log('Current install remains ambiguous. Re-run manually:');
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const installChanged =
|
||||
previousState !== undefined &&
|
||||
(previousState.version !== nextState.version ||
|
||||
previousState.packageJsonMtimeMs !== nextState.packageJsonMtimeMs ||
|
||||
previousState.scriptMtimeMs !== nextState.scriptMtimeMs);
|
||||
|
||||
if (expectedVersion && installedVersion !== expectedVersion) {
|
||||
if (previousState?.version && installedVersion !== previousState.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comparison = compareVersionsWithPrerelease(installedVersion, expectedVersion);
|
||||
if (comparison < 0) {
|
||||
console.log('');
|
||||
console.log(
|
||||
fail(
|
||||
`Update completed outside the current installation. Current binary still reports ${installedVersion}; expected ${expectedVersion}.`
|
||||
)
|
||||
);
|
||||
if (previousState?.version && previousState.version === installedVersion) {
|
||||
console.log(
|
||||
warn(
|
||||
`The current install path did not change from ${previousState.version}; another package manager likely updated a different copy of CCS.`
|
||||
)
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
console.log('Re-run manually against the current install:');
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isReinstall &&
|
||||
previousState?.version &&
|
||||
installedVersion === previousState.version &&
|
||||
!installChanged
|
||||
) {
|
||||
console.log('');
|
||||
console.log(
|
||||
fail(
|
||||
`Reinstall completed, but CCS could not verify that the current installation changed from ${previousState.version}.`
|
||||
)
|
||||
);
|
||||
console.log('');
|
||||
console.log('Re-run manually against the current install:');
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function performNpmUpdate(
|
||||
currentInstall: CurrentInstall,
|
||||
targetTag: string = 'latest',
|
||||
isReinstall: boolean = false,
|
||||
expectedVersion?: string
|
||||
): Promise<void> {
|
||||
const packageManager = detectPackageManager();
|
||||
const packageManager = currentInstall.manager;
|
||||
let updateCommand: string;
|
||||
let updateArgs: string[];
|
||||
let cacheCommand: string | null;
|
||||
let cacheArgs: string[] | null;
|
||||
const childEnv = buildPackageManagerEnv(currentInstall);
|
||||
const previousState = readInstalledPackageState(currentInstall);
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
@@ -214,9 +303,9 @@ async function performNpmUpdate(
|
||||
? spawn(`${updateCommand} ${updateArgs.join(' ')}`, [], {
|
||||
stdio: ['inherit', 'inherit', 'pipe'],
|
||||
shell: true,
|
||||
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
||||
env: { ...childEnv, NODE_NO_WARNINGS: '1' },
|
||||
})
|
||||
: spawn(updateCommand, updateArgs, { stdio: 'inherit' });
|
||||
: spawn(updateCommand, updateArgs, { stdio: 'inherit', env: childEnv });
|
||||
|
||||
// On Windows, filter stderr to hide npm cleanup warnings (EPERM on bcrypt.node etc.)
|
||||
// These warnings are cosmetic - update succeeds despite file locking by antivirus/indexing
|
||||
@@ -244,6 +333,15 @@ async function performNpmUpdate(
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
if (expectedVersion || previousState?.version) {
|
||||
verifyCurrentInstallVersion(
|
||||
currentInstall,
|
||||
targetTag,
|
||||
expectedVersion,
|
||||
previousState,
|
||||
isReinstall
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
console.log(ok(`${isReinstall ? 'Reinstall' : 'Update'} successful!`));
|
||||
console.log('');
|
||||
@@ -255,7 +353,7 @@ async function performNpmUpdate(
|
||||
console.log(fail(`${isReinstall ? 'Reinstall' : 'Update'} failed`));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command'));
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
}
|
||||
process.exit(code || 0);
|
||||
@@ -266,7 +364,7 @@ async function performNpmUpdate(
|
||||
console.log(fail(`Failed to run ${packageManager} ${isReinstall ? 'reinstall' : 'update'}`));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(color(` ${updateCommand} ${updateArgs.join(' ')}`, 'command'));
|
||||
console.log(color(` ${formatManualUpdateCommand(targetTag, currentInstall)}`, 'command'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -288,9 +386,9 @@ async function performNpmUpdate(
|
||||
? spawn(`${cacheCommand} ${cacheArgs.join(' ')}`, [], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, NODE_NO_WARNINGS: '1' },
|
||||
env: { ...childEnv, NODE_NO_WARNINGS: '1' },
|
||||
})
|
||||
: spawn(cacheCommand, cacheArgs, { stdio: 'inherit' });
|
||||
: spawn(cacheCommand, cacheArgs, { stdio: 'inherit', env: childEnv });
|
||||
|
||||
cacheChild.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
|
||||
@@ -1,75 +1,338 @@
|
||||
/**
|
||||
* Package Manager Detector Utilities
|
||||
*
|
||||
* Cross-platform package manager detection utilities for CCS.
|
||||
* Now only supports npm-based installation (npm/yarn/pnpm/bun).
|
||||
* Detect the package manager and install root that own the CURRENT CCS binary.
|
||||
* This is intentionally different from "which package manager has CCS installed
|
||||
* somewhere on the machine" because self-update must target the current install.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
|
||||
|
||||
export interface CurrentInstall {
|
||||
manager: PackageManager;
|
||||
scriptPath: string;
|
||||
resolvedScriptPath: string;
|
||||
packageRoot: string | null;
|
||||
prefix: string | null;
|
||||
detectionSource: 'path' | 'package-root' | 'default';
|
||||
}
|
||||
|
||||
export interface InstalledPackageState {
|
||||
version: string | null;
|
||||
packageJsonMtimeMs: number | null;
|
||||
scriptMtimeMs: number | null;
|
||||
}
|
||||
|
||||
const CCS_PACKAGE_NAME = '@kaitranntt/ccs';
|
||||
|
||||
function resolveScriptPath(scriptPath: string): string {
|
||||
if (path.win32.isAbsolute(scriptPath)) {
|
||||
return scriptPath;
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.realpathSync(scriptPath);
|
||||
} catch {
|
||||
return path.resolve(scriptPath);
|
||||
}
|
||||
}
|
||||
|
||||
function findPackageRoot(scriptPath: string): string | null {
|
||||
let currentDir = path.dirname(scriptPath);
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const packageJsonPath = path.join(currentDir, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
|
||||
name?: string;
|
||||
};
|
||||
if (packageJson.name === CCS_PACKAGE_NAME) {
|
||||
return currentDir;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed package.json and keep walking upward.
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
break;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPrefixBeforeMarker(packageRoot: string, marker: string): string | null {
|
||||
const index = packageRoot.lastIndexOf(marker);
|
||||
return index >= 0 ? packageRoot.slice(0, index) || path.parse(packageRoot).root : null;
|
||||
}
|
||||
|
||||
function inferInstallFromPath(
|
||||
targetPath: string
|
||||
): Pick<CurrentInstall, 'manager' | 'prefix'> | null {
|
||||
const normalizedPath = targetPath.split(path.sep).join('/');
|
||||
|
||||
if (
|
||||
normalizedPath.includes(`/install/global/node_modules/@kaitranntt/ccs`) ||
|
||||
normalizedPath.includes(`/.bun/install/global/node_modules/@kaitranntt/ccs`)
|
||||
) {
|
||||
return {
|
||||
manager: 'bun',
|
||||
prefix: getPrefixBeforeMarker(
|
||||
targetPath,
|
||||
`${path.sep}install${path.sep}global${path.sep}node_modules`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPath.includes(`/global/node_modules/@kaitranntt/ccs`)) {
|
||||
return {
|
||||
manager: 'yarn',
|
||||
prefix: getPrefixBeforeMarker(targetPath, `${path.sep}global${path.sep}node_modules`),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath.includes('/global/') &&
|
||||
normalizedPath.includes('/.pnpm/') &&
|
||||
normalizedPath.includes('/node_modules/@kaitranntt/ccs')
|
||||
) {
|
||||
const pnpmVirtualStoreMatch = targetPath.match(
|
||||
new RegExp(
|
||||
`${path.sep.replace(/\\/g, '\\\\')}global${path.sep.replace(/\\/g, '\\\\')}[^${path.sep.replace(
|
||||
/\\/g,
|
||||
'\\\\'
|
||||
)}]+${path.sep.replace(/\\/g, '\\\\')}\\.pnpm${path.sep.replace(/\\/g, '\\\\')}`
|
||||
)
|
||||
);
|
||||
if (pnpmVirtualStoreMatch) {
|
||||
return {
|
||||
manager: 'pnpm',
|
||||
prefix: getPrefixBeforeMarker(targetPath, `${path.sep}global${path.sep}`),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath.includes('/global/') &&
|
||||
normalizedPath.includes('/node_modules/@kaitranntt/ccs')
|
||||
) {
|
||||
return {
|
||||
manager: 'pnpm',
|
||||
prefix: getPrefixBeforeMarker(targetPath, `${path.sep}global${path.sep}`),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath.includes('/.pnpm/') &&
|
||||
normalizedPath.includes('/node_modules/@kaitranntt/ccs')
|
||||
) {
|
||||
return {
|
||||
manager: 'pnpm',
|
||||
prefix: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedPath.includes('/lib/node_modules/@kaitranntt/ccs')) {
|
||||
return {
|
||||
manager: 'npm',
|
||||
prefix: getPrefixBeforeMarker(targetPath, `${path.sep}lib${path.sep}node_modules`),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath.includes('/node_modules/@kaitranntt/ccs') &&
|
||||
!normalizedPath.includes('/global/node_modules/@kaitranntt/ccs') &&
|
||||
!normalizedPath.includes('/install/global/node_modules/@kaitranntt/ccs') &&
|
||||
!normalizedPath.includes('/.pnpm/')
|
||||
) {
|
||||
return {
|
||||
manager: 'npm',
|
||||
prefix: getPrefixBeforeMarker(targetPath, `${path.sep}node_modules`),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which package manager was used for installation
|
||||
* Detect the current install owner from the path of the running script.
|
||||
* Defaults to npm when the path is ambiguous because npm's global layout is
|
||||
* the safest fallback for the existing manual remediation commands.
|
||||
*/
|
||||
export function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' {
|
||||
const scriptPath = process.argv[1];
|
||||
export function detectCurrentInstall(scriptPath: string = process.argv[1] || ''): CurrentInstall {
|
||||
const resolvedScriptPath = resolveScriptPath(scriptPath);
|
||||
const pathMatch = inferInstallFromPath(resolvedScriptPath) ?? inferInstallFromPath(scriptPath);
|
||||
const packageRoot = findPackageRoot(resolvedScriptPath);
|
||||
|
||||
// Check if script path contains package manager indicators
|
||||
if (scriptPath.includes('.pnpm')) return 'pnpm';
|
||||
if (scriptPath.includes('yarn')) return 'yarn';
|
||||
if (scriptPath.includes('bun')) return 'bun';
|
||||
|
||||
// Check parent directories for lock files
|
||||
const binDir = path.dirname(scriptPath);
|
||||
|
||||
let checkDir = binDir;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (fs.existsSync(path.join(checkDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
||||
if (fs.existsSync(path.join(checkDir, 'yarn.lock'))) return 'yarn';
|
||||
if (fs.existsSync(path.join(checkDir, 'bun.lockb'))) return 'bun';
|
||||
checkDir = path.dirname(checkDir);
|
||||
if (pathMatch) {
|
||||
return {
|
||||
manager: pathMatch.manager,
|
||||
scriptPath,
|
||||
resolvedScriptPath,
|
||||
packageRoot,
|
||||
prefix: pathMatch.prefix,
|
||||
detectionSource: 'path',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if package managers are available on the system
|
||||
try {
|
||||
const yarnResult = spawnSync('yarn', ['global', 'list', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (yarnResult.status === 0 && yarnResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'yarn';
|
||||
if (packageRoot) {
|
||||
const packageRootMatch = inferInstallFromPath(packageRoot);
|
||||
if (packageRootMatch) {
|
||||
return {
|
||||
manager: packageRootMatch.manager,
|
||||
scriptPath,
|
||||
resolvedScriptPath,
|
||||
packageRoot,
|
||||
prefix: packageRootMatch.prefix,
|
||||
detectionSource: 'package-root',
|
||||
};
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to next check
|
||||
}
|
||||
|
||||
return {
|
||||
manager: 'npm',
|
||||
scriptPath,
|
||||
resolvedScriptPath,
|
||||
packageRoot,
|
||||
prefix: null,
|
||||
detectionSource: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible helper for callers that only need the package manager.
|
||||
*/
|
||||
export function detectPackageManager(scriptPath: string = process.argv[1] || ''): PackageManager {
|
||||
return detectCurrentInstall(scriptPath).manager;
|
||||
}
|
||||
|
||||
export function buildPackageManagerEnv(
|
||||
install: CurrentInstall,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env
|
||||
): NodeJS.ProcessEnv {
|
||||
if (!install.prefix) {
|
||||
return { ...baseEnv };
|
||||
}
|
||||
|
||||
switch (install.manager) {
|
||||
case 'npm':
|
||||
return {
|
||||
...baseEnv,
|
||||
npm_config_prefix: install.prefix,
|
||||
NPM_CONFIG_PREFIX: install.prefix,
|
||||
};
|
||||
case 'yarn':
|
||||
return {
|
||||
...baseEnv,
|
||||
YARN_GLOBAL_FOLDER: install.prefix,
|
||||
};
|
||||
case 'pnpm':
|
||||
return {
|
||||
...baseEnv,
|
||||
PNPM_HOME: install.prefix,
|
||||
};
|
||||
case 'bun':
|
||||
return {
|
||||
...baseEnv,
|
||||
BUN_INSTALL: install.prefix,
|
||||
};
|
||||
default:
|
||||
return { ...baseEnv };
|
||||
}
|
||||
}
|
||||
|
||||
export function readInstalledPackageVersion(install: CurrentInstall): string | null {
|
||||
return readInstalledPackageState(install).version;
|
||||
}
|
||||
|
||||
function readFileMtimeMs(filePath: string): number | null {
|
||||
try {
|
||||
const pnpmResult = spawnSync('pnpm', ['list', '-g', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (pnpmResult.status === 0 && pnpmResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'pnpm';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to next check
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function readInstalledPackageState(install: CurrentInstall): InstalledPackageState {
|
||||
if (!install.packageRoot) {
|
||||
return {
|
||||
version: null,
|
||||
packageJsonMtimeMs: null,
|
||||
scriptMtimeMs: readFileMtimeMs(install.resolvedScriptPath),
|
||||
};
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(install.packageRoot, 'package.json');
|
||||
const packageJsonMtimeMs = readFileMtimeMs(packageJsonPath);
|
||||
const scriptMtimeMs = readFileMtimeMs(install.resolvedScriptPath);
|
||||
|
||||
try {
|
||||
const bunResult = spawnSync('bun', ['pm', 'ls', '-g', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (bunResult.status === 0 && bunResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'bun';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to default
|
||||
}
|
||||
|
||||
return 'npm';
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
version: typeof packageJson.version === 'string' ? packageJson.version : null,
|
||||
packageJsonMtimeMs,
|
||||
scriptMtimeMs,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
version: null,
|
||||
packageJsonMtimeMs,
|
||||
scriptMtimeMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function quoteForShell(value: string): string {
|
||||
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function quoteForCmd(value: string): string {
|
||||
return value.replace(/'/g, "''");
|
||||
}
|
||||
|
||||
function formatWindowsEnvCommand(envVar: string, value: string, command: string): string {
|
||||
return `powershell -NoProfile -Command "$env:${envVar}='${quoteForCmd(value)}'; ${command}"`;
|
||||
}
|
||||
|
||||
export function formatManualUpdateCommand(
|
||||
targetTag: string,
|
||||
install: CurrentInstall = detectCurrentInstall(),
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): string {
|
||||
const command = {
|
||||
npm: `npm install -g @kaitranntt/ccs@${targetTag}`,
|
||||
yarn: `yarn global add @kaitranntt/ccs@${targetTag}`,
|
||||
pnpm: `pnpm add -g @kaitranntt/ccs@${targetTag}`,
|
||||
bun: `bun add -g @kaitranntt/ccs@${targetTag}`,
|
||||
}[install.manager];
|
||||
|
||||
if (!install.prefix) {
|
||||
return command;
|
||||
}
|
||||
|
||||
const envVar = {
|
||||
npm: 'NPM_CONFIG_PREFIX',
|
||||
yarn: 'YARN_GLOBAL_FOLDER',
|
||||
pnpm: 'PNPM_HOME',
|
||||
bun: 'BUN_INSTALL',
|
||||
}[install.manager];
|
||||
|
||||
if (platform === 'win32') {
|
||||
return formatWindowsEnvCommand(envVar, install.prefix, command);
|
||||
}
|
||||
|
||||
return `${envVar}=${quoteForShell(install.prefix)} ${command}`;
|
||||
}
|
||||
Reference in new issue
Block a user