shellwise 0.2.6 → 0.2.9
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/README.md +18 -6
- package/package.json +1 -1
- package/src/cli/add.ts +4 -0
- package/src/cli/init.ts +8 -0
- package/src/cli/search.ts +11 -8
- package/src/daemon/server.ts +29 -5
- package/src/db/queries.ts +10 -0
- package/src/index.ts +40 -6
- package/src/search/index.ts +1 -1
- package/src/search/scorer.ts +7 -1
- package/src/tui/input.ts +5 -1
- package/src/utils/update-check.ts +87 -0
package/README.md
CHANGED
|
@@ -60,14 +60,11 @@ eval "$(shellwise init bash)"
|
|
|
60
60
|
|
|
61
61
|
## Update
|
|
62
62
|
|
|
63
|
+
`shellwise version` and `shellwise stats` will notify you when a new version is available.
|
|
64
|
+
|
|
63
65
|
```bash
|
|
64
|
-
# Homebrew
|
|
65
66
|
brew upgrade shellwise
|
|
66
|
-
|
|
67
|
-
# Bun
|
|
68
67
|
bun install -g shellwise@latest
|
|
69
|
-
|
|
70
|
-
# npm
|
|
71
68
|
npm install -g shellwise@latest
|
|
72
69
|
```
|
|
73
70
|
|
|
@@ -103,14 +100,29 @@ Both `shellwise` and `sw` work as the command name:
|
|
|
103
100
|
```bash
|
|
104
101
|
shellwise search [--query <text>] # Interactive fuzzy search (Ctrl+R)
|
|
105
102
|
shellwise suggest --query <text> # Get top suggestion (used by shell hook)
|
|
106
|
-
shellwise add
|
|
103
|
+
shellwise add <cmd> # Save a command to history
|
|
104
|
+
shellwise delete [query] # Interactive search & delete a command
|
|
107
105
|
shellwise init <zsh|bash> # Output shell integration script
|
|
108
106
|
shellwise import [zsh|bash] # Import existing shell history
|
|
109
107
|
shellwise stats # Show usage statistics
|
|
110
108
|
shellwise prune --days <n> # Remove entries older than n days
|
|
111
109
|
shellwise daemon start|stop|status # Manage background daemon
|
|
110
|
+
shellwise version # Show current version
|
|
112
111
|
```
|
|
113
112
|
|
|
113
|
+
### Delete a command
|
|
114
|
+
|
|
115
|
+
Opens an interactive picker to search and delete a command from history:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
shellwise delete # Browse all commands, pick one to delete
|
|
119
|
+
shellwise delete git # Pre-filter with "git", pick one to delete
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
1. Run the command and press `Enter` to open the picker
|
|
123
|
+
2. Type to filter results
|
|
124
|
+
3. `Tab` / `↑↓` to navigate, `Enter` to confirm deletion, `Esc` to cancel
|
|
125
|
+
|
|
114
126
|
### Import existing history
|
|
115
127
|
|
|
116
128
|
```bash
|
package/package.json
CHANGED
package/src/cli/add.ts
CHANGED
|
@@ -22,6 +22,10 @@ export function runAdd(opts: AddOptions): void {
|
|
|
22
22
|
// Only save successful commands (exit code 0)
|
|
23
23
|
if (opts.exitCode !== undefined && opts.exitCode !== 0) return;
|
|
24
24
|
|
|
25
|
+
// Skip shellwise's own commands
|
|
26
|
+
const baseCmd = cmd.split(/\s+/)[0];
|
|
27
|
+
if (baseCmd === "shellwise" || baseCmd === "sw") return;
|
|
28
|
+
|
|
25
29
|
insertCommand({
|
|
26
30
|
command: cmd,
|
|
27
31
|
cwd: opts.cwd,
|
package/src/cli/init.ts
CHANGED
|
@@ -97,6 +97,14 @@ __sw_precmd() {
|
|
|
97
97
|
--session "\$SW_SESSION_ID" \\
|
|
98
98
|
--shell "zsh" &!
|
|
99
99
|
|
|
100
|
+
# Show update notice if available
|
|
101
|
+
local __sw_line
|
|
102
|
+
for __sw_line in "\${__sw_tcp_result[@]}"; do
|
|
103
|
+
if [[ "\$__sw_line" == UPDATE$'\\t'* ]]; then
|
|
104
|
+
print -P "%F{yellow}\${__sw_line#UPDATE }%f"
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
|
|
100
108
|
unset __SW_COMMAND __SW_START_TIME
|
|
101
109
|
fi
|
|
102
110
|
}
|
package/src/cli/search.ts
CHANGED
|
@@ -25,7 +25,7 @@ interface SearchState {
|
|
|
25
25
|
renderedLines: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function pickCommand(initialQuery: string = ""): string | null {
|
|
29
29
|
const cwd = process.env.PWD || process.cwd();
|
|
30
30
|
|
|
31
31
|
const state: SearchState = {
|
|
@@ -78,22 +78,18 @@ export function runSearch(initialQuery: string = ""): void {
|
|
|
78
78
|
|
|
79
79
|
if (key.type === "special" && key.key === "escape") {
|
|
80
80
|
cleanup();
|
|
81
|
-
return;
|
|
81
|
+
return null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (key.type === "ctrl" && key.char === "c") {
|
|
85
85
|
cleanup();
|
|
86
|
-
return;
|
|
86
|
+
return null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (key.type === "special" && key.key === "enter") {
|
|
90
90
|
const selected = state.results[state.selectedIndex];
|
|
91
91
|
cleanup();
|
|
92
|
-
|
|
93
|
-
// Output to stdout (fd 1) for shell to capture
|
|
94
|
-
writeSync(1, selected.command);
|
|
95
|
-
}
|
|
96
|
-
return;
|
|
92
|
+
return selected?.command ?? null;
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
let needsSearch = false;
|
|
@@ -197,6 +193,13 @@ export function runSearch(initialQuery: string = ""): void {
|
|
|
197
193
|
}
|
|
198
194
|
}
|
|
199
195
|
|
|
196
|
+
export function runSearch(initialQuery: string = ""): void {
|
|
197
|
+
const selected = pickCommand(initialQuery);
|
|
198
|
+
if (selected) {
|
|
199
|
+
writeSync(1, selected);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
200
203
|
function getVisibleCount(): number {
|
|
201
204
|
const { rows } = getTerminalSize();
|
|
202
205
|
return Math.min(Math.max(rows - 4, 3), 15); // 3 minimum, 15 max
|
package/src/daemon/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb, closeDb } from "../db/connection";
|
|
|
2
2
|
import { insertCommand } from "../db/queries";
|
|
3
3
|
import { getHostname } from "../utils/platform";
|
|
4
4
|
import { getCommonSuggestions } from "../data/common-commands";
|
|
5
|
+
import { checkForUpdate, getUpdateNotice } from "../utils/update-check";
|
|
5
6
|
import { parseRequest, getSocketPath, getPidPath, getDaemonPort } from "./protocol";
|
|
6
7
|
import { unlinkSync, writeFileSync, existsSync } from "fs";
|
|
7
8
|
import type { Socket } from "bun";
|
|
@@ -10,6 +11,7 @@ const IDLE_TIMEOUT = 30 * 60_000; // 30 min
|
|
|
10
11
|
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
11
12
|
let server: ReturnType<typeof Bun.listen> | null = null;
|
|
12
13
|
let tcpServer: ReturnType<typeof Bun.listen> | null = null;
|
|
14
|
+
let updateNotified = false;
|
|
13
15
|
|
|
14
16
|
// Pre-warm DB + prepared statements on start
|
|
15
17
|
let suggestPrefix: ReturnType<ReturnType<typeof getDb>["prepare"]>;
|
|
@@ -57,10 +59,14 @@ function handleRequest(raw: string): string {
|
|
|
57
59
|
// Escape LIKE wildcards in user input
|
|
58
60
|
const escapedQuery = req.query.replace(/[%_\\]/g, "\\$&");
|
|
59
61
|
|
|
60
|
-
// History: prefix matches
|
|
62
|
+
// History: prefix matches (exact match first)
|
|
61
63
|
const prefixes = suggestPrefix.all(escapedQuery, historyLimit) as { command: string }[];
|
|
62
64
|
for (const r of prefixes) {
|
|
63
|
-
if (r.command
|
|
65
|
+
if (r.command === req.query) {
|
|
66
|
+
historyResults.unshift(r.command);
|
|
67
|
+
} else {
|
|
68
|
+
historyResults.push(r.command);
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
// History: contains matches (fill remaining)
|
|
@@ -71,7 +77,7 @@ function handleRequest(raw: string): string {
|
|
|
71
77
|
command: string;
|
|
72
78
|
}[];
|
|
73
79
|
for (const r of contains) {
|
|
74
|
-
if (!resultSet.has(r.command)
|
|
80
|
+
if (!resultSet.has(r.command)) {
|
|
75
81
|
historyResults.push(r.command);
|
|
76
82
|
if (historyResults.length >= historyLimit) break;
|
|
77
83
|
}
|
|
@@ -81,7 +87,7 @@ function handleRequest(raw: string): string {
|
|
|
81
87
|
// Common commands: fill with suggestions not already in history
|
|
82
88
|
const seen = new Set(historyResults);
|
|
83
89
|
const commonResults = getCommonSuggestions(req.query, 10)
|
|
84
|
-
.filter((cmd) => !seen.has(cmd)
|
|
90
|
+
.filter((cmd) => !seen.has(cmd))
|
|
85
91
|
.slice(0, 5);
|
|
86
92
|
|
|
87
93
|
// Merge: history first, then common
|
|
@@ -93,7 +99,9 @@ function handleRequest(raw: string): string {
|
|
|
93
99
|
case "ADD": {
|
|
94
100
|
const cmd = req.command.trim();
|
|
95
101
|
if (!cmd || cmd.length < 2 || cmd.startsWith(" ")) return "OK\n\n";
|
|
96
|
-
if (req.exitCode !== 0) return "OK\n\n";
|
|
102
|
+
if (req.exitCode !== 0) return "OK\n\n";
|
|
103
|
+
const baseCmd = cmd.split(/\s+/)[0];
|
|
104
|
+
if (baseCmd === "shellwise" || baseCmd === "sw") return "OK\n\n";
|
|
97
105
|
insertCommand({
|
|
98
106
|
command: cmd,
|
|
99
107
|
cwd: req.cwd || undefined,
|
|
@@ -103,6 +111,22 @@ function handleRequest(raw: string): string {
|
|
|
103
111
|
session_id: req.session || undefined,
|
|
104
112
|
shell: req.shell || undefined,
|
|
105
113
|
});
|
|
114
|
+
|
|
115
|
+
// Check update notice from cache (sync, fast, once per daemon session)
|
|
116
|
+
const pkg = require("../../package.json");
|
|
117
|
+
if (!updateNotified) {
|
|
118
|
+
const notice = getUpdateNotice(pkg.version);
|
|
119
|
+
if (notice) {
|
|
120
|
+
updateNotified = true;
|
|
121
|
+
// Refresh cache in background
|
|
122
|
+
checkForUpdate(pkg.version, true).catch(() => {});
|
|
123
|
+
return `OK\nUPDATE\t${notice}\n\n`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Refresh cache in background for next time
|
|
128
|
+
checkForUpdate(pkg.version, true).catch(() => {});
|
|
129
|
+
|
|
106
130
|
return "OK\n\n";
|
|
107
131
|
}
|
|
108
132
|
|
package/src/db/queries.ts
CHANGED
|
@@ -166,6 +166,16 @@ export function pruneOlderThan(days: number): number {
|
|
|
166
166
|
return result.changes;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export function deleteCommand(command: string): boolean {
|
|
170
|
+
const db = getDb();
|
|
171
|
+
const hash = hashCommand(command);
|
|
172
|
+
|
|
173
|
+
const result = db.run("DELETE FROM commands WHERE command_hash = ?", [hash]);
|
|
174
|
+
db.run("DELETE FROM command_stats WHERE command_hash = ?", [hash]);
|
|
175
|
+
|
|
176
|
+
return result.changes > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
export function getExistingHashes(): Set<string> {
|
|
170
180
|
const db = getDb();
|
|
171
181
|
const rows = db
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { runAdd } from "./cli/add";
|
|
4
|
-
import { runSearch } from "./cli/search";
|
|
4
|
+
import { runSearch, pickCommand } from "./cli/search";
|
|
5
5
|
import { runSuggest } from "./cli/suggest";
|
|
6
6
|
import { runInit } from "./cli/init";
|
|
7
7
|
import { runImport } from "./cli/import";
|
|
8
8
|
import { runStats } from "./cli/stats";
|
|
9
9
|
import { runPrune } from "./cli/prune";
|
|
10
|
+
import { deleteCommand } from "./db/queries";
|
|
10
11
|
import { closeDb } from "./db/connection";
|
|
12
|
+
import { checkForUpdate } from "./utils/update-check";
|
|
11
13
|
import { startServer, isDaemonRunning, getDaemonInfo } from "./daemon/server";
|
|
12
14
|
import { daemonRequest } from "./daemon/client";
|
|
13
15
|
|
|
@@ -34,12 +36,14 @@ Usage: shellwise <command> [options] (or: sw <command>)
|
|
|
34
36
|
Commands:
|
|
35
37
|
search [--query <text>] Interactive fuzzy search (Ctrl+R)
|
|
36
38
|
suggest --query <text> Get top suggestion (used by shell hook)
|
|
37
|
-
add
|
|
39
|
+
add <cmd> Save a command to history
|
|
40
|
+
delete <cmd> Delete a command from history
|
|
38
41
|
init <zsh|bash> Output shell integration script
|
|
39
42
|
import [zsh|bash] Import existing shell history
|
|
40
43
|
stats Show usage statistics
|
|
41
44
|
prune --days <n> Remove entries older than n days
|
|
42
45
|
daemon start|stop|status Manage background daemon (faster suggest)
|
|
46
|
+
version Show current version
|
|
43
47
|
|
|
44
48
|
Setup:
|
|
45
49
|
Add to ~/.zshrc: eval "$(shellwise init zsh)"
|
|
@@ -79,20 +83,21 @@ async function main(): Promise<void> {
|
|
|
79
83
|
|
|
80
84
|
case "add": {
|
|
81
85
|
const flags = parseFlags(args.slice(1));
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
const addCmd = flags.command || args.slice(1).filter(a => !a.startsWith("--")).join(" ");
|
|
87
|
+
if (!addCmd) {
|
|
88
|
+
console.error("Usage: shellwise add <cmd>");
|
|
84
89
|
process.exit(1);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
// Try daemon first
|
|
88
93
|
// Strip tabs from command to avoid breaking protocol delimiter
|
|
89
|
-
const safeCommand =
|
|
94
|
+
const safeCommand = addCmd.replace(/\t/g, " ");
|
|
90
95
|
const addMsg = `ADD\t${safeCommand}\t${flags.cwd || ""}\t${flags["exit-code"] || "0"}\t${flags.duration || "0"}\t${flags.session || ""}\t${flags.shell || ""}\n`;
|
|
91
96
|
const addResult = await daemonRequest(addMsg);
|
|
92
97
|
if (!addResult) {
|
|
93
98
|
// Fallback: direct
|
|
94
99
|
runAdd({
|
|
95
|
-
command:
|
|
100
|
+
command: addCmd,
|
|
96
101
|
cwd: flags.cwd,
|
|
97
102
|
exitCode: flags["exit-code"] ? parseInt(flags["exit-code"]) : undefined,
|
|
98
103
|
duration: flags.duration ? parseInt(flags.duration) : undefined,
|
|
@@ -185,6 +190,28 @@ async function main(): Promise<void> {
|
|
|
185
190
|
break;
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
case "delete": {
|
|
194
|
+
const delQuery = args.slice(1).join(" ");
|
|
195
|
+
const delCmd = pickCommand(delQuery);
|
|
196
|
+
if (delCmd) {
|
|
197
|
+
const deleted = deleteCommand(delCmd);
|
|
198
|
+
if (deleted) {
|
|
199
|
+
console.log(`Deleted: ${delCmd}`);
|
|
200
|
+
} else {
|
|
201
|
+
console.log("Command not found in history.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case "version":
|
|
208
|
+
case "--version":
|
|
209
|
+
case "-v": {
|
|
210
|
+
const pkg = require("../package.json");
|
|
211
|
+
console.log(pkg.version);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
case "help":
|
|
189
216
|
case "--help":
|
|
190
217
|
case "-h":
|
|
@@ -199,6 +226,13 @@ async function main(): Promise<void> {
|
|
|
199
226
|
}
|
|
200
227
|
} finally {
|
|
201
228
|
if (command !== "daemon") closeDb();
|
|
229
|
+
|
|
230
|
+
// Check for updates on interactive commands (cached, max once per day)
|
|
231
|
+
const silentCommands = new Set(["suggest", "add", "search", "init", "daemon", "_run"]);
|
|
232
|
+
if (command && !silentCommands.has(command)) {
|
|
233
|
+
const pkg = require("../package.json");
|
|
234
|
+
await checkForUpdate(pkg.version);
|
|
235
|
+
}
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
|
package/src/search/index.ts
CHANGED
|
@@ -55,5 +55,5 @@ export function search(input: SearchInput): ScoredResult[] {
|
|
|
55
55
|
cwdCommands = new Set(rows.map((r) => r.command_hash));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
return rankResults(matches, dbResults, input.cwd, cwdCommands).slice(0, limit);
|
|
58
|
+
return rankResults(matches, dbResults, input.query, input.cwd, cwdCommands).slice(0, limit);
|
|
59
59
|
}
|
package/src/search/scorer.ts
CHANGED
|
@@ -27,6 +27,7 @@ const DEFAULT_WEIGHTS: ScoreWeights = {
|
|
|
27
27
|
export function rankResults(
|
|
28
28
|
matches: FuzzyMatch[],
|
|
29
29
|
stats: CommandStats[],
|
|
30
|
+
query: string,
|
|
30
31
|
currentCwd?: string,
|
|
31
32
|
cwdCommands?: Set<string>,
|
|
32
33
|
weights: ScoreWeights = DEFAULT_WEIGHTS
|
|
@@ -65,6 +66,11 @@ export function rankResults(
|
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
results.sort((a, b) =>
|
|
69
|
+
results.sort((a, b) => {
|
|
70
|
+
const aExact = a.command.toLowerCase() === query ? 1 : 0;
|
|
71
|
+
const bExact = b.command.toLowerCase() === query ? 1 : 0;
|
|
72
|
+
if (aExact !== bExact) return bExact - aExact;
|
|
73
|
+
return b.finalScore - a.finalScore;
|
|
74
|
+
});
|
|
69
75
|
return results;
|
|
70
76
|
}
|
package/src/tui/input.ts
CHANGED
|
@@ -23,13 +23,17 @@ export type SpecialKey =
|
|
|
23
23
|
export function parseKeypress(data: Buffer): KeyEvent {
|
|
24
24
|
const str = data.toString("utf-8");
|
|
25
25
|
|
|
26
|
+
// Backspace (127 = DEL, 8 = BS)
|
|
27
|
+
if (data.length === 1 && (data[0] === 127 || data[0] === 8)) {
|
|
28
|
+
return { type: "special", key: "backspace" };
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
// Ctrl combinations
|
|
27
32
|
if (data.length === 1 && data[0] < 32) {
|
|
28
33
|
const char = data[0];
|
|
29
34
|
if (char === 13) return { type: "special", key: "enter" };
|
|
30
35
|
if (char === 9) return { type: "special", key: "tab" };
|
|
31
36
|
if (char === 27) return { type: "special", key: "escape" };
|
|
32
|
-
if (char === 127) return { type: "special", key: "backspace" };
|
|
33
37
|
// Ctrl+A = 1, Ctrl+B = 2, etc.
|
|
34
38
|
return { type: "ctrl", char: String.fromCharCode(char + 96) };
|
|
35
39
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getDataDir } from "./paths";
|
|
4
|
+
import { color } from "../tui/theme";
|
|
5
|
+
|
|
6
|
+
const CHECK_INTERVAL = 24 * 3600_000; // 1 day
|
|
7
|
+
|
|
8
|
+
interface CachedCheck {
|
|
9
|
+
latestVersion: string;
|
|
10
|
+
checkedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getCachePath(): string {
|
|
14
|
+
return join(getDataDir(), "update-check.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readCache(): CachedCheck | null {
|
|
18
|
+
const path = getCachePath();
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeCache(data: CachedCheck): void {
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(getCachePath(), JSON.stringify(data));
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compareVersions(current: string, latest: string): number {
|
|
34
|
+
const a = current.split(".").map(Number);
|
|
35
|
+
const b = latest.split(".").map(Number);
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
37
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return -1;
|
|
38
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return 1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function checkForUpdate(
|
|
44
|
+
currentVersion: string,
|
|
45
|
+
silent: boolean = false
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const cache = readCache();
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
// Cache hit — skip network, only notify if not silent
|
|
51
|
+
if (cache && now - cache.checkedAt < CHECK_INTERVAL) {
|
|
52
|
+
if (!silent && compareVersions(currentVersion, cache.latestVersion) < 0) {
|
|
53
|
+
printUpdateNotice(currentVersion, cache.latestVersion);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cache miss — fetch in background, don't block
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch("https://registry.npmjs.org/shellwise/latest", {
|
|
61
|
+
signal: AbortSignal.timeout(3000),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) return;
|
|
64
|
+
const data = (await res.json()) as { version: string };
|
|
65
|
+
writeCache({ latestVersion: data.version, checkedAt: now });
|
|
66
|
+
|
|
67
|
+
if (!silent && compareVersions(currentVersion, data.version) < 0) {
|
|
68
|
+
printUpdateNotice(currentVersion, data.version);
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getUpdateNotice(currentVersion: string): string | null {
|
|
74
|
+
const cache = readCache();
|
|
75
|
+
if (!cache) return null;
|
|
76
|
+
if (compareVersions(currentVersion, cache.latestVersion) < 0) {
|
|
77
|
+
return `Update available: ${currentVersion} → ${cache.latestVersion}. Run: brew upgrade shellwise | bun install -g shellwise@latest | npm install -g shellwise@latest`;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printUpdateNotice(current: string, latest: string): void {
|
|
83
|
+
console.log(
|
|
84
|
+
`\n${color.yellow}Update available: ${current} → ${latest}${color.reset}` +
|
|
85
|
+
`\n${color.dim}Run: brew upgrade shellwise | bun install -g shellwise@latest | npm install -g shellwise@latest${color.reset}`
|
|
86
|
+
);
|
|
87
|
+
}
|