shellwise 0.2.7 → 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 +16 -6
- package/package.json +1 -1
- package/src/cli/add.ts +4 -0
- package/src/cli/init.ts +8 -0
- package/src/daemon/server.ts +29 -5
- package/src/index.ts +8 -0
- 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
|
|
|
@@ -110,9 +107,22 @@ shellwise import [zsh|bash] # Import existing shell history
|
|
|
110
107
|
shellwise stats # Show usage statistics
|
|
111
108
|
shellwise prune --days <n> # Remove entries older than n days
|
|
112
109
|
shellwise daemon start|stop|status # Manage background daemon
|
|
113
|
-
shellwise version
|
|
110
|
+
shellwise version # Show current version
|
|
114
111
|
```
|
|
115
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
|
+
|
|
116
126
|
### Import existing history
|
|
117
127
|
|
|
118
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/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/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { runStats } from "./cli/stats";
|
|
|
9
9
|
import { runPrune } from "./cli/prune";
|
|
10
10
|
import { deleteCommand } from "./db/queries";
|
|
11
11
|
import { closeDb } from "./db/connection";
|
|
12
|
+
import { checkForUpdate } from "./utils/update-check";
|
|
12
13
|
import { startServer, isDaemonRunning, getDaemonInfo } from "./daemon/server";
|
|
13
14
|
import { daemonRequest } from "./daemon/client";
|
|
14
15
|
|
|
@@ -225,6 +226,13 @@ async function main(): Promise<void> {
|
|
|
225
226
|
}
|
|
226
227
|
} finally {
|
|
227
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
|
+
}
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
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
|
+
}
|