Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ jobs:
- run: npm ci
- run: npm run lint
- run: npm run build
- run: npm run test:all
- run: npm run test
- run: npm run test:integration
env:
TIGRIS_STORAGE_ACCESS_KEY_ID: ${{ secrets.TIGRIS_STORAGE_ACCESS_KEY_ID }}
TIGRIS_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.TIGRIS_STORAGE_SECRET_ACCESS_KEY }}
Expand Down
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export const DEFAULT_IAM_ENDPOINT = 'https://iam.storageapi.dev';
export const DEFAULT_MGMT_ENDPOINT = 'https://mgmt.storageapi.dev';
export const NPM_REGISTRY_URL =
'https://registry.npmjs.org/@tigrisdata/cli/latest';
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // Check for updates every 24 hours
export const UPDATE_NOTIFY_INTERVAL_MS = 6 * 60 * 60 * 1000; // Show update notification every 6 hours
export const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // Check for updates every 6 hours
export const UPDATE_NOTIFY_INTERVAL_MS = 1 * 60 * 60 * 1000; // Show update notification every 1 hour
62 changes: 62 additions & 0 deletions src/lib/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { failWithError } from '@utils/exit.js';
import {
msg,
printAlreadyDone,
printStart,
printSuccess,
} from '@utils/messages.js';
import { getFormat } from '@utils/options.js';
import {
fetchLatestVersion,
getUpdateCommand,
isNewerVersion,
} from '@utils/update-check.js';
import { execSync } from 'child_process';

import { version as currentVersion } from '../../package.json';

const context = msg('update');

export default async function update(
options: Record<string, unknown> = {}
): Promise<void> {
const format = getFormat(options);

printStart(context);

try {
// Always fetch fresh when the user explicitly runs `tigris update`
const latestVersion = await fetchLatestVersion();

const updateAvailable = isNewerVersion(currentVersion, latestVersion);
const updateCommand = getUpdateCommand();

if (format === 'json') {
console.log(
JSON.stringify({
currentVersion,
latestVersion,
updateAvailable,
updateCommand,
})
);
return;
}

console.log(`Current version: ${currentVersion}`);

if (updateAvailable) {
console.log(`Latest version: ${latestVersion}`);
console.log('Updating...');
execSync(updateCommand, {
stdio: 'inherit',
...(process.platform === 'win32' ? { shell: 'powershell.exe' } : {}),
});
printSuccess(context, { latestVersion });
} else {
printAlreadyDone(context, { currentVersion });
}
} catch (error) {
failWithError(context, error);
}
}
11 changes: 11 additions & 0 deletions src/specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ commands:
onSuccess: ''
onFailure: 'Failed to get user information'
onAlreadyDone: "Not authenticated\nRun \"tigris login\" to authenticate"
# update
- name: update
description: Update the CLI to the latest version
examples:
- "tigris update"
messages:
onStart: 'Checking for updates...'
onSuccess: 'Updated to {{latestVersion}}'
onFailure: 'Failed to update'
onAlreadyDone: 'Already on the latest version ({{currentVersion}})'

# logout
- name: logout
description: End the current session and clear login state. Credentials saved via 'configure' are kept
Expand Down
76 changes: 48 additions & 28 deletions src/utils/update-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface UpdateCheckCache {

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

function readUpdateCache(): UpdateCheckCache | null {
export function readUpdateCache(): UpdateCheckCache | null {
try {
const data = readFileSync(CACHE_PATH, 'utf-8');
const parsed = JSON.parse(data);
Expand Down Expand Up @@ -92,9 +92,33 @@ export function isNewerVersion(current: string, latest: string): boolean {
return false;
}

function fetchLatestVersionInBackground(): void {
try {
const req = https.get(NPM_REGISTRY_URL, { timeout: 5000 }, (res) => {
/**
* Returns the platform-appropriate shell command for updating the CLI.
*/
export function getUpdateCommand(): string {
const isBinary =
(globalThis as { __TIGRIS_BINARY?: boolean }).__TIGRIS_BINARY === true;
const isWindows = process.platform === 'win32';

if (!isBinary) {
return 'npm install -g @tigrisdata/cli';
} else if (isWindows) {
return 'irm https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.ps1 | iex';
} else {
return 'curl -fsSL https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.sh | sh';
}
}

/**
* Fetch the latest published version string from the npm registry.
* When `unref` is true the underlying socket is unref'd so it won't
* keep the process alive (used by the background check).
*/
export function fetchLatestVersion(
options: { unref?: boolean } = {}
): Promise<string> {
return new Promise((resolve, reject) => {
const req = https.get(NPM_REGISTRY_URL, { timeout: 10000 }, (res) => {
let data = '';
res.on('data', (chunk: Buffer) => {
data += chunk;
Expand All @@ -103,32 +127,40 @@ function fetchLatestVersionInBackground(): void {
try {
const json = JSON.parse(data);
if (typeof json.version === 'string') {
const existing = readUpdateCache();
writeUpdateCache({
...existing,
...readUpdateCache(),
latestVersion: json.version,
lastChecked: Date.now(),
});
resolve(json.version);
} else {
reject(new Error('Unexpected registry response'));
}
} catch {
// Silent on parse failure
reject(new Error('Failed to parse registry response'));
}
});
});
req.on('error', () => {
// Silent on network failure
req.on('error', (err) => {
reject(err);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (options.unref) {
req.on('socket', (socket) => {
socket.unref();
});
}
req.end();
// Unref so the request doesn't keep the process alive
req.on('socket', (socket) => {
socket.unref();
});
} catch {
});
}

function fetchLatestVersionInBackground(): void {
fetchLatestVersion({ unref: true }).catch(() => {
// Silent on failure
}
});
}

export function checkForUpdates(): void {
Expand All @@ -146,20 +178,8 @@ export function checkForUpdates(): void {
!cache.lastNotified ||
Date.now() - cache.lastNotified > notifyIntervalMs
) {
const isBinary =
(globalThis as { __TIGRIS_BINARY?: boolean }).__TIGRIS_BINARY === true;
const isWindows = process.platform === 'win32';
const line1 = `Update available: ${currentVersion} → ${cache.latestVersion}`;
let line2: string;
if (!isBinary) {
line2 = 'Run `npm install -g @tigrisdata/cli` to upgrade.';
} else if (isWindows) {
line2 =
'Run `irm https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.ps1 | iex`';
} else {
line2 =
'Run `curl -fsSL https://raw.githubusercontent.com/tigrisdata/cli/main/scripts/install.sh | sh`';
}
const line2 = 'Run "tigris update" to upgrade.';
const width = Math.max(line1.length, line2.length) + 4;
const top = '┌' + '─'.repeat(width - 2) + '┐';
const bot = '└' + '─'.repeat(width - 2) + '┘';
Expand Down