Skip to content

Commit e51c01e

Browse files
designcodeclaude
andauthored
feat: update command (#57)
* ci: split test:all in release workflow to isolate unit tests from secrets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update command Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ee81e5b commit e51c01e

5 files changed

Lines changed: 125 additions & 31 deletions

File tree

.github/workflows/release.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ jobs:
3434
- run: npm ci
3535
- run: npm run lint
3636
- run: npm run build
37-
- run: npm run test:all
37+
- run: npm run test
38+
- run: npm run test:integration
3839
env:
3940
TIGRIS_STORAGE_ACCESS_KEY_ID: ${{ secrets.TIGRIS_STORAGE_ACCESS_KEY_ID }}
4041
TIGRIS_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.TIGRIS_STORAGE_SECRET_ACCESS_KEY }}

src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ export const DEFAULT_IAM_ENDPOINT = 'https://iam.storageapi.dev';
33
export const DEFAULT_MGMT_ENDPOINT = 'https://mgmt.storageapi.dev';
44
export const NPM_REGISTRY_URL =
55
'https://registry.npmjs.org/@tigrisdata/cli/latest';
6-
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // Check for updates every 24 hours
7-
export const UPDATE_NOTIFY_INTERVAL_MS = 6 * 60 * 60 * 1000; // Show update notification every 6 hours
6+
export const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // Check for updates every 6 hours
7+
export const UPDATE_NOTIFY_INTERVAL_MS = 1 * 60 * 60 * 1000; // Show update notification every 1 hour

src/lib/update.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { failWithError } from '@utils/exit.js';
2+
import {
3+
msg,
4+
printAlreadyDone,
5+
printStart,
6+
printSuccess,
7+
} from '@utils/messages.js';
8+
import { getFormat } from '@utils/options.js';
9+
import {
10+
fetchLatestVersion,
11+
getUpdateCommand,
12+
isNewerVersion,
13+
} from '@utils/update-check.js';
14+
import { execSync } from 'child_process';
15+
16+
import { version as currentVersion } from '../../package.json';
17+
18+
const context = msg('update');
19+
20+
export default async function update(
21+
options: Record<string, unknown> = {}
22+
): Promise<void> {
23+
const format = getFormat(options);
24+
25+
printStart(context);
26+
27+
try {
28+
// Always fetch fresh when the user explicitly runs `tigris update`
29+
const latestVersion = await fetchLatestVersion();
30+
31+
const updateAvailable = isNewerVersion(currentVersion, latestVersion);
32+
const updateCommand = getUpdateCommand();
33+
34+
if (format === 'json') {
35+
console.log(
36+
JSON.stringify({
37+
currentVersion,
38+
latestVersion,
39+
updateAvailable,
40+
updateCommand,
41+
})
42+
);
43+
return;
44+
}
45+
46+
console.log(`Current version: ${currentVersion}`);
47+
48+
if (updateAvailable) {
49+
console.log(`Latest version: ${latestVersion}`);
50+
console.log('Updating...');
51+
execSync(updateCommand, {
52+
stdio: 'inherit',
53+
...(process.platform === 'win32' ? { shell: 'powershell.exe' } : {}),
54+
});
55+
printSuccess(context, { latestVersion });
56+
} else {
57+
printAlreadyDone(context, { currentVersion });
58+
}
59+
} catch (error) {
60+
failWithError(context, error);
61+
}
62+
}

src/specs.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,17 @@ commands:
215215
onSuccess: ''
216216
onFailure: 'Failed to get user information'
217217
onAlreadyDone: "Not authenticated\nRun \"tigris login\" to authenticate"
218+
# update
219+
- name: update
220+
description: Update the CLI to the latest version
221+
examples:
222+
- "tigris update"
223+
messages:
224+
onStart: 'Checking for updates...'
225+
onSuccess: 'Updated to {{latestVersion}}'
226+
onFailure: 'Failed to update'
227+
onAlreadyDone: 'Already on the latest version ({{currentVersion}})'
228+
218229
# logout
219230
- name: logout
220231
description: End the current session and clear login state. Credentials saved via 'configure' are kept

src/utils/update-check.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface UpdateCheckCache {
1818

1919
const CACHE_PATH = join(homedir(), '.tigris', 'update-check.json');
2020

21-
function readUpdateCache(): UpdateCheckCache | null {
21+
export function readUpdateCache(): UpdateCheckCache | null {
2222
try {
2323
const data = readFileSync(CACHE_PATH, 'utf-8');
2424
const parsed = JSON.parse(data);
@@ -92,9 +92,33 @@ export function isNewerVersion(current: string, latest: string): boolean {
9292
return false;
9393
}
9494

95-
function fetchLatestVersionInBackground(): void {
96-
try {
97-
const req = https.get(NPM_REGISTRY_URL, { timeout: 5000 }, (res) => {
95+
/**
96+
* Returns the platform-appropriate shell command for updating the CLI.
97+
*/
98+
export function getUpdateCommand(): string {
99+
const isBinary =
100+
(globalThis as { __TIGRIS_BINARY?: boolean }).__TIGRIS_BINARY === true;
101+
const isWindows = process.platform === 'win32';
102+
103+
if (!isBinary) {
104+
return 'npm install -g @tigrisdata/cli';
105+
} else if (isWindows) {
106+
return 'irm https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.ps1 | iex';
107+
} else {
108+
return 'curl -fsSL https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.sh | sh';
109+
}
110+
}
111+
112+
/**
113+
* Fetch the latest published version string from the npm registry.
114+
* When `unref` is true the underlying socket is unref'd so it won't
115+
* keep the process alive (used by the background check).
116+
*/
117+
export function fetchLatestVersion(
118+
options: { unref?: boolean } = {}
119+
): Promise<string> {
120+
return new Promise((resolve, reject) => {
121+
const req = https.get(NPM_REGISTRY_URL, { timeout: 10000 }, (res) => {
98122
let data = '';
99123
res.on('data', (chunk: Buffer) => {
100124
data += chunk;
@@ -103,32 +127,40 @@ function fetchLatestVersionInBackground(): void {
103127
try {
104128
const json = JSON.parse(data);
105129
if (typeof json.version === 'string') {
106-
const existing = readUpdateCache();
107130
writeUpdateCache({
108-
...existing,
131+
...readUpdateCache(),
109132
latestVersion: json.version,
110133
lastChecked: Date.now(),
111134
});
135+
resolve(json.version);
136+
} else {
137+
reject(new Error('Unexpected registry response'));
112138
}
113139
} catch {
114-
// Silent on parse failure
140+
reject(new Error('Failed to parse registry response'));
115141
}
116142
});
117143
});
118-
req.on('error', () => {
119-
// Silent on network failure
144+
req.on('error', (err) => {
145+
reject(err);
120146
});
121147
req.on('timeout', () => {
122148
req.destroy();
149+
reject(new Error('Request timed out'));
123150
});
151+
if (options.unref) {
152+
req.on('socket', (socket) => {
153+
socket.unref();
154+
});
155+
}
124156
req.end();
125-
// Unref so the request doesn't keep the process alive
126-
req.on('socket', (socket) => {
127-
socket.unref();
128-
});
129-
} catch {
157+
});
158+
}
159+
160+
function fetchLatestVersionInBackground(): void {
161+
fetchLatestVersion({ unref: true }).catch(() => {
130162
// Silent on failure
131-
}
163+
});
132164
}
133165

134166
export function checkForUpdates(): void {
@@ -146,20 +178,8 @@ export function checkForUpdates(): void {
146178
!cache.lastNotified ||
147179
Date.now() - cache.lastNotified > notifyIntervalMs
148180
) {
149-
const isBinary =
150-
(globalThis as { __TIGRIS_BINARY?: boolean }).__TIGRIS_BINARY === true;
151-
const isWindows = process.platform === 'win32';
152181
const line1 = `Update available: ${currentVersion}${cache.latestVersion}`;
153-
let line2: string;
154-
if (!isBinary) {
155-
line2 = 'Run `npm install -g @tigrisdata/cli` to upgrade.';
156-
} else if (isWindows) {
157-
line2 =
158-
'Run `irm https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.ps1 | iex`';
159-
} else {
160-
line2 =
161-
'Run `curl -fsSL https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.sh | sh`';
162-
}
182+
const line2 = 'Run "tigris update" to upgrade.';
163183
const width = Math.max(line1.length, line2.length) + 4;
164184
const top = '┌' + '─'.repeat(width - 2) + '┐';
165185
const bot = '└' + '─'.repeat(width - 2) + '┘';

0 commit comments

Comments
 (0)