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 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 # Show current 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellwise",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Smart command history with inline auto-suggest and fuzzy search for your terminal",
5
5
  "type": "module",
6
6
  "bin": {
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
  }
@@ -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 !== req.query) historyResults.push(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) && r.command !== req.query) {
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) && cmd !== req.query)
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"; // Only save successful commands
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
 
@@ -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
  }
@@ -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) => b.finalScore - a.finalScore);
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
+ }