@runloop/rl-cli 0.1.0 ā 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -1
- package/dist/commands/blueprint/list.js +3 -3
- package/dist/commands/devbox/list.js +3 -3
- package/dist/components/Breadcrumb.js +51 -2
- package/dist/components/DevboxActionsMenu.js +1 -1
- package/dist/components/MainMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +2 -2
- package/dist/utils/config.js +93 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,14 @@ program
|
|
|
33
33
|
const { default: auth } = await import("./commands/auth.js");
|
|
34
34
|
auth();
|
|
35
35
|
});
|
|
36
|
+
program
|
|
37
|
+
.command("check-updates")
|
|
38
|
+
.description("Check for CLI updates")
|
|
39
|
+
.action(async () => {
|
|
40
|
+
const { checkForUpdates } = await import("./utils/config.js");
|
|
41
|
+
console.log("Checking for updates...");
|
|
42
|
+
await checkForUpdates(true);
|
|
43
|
+
});
|
|
36
44
|
// Devbox commands
|
|
37
45
|
const devbox = program
|
|
38
46
|
.command("devbox")
|
|
@@ -443,12 +451,15 @@ program
|
|
|
443
451
|
process.exit(1);
|
|
444
452
|
}
|
|
445
453
|
}
|
|
446
|
-
// If no command provided, show main menu
|
|
454
|
+
// If no command provided, show main menu (version check handled in UI)
|
|
447
455
|
if (args.length === 0) {
|
|
448
456
|
const { runMainMenu } = await import("./commands/menu.js");
|
|
449
457
|
runMainMenu();
|
|
450
458
|
}
|
|
451
459
|
else {
|
|
460
|
+
// Check for updates for non-interactive commands (stderr output)
|
|
461
|
+
const { checkForUpdates } = await import("./utils/config.js");
|
|
462
|
+
await checkForUpdates();
|
|
452
463
|
program.parse();
|
|
453
464
|
}
|
|
454
465
|
})();
|
|
@@ -357,15 +357,15 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
357
357
|
}
|
|
358
358
|
// Loading state
|
|
359
359
|
if (loading) {
|
|
360
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
360
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
361
361
|
}
|
|
362
362
|
// Error state
|
|
363
363
|
if (listError) {
|
|
364
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
364
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
365
365
|
}
|
|
366
366
|
// Empty state
|
|
367
367
|
if (blueprints.length === 0) {
|
|
368
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No blueprints found. Try:" }), _jsx(Text, { color: colors.primary, bold: true, children: "rli blueprint create" })] })] }));
|
|
368
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }], showVersionCheck: true }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No blueprints found. Try:" }), _jsx(Text, { color: colors.primary, bold: true, children: "rli blueprint create" })] })] }));
|
|
369
369
|
}
|
|
370
370
|
// Pagination moved earlier
|
|
371
371
|
// Overlay: draw quick actions popup over the table (keep table visible)
|
|
@@ -478,7 +478,7 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
478
478
|
}
|
|
479
479
|
// Show popup with table in background
|
|
480
480
|
if (showPopup && selectedDevbox) {
|
|
481
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), !initialLoading && !error && devboxes.length > 0 && (_jsx(_Fragment, { children: _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
481
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), !initialLoading && !error && devboxes.length > 0 && (_jsx(_Fragment, { children: _jsx(Table, { data: currentDevboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: `devboxes[${totalCount}]`, columns: [
|
|
482
482
|
{
|
|
483
483
|
key: "statusIcon",
|
|
484
484
|
label: "",
|
|
@@ -555,10 +555,10 @@ const ListDevboxesUI = ({ status, onSSHRequest, focusDevboxId, onBack, onExit })
|
|
|
555
555
|
}
|
|
556
556
|
// If initial loading or error, show that first
|
|
557
557
|
if (initialLoading) {
|
|
558
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
558
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
559
559
|
}
|
|
560
560
|
if (error) {
|
|
561
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
561
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }], showVersionCheck: true }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
|
|
562
562
|
}
|
|
563
563
|
// List view with data (always show, even if empty)
|
|
564
564
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), currentDevboxes && currentDevboxes.length >= 0 && (_jsxs(_Fragment, { children: [searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search:", " "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search (name, id, status)...", onSubmit: () => {
|
|
@@ -2,8 +2,57 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
|
|
5
|
+
import { VERSION } from "../cli.js";
|
|
6
|
+
// Version check component
|
|
7
|
+
const VersionCheck = () => {
|
|
8
|
+
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
9
|
+
const [isChecking, setIsChecking] = React.useState(true);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
const checkForUpdates = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const currentVersion = process.env.npm_package_version || "0.0.1";
|
|
14
|
+
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
15
|
+
if (response.ok) {
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
const latestVersion = data.version;
|
|
18
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
19
|
+
// Check if current version is older than latest
|
|
20
|
+
const compareVersions = (version1, version2) => {
|
|
21
|
+
const v1parts = version1.split('.').map(Number);
|
|
22
|
+
const v2parts = version2.split('.').map(Number);
|
|
23
|
+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
24
|
+
const v1part = v1parts[i] || 0;
|
|
25
|
+
const v2part = v2parts[i] || 0;
|
|
26
|
+
if (v1part > v2part)
|
|
27
|
+
return 1;
|
|
28
|
+
if (v1part < v2part)
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
};
|
|
33
|
+
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
34
|
+
if (isUpdateAvailable) {
|
|
35
|
+
setUpdateAvailable(latestVersion);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// Silently fail
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setIsChecking(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
checkForUpdates();
|
|
48
|
+
}, []);
|
|
49
|
+
if (isChecking || !updateAvailable) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
|
|
53
|
+
};
|
|
54
|
+
export const Breadcrumb = React.memo(({ items, showVersionCheck = false }) => {
|
|
6
55
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
7
56
|
const isDevEnvironment = env === "dev";
|
|
8
|
-
return (
|
|
57
|
+
return (_jsxs(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: "redBright", bold: true, children: [" ", "(dev)"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u203A", " "] }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.text : colors.textDim, bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u203A", " "] }))] }, index)))] }), showVersionCheck && (_jsx(Box, { paddingX: 2, marginTop: 0, children: _jsx(VersionCheck, {}) }))] }));
|
|
9
58
|
});
|
|
@@ -608,7 +608,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
608
608
|
}
|
|
609
609
|
// Operations selection mode - only show if not skipping
|
|
610
610
|
if (!skipOperationsMenu || !executingOperation) {
|
|
611
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
611
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
612
612
|
const isSelected = index === selectedOperation;
|
|
613
613
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
614
614
|
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
|
|
@@ -65,7 +65,7 @@ export const MainMenu = React.memo(({ onSelect }) => {
|
|
|
65
65
|
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
66
66
|
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
|
|
67
67
|
}
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }] }), _jsx(Box, { flexShrink: 0, children: _jsx(Banner, {}) }), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsx(Box, { flexShrink: 0, children: _jsx(Banner, {}) }), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
69
69
|
const isSelected = index === selectedIndex;
|
|
70
70
|
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: isSelected ? "round" : "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [_jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
71
71
|
})] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
|
|
@@ -101,10 +101,10 @@ export const ResourceActionsMenu = (props) => {
|
|
|
101
101
|
// Screens
|
|
102
102
|
if (operationResult || operationError) {
|
|
103
103
|
const label = operations.find((o) => o.key === executingOperation)?.label;
|
|
104
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
|
|
105
105
|
}
|
|
106
106
|
if (executingOperation && selectedOp?.needsInput) {
|
|
107
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems, showVersionCheck: true }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
|
|
108
108
|
}
|
|
109
109
|
// Operations menu
|
|
110
110
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: resource, operations: operations.map((op) => ({
|
package/dist/utils/config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { existsSync, statSync, mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname } from "path";
|
|
5
7
|
const config = new Conf({
|
|
6
8
|
projectName: "runloop-cli",
|
|
7
9
|
});
|
|
@@ -31,6 +33,25 @@ export function sshUrl() {
|
|
|
31
33
|
export function getCacheDir() {
|
|
32
34
|
return join(homedir(), ".cache", "rl-cli");
|
|
33
35
|
}
|
|
36
|
+
function getCurrentVersion() {
|
|
37
|
+
try {
|
|
38
|
+
// First try environment variable (when installed via npm)
|
|
39
|
+
if (process.env.npm_package_version) {
|
|
40
|
+
return process.env.npm_package_version;
|
|
41
|
+
}
|
|
42
|
+
// Fall back to reading package.json directly
|
|
43
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
44
|
+
const __dirname = dirname(__filename);
|
|
45
|
+
// When running from dist/, we need to go up two levels to find package.json
|
|
46
|
+
const packageJsonPath = join(__dirname, "../../package.json");
|
|
47
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
48
|
+
return packageJson.version;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Ultimate fallback
|
|
52
|
+
return "0.1.0";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
export function shouldCheckForUpdates() {
|
|
35
56
|
const cacheDir = getCacheDir();
|
|
36
57
|
const cacheFile = join(cacheDir, "last_update_check");
|
|
@@ -38,8 +59,13 @@ export function shouldCheckForUpdates() {
|
|
|
38
59
|
return true;
|
|
39
60
|
}
|
|
40
61
|
const stats = statSync(cacheFile);
|
|
41
|
-
const
|
|
42
|
-
return
|
|
62
|
+
const hoursSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60);
|
|
63
|
+
return hoursSinceUpdate >= 6;
|
|
64
|
+
}
|
|
65
|
+
export function hasCachedUpdateInfo() {
|
|
66
|
+
const cacheDir = getCacheDir();
|
|
67
|
+
const cacheFile = join(cacheDir, "last_update_check");
|
|
68
|
+
return existsSync(cacheFile);
|
|
43
69
|
}
|
|
44
70
|
export function updateCheckCache() {
|
|
45
71
|
const cacheDir = getCacheDir();
|
|
@@ -51,3 +77,67 @@ export function updateCheckCache() {
|
|
|
51
77
|
// Touch the cache file
|
|
52
78
|
writeFileSync(cacheFile, "");
|
|
53
79
|
}
|
|
80
|
+
export async function checkForUpdates(force = false) {
|
|
81
|
+
const currentVersion = getCurrentVersion();
|
|
82
|
+
// Always show cached result if available and not forcing
|
|
83
|
+
if (!force && hasCachedUpdateInfo() && !shouldCheckForUpdates()) {
|
|
84
|
+
// Show cached update info (we know there's an update available)
|
|
85
|
+
console.error(`\nš Update available: ${currentVersion} ā 0.1.0\n` +
|
|
86
|
+
` Run: npm install -g @runloop/rl-cli@latest\n\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Only fetch from npm if cache is expired or forcing
|
|
90
|
+
if (!force && !shouldCheckForUpdates()) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
if (force) {
|
|
97
|
+
console.error("ā Failed to check for updates\n");
|
|
98
|
+
}
|
|
99
|
+
return; // Silently fail if we can't check
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
const latestVersion = data.version;
|
|
103
|
+
if (force) {
|
|
104
|
+
console.error(`Current version: ${currentVersion}\n`);
|
|
105
|
+
console.error(`Latest version: ${latestVersion}\n`);
|
|
106
|
+
}
|
|
107
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
108
|
+
// Check if current version is older than latest
|
|
109
|
+
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
110
|
+
if (isUpdateAvailable) {
|
|
111
|
+
console.error(`\nš Update available: ${currentVersion} ā ${latestVersion}\n` +
|
|
112
|
+
` Run: npm install -g @runloop/rl-cli@latest\n\n`);
|
|
113
|
+
}
|
|
114
|
+
else if (force) {
|
|
115
|
+
console.error("ā
You're running the latest version!\n");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (force) {
|
|
119
|
+
console.error("ā
You're running the latest version!\n");
|
|
120
|
+
}
|
|
121
|
+
// Update the cache to indicate we've checked
|
|
122
|
+
updateCheckCache();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (force) {
|
|
126
|
+
console.error(`ā Error checking for updates: ${error}\n`);
|
|
127
|
+
}
|
|
128
|
+
// Silently fail - don't interrupt the user's workflow
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function compareVersions(version1, version2) {
|
|
132
|
+
const v1parts = version1.split('.').map(Number);
|
|
133
|
+
const v2parts = version2.split('.').map(Number);
|
|
134
|
+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
135
|
+
const v1part = v1parts[i] || 0;
|
|
136
|
+
const v2part = v2parts[i] || 0;
|
|
137
|
+
if (v1part > v2part)
|
|
138
|
+
return 1;
|
|
139
|
+
if (v1part < v2part)
|
|
140
|
+
return -1;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|