@runloop/rl-cli 0.1.0 → 0.1.2

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 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,53 @@ 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
- export const Breadcrumb = React.memo(({ items }) => {
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
+ // Import the utility functions from config
14
+ const { checkForUpdates: checkForUpdatesUtil } = await import("../utils/config.js");
15
+ // Use the same logic as the non-interactive version
16
+ // We'll call the utility function and capture its output
17
+ const originalConsoleError = console.error;
18
+ let updateMessage = "";
19
+ // Capture the console.error output
20
+ console.error = (...args) => {
21
+ updateMessage = args.join(' ');
22
+ originalConsoleError(...args);
23
+ };
24
+ // Call the update check utility
25
+ await checkForUpdatesUtil(false);
26
+ // Restore original console.error
27
+ console.error = originalConsoleError;
28
+ // Parse the update message to extract the latest version
29
+ if (updateMessage.includes("Update available:")) {
30
+ const match = updateMessage.match(/Update available: .+ → (.+)/);
31
+ if (match && match[1]) {
32
+ setUpdateAvailable(match[1]);
33
+ }
34
+ }
35
+ }
36
+ catch (error) {
37
+ // Silently fail
38
+ }
39
+ finally {
40
+ setIsChecking(false);
41
+ }
42
+ };
43
+ checkForUpdates();
44
+ }, []);
45
+ if (isChecking || !updateAvailable) {
46
+ return null;
47
+ }
48
+ 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" })] }));
49
+ };
50
+ export const Breadcrumb = React.memo(({ items, showVersionCheck = false }) => {
6
51
  const env = process.env.RUNLOOP_ENV?.toLowerCase();
7
52
  const isDevEnvironment = env === "dev";
8
- return (_jsx(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, 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)))] }) }));
53
+ 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
54
  });
@@ -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) => ({
@@ -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
+ export 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,16 +59,104 @@ export function shouldCheckForUpdates() {
38
59
  return true;
39
60
  }
40
61
  const stats = statSync(cacheFile);
41
- const daysSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
42
- return daysSinceUpdate >= 1;
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
- export function updateCheckCache() {
70
+ export function updateCheckCache(latestVersion) {
45
71
  const cacheDir = getCacheDir();
46
72
  const cacheFile = join(cacheDir, "last_update_check");
47
73
  // Create cache directory if it doesn't exist
48
74
  if (!existsSync(cacheDir)) {
49
75
  mkdirSync(cacheDir, { recursive: true });
50
76
  }
51
- // Touch the cache file
52
- writeFileSync(cacheFile, "");
77
+ // Store the latest version in the cache file
78
+ writeFileSync(cacheFile, latestVersion);
79
+ }
80
+ export function getCachedLatestVersion() {
81
+ const cacheDir = getCacheDir();
82
+ const cacheFile = join(cacheDir, "last_update_check");
83
+ if (!existsSync(cacheFile)) {
84
+ return null;
85
+ }
86
+ try {
87
+ return readFileSync(cacheFile, 'utf-8').trim();
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ export async function checkForUpdates(force = false) {
94
+ const currentVersion = getCurrentVersion();
95
+ // Always show cached result if available and not forcing
96
+ if (!force && hasCachedUpdateInfo() && !shouldCheckForUpdates()) {
97
+ const cachedLatestVersion = getCachedLatestVersion();
98
+ if (cachedLatestVersion && cachedLatestVersion !== currentVersion) {
99
+ // Check if current version is older than cached latest
100
+ const isUpdateAvailable = compareVersions(cachedLatestVersion, currentVersion) > 0;
101
+ if (isUpdateAvailable) {
102
+ console.error(`\nšŸ”„ Update available: ${currentVersion} → ${cachedLatestVersion}\n` +
103
+ ` Run: npm install -g @runloop/rl-cli@latest\n\n`);
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ // Only fetch from npm if cache is expired or forcing
109
+ if (!force && !shouldCheckForUpdates()) {
110
+ return;
111
+ }
112
+ try {
113
+ const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
114
+ if (!response.ok) {
115
+ if (force) {
116
+ console.error("āŒ Failed to check for updates\n");
117
+ }
118
+ return; // Silently fail if we can't check
119
+ }
120
+ const data = await response.json();
121
+ const latestVersion = data.version;
122
+ if (force) {
123
+ console.error(`Current version: ${currentVersion}\n`);
124
+ console.error(`Latest version: ${latestVersion}\n`);
125
+ }
126
+ if (latestVersion && latestVersion !== currentVersion) {
127
+ // Check if current version is older than latest
128
+ const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
129
+ if (isUpdateAvailable) {
130
+ console.error(`\nšŸ”„ Update available: ${currentVersion} → ${latestVersion}\n` +
131
+ ` Run: npm install -g @runloop/rl-cli@latest\n\n`);
132
+ }
133
+ else if (force) {
134
+ console.error("āœ… You're running the latest version!\n");
135
+ }
136
+ }
137
+ else if (force) {
138
+ console.error("āœ… You're running the latest version!\n");
139
+ }
140
+ // Update the cache with the latest version
141
+ updateCheckCache(latestVersion);
142
+ }
143
+ catch (error) {
144
+ if (force) {
145
+ console.error(`āŒ Error checking for updates: ${error}\n`);
146
+ }
147
+ // Silently fail - don't interrupt the user's workflow
148
+ }
149
+ }
150
+ function compareVersions(version1, version2) {
151
+ const v1parts = version1.split('.').map(Number);
152
+ const v2parts = version2.split('.').map(Number);
153
+ for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
154
+ const v1part = v1parts[i] || 0;
155
+ const v2part = v2parts[i] || 0;
156
+ if (v1part > v2part)
157
+ return 1;
158
+ if (v1part < v2part)
159
+ return -1;
160
+ }
161
+ return 0;
53
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Beautiful CLI for Runloop devbox management",
5
5
  "type": "module",
6
6
  "bin": {