-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathupgrade.ts
More file actions
162 lines (138 loc) · 7.32 KB
/
upgrade.ts
File metadata and controls
162 lines (138 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import { Command } from 'commander';
import path from 'path';
import { logger } from '../core/logger.js';
import { readConfig, writeConfig } from '../core/config.js';
import { USER_PATHS } from '../system/paths.js';
import { readPolicy, resolveUpgradePolicy } from '../core/policy.js';
import {
checkForUpdates,
pickAsset,
fetchSha256Sums,
downloadFile,
verifySha256,
applyUpdateInstaller,
applyUpdatePortableExe,
detectInstallContext
} from '../core/selfUpdate.js';
import { isAdmin } from '../system/powershell.js';
function isSystemScopePath(filePath: string): boolean {
const normalized = filePath.toLowerCase();
return normalized.includes('\\program files') || normalized.includes('\\program files (x86)') || normalized.includes('\\programdata');
}
export const upgradeCommand = new Command('upgrade')
.description('Upgrade cloudsqlctl to the latest version')
.option('--check-only', 'Only check for updates, do not download or install')
.option('--no-install', 'Download only, do not install')
.option('--asset <mode>', 'Asset type to download (auto, installer, exe)', 'auto')
.option('--dir <path>', 'Download directory', path.join(USER_PATHS.HOME, 'downloads', 'updates'))
.option('--force', 'Force update even if version is same or older')
.option('--no-silent', 'Run installer in interactive mode (installer only)')
.option('--no-elevate', 'Do not attempt to elevate privileges (installer only)')
.option('--channel <channel>', 'Update channel (stable or beta)')
.option('--version <version>', 'Install a specific version (e.g. 0.4.14 or v0.4.14)')
.option('--pin <version>', 'Pin to a specific version for future upgrades')
.option('--unpin', 'Clear pinned version')
.option('--json', 'Output status in JSON format')
.action(async (options) => {
try {
const currentVersion = process.env.CLOUDSQLCTL_VERSION || '0.0.0';
const policy = await readPolicy();
const config = await readConfig();
const policyResolved = resolveUpgradePolicy(policy, {
channel: options.channel,
version: options.version,
pin: options.pin,
unpin: options.unpin
});
const channel = ((policyResolved.channel || options.channel || config.updateChannel || 'stable') as 'stable' | 'beta');
if (channel !== 'stable' && channel !== 'beta') {
throw new Error(`Invalid channel '${channel}'. Use 'stable' or 'beta'.`);
}
if (options.unpin) {
await writeConfig({ pinnedVersion: undefined });
}
if (options.pin) {
await writeConfig({ pinnedVersion: options.pin, updateChannel: channel });
} else if (options.channel) {
await writeConfig({ updateChannel: channel });
}
const targetVersion = policyResolved.targetVersion || options.version || options.pin || (options.unpin ? undefined : config.pinnedVersion);
if (!options.json) {
const suffix = targetVersion ? ` (target: ${targetVersion})` : '';
logger.info(`Checking for updates (Current: v${currentVersion}, channel: ${channel})${suffix}...`);
}
const status = await checkForUpdates(currentVersion, { channel, targetVersion });
if (options.json) {
console.log(JSON.stringify(status, null, 2));
if (options.checkOnly) return;
}
if (!status.updateAvailable && !options.force) {
if (!options.json) logger.info(`You are already on the latest version (v${status.latestVersion}).`);
return;
}
if (!options.json) logger.info(`New version available: v${status.latestVersion}`);
if (options.checkOnly) return;
if (!status.releaseInfo) {
throw new Error('Release info missing');
}
// 1. Pick Asset
const assetMode = options.asset as 'auto' | 'installer' | 'exe';
const asset = pickAsset(status.releaseInfo, assetMode);
if (!options.json) logger.info(`Selected asset: ${asset.name}`);
// 2. Fetch Checksums
if (!options.json) logger.info('Fetching checksums...');
const checksums = await fetchSha256Sums(status.releaseInfo);
const expectedHash = checksums.get(asset.name);
if (!expectedHash) {
throw new Error(`No checksum found for ${asset.name}`);
}
// 3. Download
const downloadDir = options.dir;
const downloadPath = path.join(downloadDir, asset.name);
if (!options.json) logger.info(`Downloading to ${downloadPath}...`);
await downloadFile(asset.url, downloadPath);
// 4. Verify
if (!options.json) logger.info('Verifying checksum...');
const valid = await verifySha256(downloadPath, expectedHash);
if (!valid) {
throw new Error('Checksum verification failed! File may be corrupted.');
}
if (!options.json) logger.info('Checksum verified.');
if (!options.install) {
if (!options.json) logger.info('Download complete. Install skipped (--no-install).');
return;
}
// 5. Apply Update
const context = options.asset === 'auto' ? detectInstallContext() : options.asset;
const admin = await isAdmin();
const systemScope = isSystemScopePath(process.execPath);
if (context === 'installer' && !admin && options.elevate === false) {
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
}
if (context === 'installer' || asset.name.endsWith('.exe') && asset.name.includes('setup')) {
if (!options.json) logger.info('Applying update via installer...');
const shouldElevate = !admin && options.elevate !== false;
await applyUpdateInstaller(downloadPath, options.silent !== false, shouldElevate);
} else {
if (!options.json) logger.info('Applying portable update...');
if (systemScope && !admin) {
throw new Error('Portable updates to system-scope installs require admin. Use the installer or re-run as admin.');
}
// For portable, we need to know the target exe.
// If running packaged, it's process.execPath.
// If running node, we can't really update "node.exe", so we assume dev env and warn.
if (path.basename(process.execPath).toLowerCase() === 'node.exe') {
logger.warn('Cannot auto-update when running via node. Please update source code or download binary manually.');
return;
}
await applyUpdatePortableExe(downloadPath, process.execPath);
}
} catch (error) {
if (options.json) {
console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
} else {
logger.error('Upgrade failed', error);
}
process.exit(1);
}
});