Skip to content

Commit 4cd3920

Browse files
authored
Merge pull request #12 from shelltime/feat/cli-version-check
feat(version): add CLI version check on extension startup
2 parents 56bd140 + fc67b4a commit 4cd3920

5 files changed

Lines changed: 767 additions & 1 deletion

File tree

lua/shelltime/config.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ local defaults = {
1111
heartbeat_interval = 120000, -- 2 minutes in ms
1212
debounce_interval = 30000, -- 30 seconds in ms
1313
debug = false,
14+
api_endpoint = nil, -- API endpoint for version check
15+
web_endpoint = nil, -- Web endpoint for update command
1416
}
1517

1618
-- Default config file path
@@ -20,6 +22,7 @@ local default_config_path = '~/.shelltime/config.yaml'
2022
local cached_config = nil
2123
local cached_mtime = nil
2224
local config_path = nil
25+
local test_mode = false
2326

2427
--- Expand tilde in path
2528
---@param path string Path with possible tilde
@@ -89,6 +92,15 @@ local function merge_config(file_config)
8992
config.debounce_interval = file_config.debounceInterval
9093
end
9194

95+
-- API and web endpoints for version check
96+
if file_config.apiEndpoint then
97+
config.api_endpoint = file_config.apiEndpoint
98+
end
99+
100+
if file_config.webEndpoint then
101+
config.web_endpoint = file_config.webEndpoint
102+
end
103+
92104
return config
93105
end
94106

@@ -101,11 +113,17 @@ function M.setup(opts)
101113
-- Force reload
102114
cached_config = nil
103115
cached_mtime = nil
116+
test_mode = false
104117
end
105118

106119
--- Get merged configuration
107120
---@return table Configuration
108121
function M.get_config()
122+
-- In test mode, always return cached config
123+
if test_mode and cached_config then
124+
return cached_config
125+
end
126+
109127
local path = expand_path(config_path or default_config_path)
110128
local mtime = get_mtime(path)
111129

@@ -142,4 +160,11 @@ function M.get_config_path()
142160
return config_path or default_config_path
143161
end
144162

163+
--- Set config values directly (for testing only)
164+
---@param config_values table Config values to set
165+
function M._set_for_testing(config_values)
166+
cached_config = vim.tbl_deep_extend('force', {}, defaults, config_values)
167+
test_mode = true
168+
end
169+
145170
return M

lua/shelltime/sender.lua

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
local config = require('shelltime.config')
44
local socket = require('shelltime.socket')
55
local heartbeat = require('shelltime.heartbeat')
6+
local version = require('shelltime.version')
67

78
local M = {}
89

@@ -63,9 +64,18 @@ function M.start()
6364
end)
6465
end)
6566

66-
-- Check initial connection status
67+
-- Check initial connection status and CLI version
6768
vim.schedule(function()
6869
is_connected = socket.is_connected_sync()
70+
71+
-- Check CLI version in background (non-blocking)
72+
if is_connected then
73+
socket.get_status(function(status, err)
74+
if status and status.version then
75+
version.check_version(status.version)
76+
end
77+
end)
78+
end
6979
end)
7080
end
7181

lua/shelltime/version.lua

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
-- Version checker for shelltime
2+
3+
local config = require('shelltime.config')
4+
5+
local M = {}
6+
7+
-- Track if warning has been shown this session
8+
local has_shown_warning = false
9+
10+
-- Version check API endpoint path
11+
local VERSION_CHECK_ENDPOINT = '/api/v1/cli/version-check'
12+
13+
--- URL encode a string
14+
---@param str string String to encode
15+
---@return string Encoded string
16+
local function url_encode(str)
17+
if str then
18+
str = string.gsub(str, '\n', '\r\n')
19+
str = string.gsub(str, '([^%w _%%%-%.~])', function(c)
20+
return string.format('%%%02X', string.byte(c))
21+
end)
22+
str = string.gsub(str, ' ', '+')
23+
end
24+
return str
25+
end
26+
27+
--- Parse JSON response using Neovim's built-in JSON decoder
28+
---@param json_str string JSON string
29+
---@return table|nil Parsed table or nil on error
30+
local function parse_json(json_str)
31+
local ok, result = pcall(vim.json.decode, json_str)
32+
if not ok or type(result) ~= 'table' then
33+
return nil
34+
end
35+
return result
36+
end
37+
38+
--- Check CLI version against server
39+
---@param daemon_version string Current daemon version
40+
---@param callback function|nil Optional callback(result, error)
41+
function M.check_version(daemon_version, callback)
42+
local api_endpoint = config.get('api_endpoint')
43+
local web_endpoint = config.get('web_endpoint')
44+
45+
if not api_endpoint or not web_endpoint then
46+
if config.get('debug') then
47+
vim.notify('[shelltime] No API/web endpoint configured, skipping version check', vim.log.levels.DEBUG)
48+
end
49+
if callback then
50+
callback(nil, 'No endpoint configured')
51+
end
52+
return
53+
end
54+
55+
if has_shown_warning then
56+
if config.get('debug') then
57+
vim.notify('[shelltime] Version warning already shown this session', vim.log.levels.DEBUG)
58+
end
59+
if callback then
60+
callback(nil, 'Already shown')
61+
end
62+
return
63+
end
64+
65+
local url = api_endpoint .. VERSION_CHECK_ENDPOINT .. '?version=' .. url_encode(daemon_version)
66+
67+
if config.get('debug') then
68+
vim.notify('[shelltime] Checking version at: ' .. url, vim.log.levels.DEBUG)
69+
end
70+
71+
-- Use curl asynchronously via vim.fn.jobstart
72+
local stdout_data = {}
73+
74+
vim.fn.jobstart({ 'curl', '-sSL', '-m', '5', '-H', 'Accept: application/json', url }, {
75+
stdout_buffered = true,
76+
on_stdout = function(_, data)
77+
if data then
78+
for _, line in ipairs(data) do
79+
if line ~= '' then
80+
table.insert(stdout_data, line)
81+
end
82+
end
83+
end
84+
end,
85+
on_exit = function(_, exit_code)
86+
vim.schedule(function()
87+
if exit_code ~= 0 then
88+
if config.get('debug') then
89+
vim.notify('[shelltime] Version check failed with exit code: ' .. exit_code, vim.log.levels.DEBUG)
90+
end
91+
if callback then
92+
callback(nil, 'curl failed')
93+
end
94+
return
95+
end
96+
97+
local response = table.concat(stdout_data, '')
98+
local result = parse_json(response)
99+
100+
if result then
101+
if not result.isLatest then
102+
has_shown_warning = true
103+
M.show_update_warning(daemon_version, result.latestVersion, web_endpoint)
104+
elseif config.get('debug') then
105+
vim.notify('[shelltime] CLI version ' .. daemon_version .. ' is up to date', vim.log.levels.DEBUG)
106+
end
107+
108+
if callback then
109+
callback(result, nil)
110+
end
111+
else
112+
if config.get('debug') then
113+
vim.notify('[shelltime] Failed to parse version check response', vim.log.levels.DEBUG)
114+
end
115+
if callback then
116+
callback(nil, 'Parse error')
117+
end
118+
end
119+
end)
120+
end,
121+
})
122+
end
123+
124+
--- Show update warning notification
125+
---@param current_version string Current version
126+
---@param latest_version string Latest available version
127+
---@param web_endpoint string Web endpoint for update command
128+
function M.show_update_warning(current_version, latest_version, web_endpoint)
129+
local update_command = 'curl -sSL ' .. web_endpoint .. '/i | bash'
130+
local message = string.format(
131+
'[shelltime] CLI update available: %s -> %s\n\nRun: %s',
132+
current_version,
133+
latest_version,
134+
update_command
135+
)
136+
137+
vim.notify(message, vim.log.levels.WARN)
138+
139+
-- Also copy to clipboard if available
140+
if vim.fn.has('clipboard') == 1 then
141+
vim.fn.setreg('+', update_command)
142+
vim.notify('[shelltime] Update command copied to clipboard', vim.log.levels.INFO)
143+
end
144+
end
145+
146+
return M

tests/helpers/reset.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function M.reset_all()
1616
'shelltime.heartbeat',
1717
'shelltime.sender',
1818
'shelltime.socket',
19+
'shelltime.version',
1920
'shelltime.utils',
2021
'shelltime.utils.system',
2122
'shelltime.utils.git',

0 commit comments

Comments
 (0)