shellwise 0.1.0
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 +141 -0
- package/bin/sw.js +16 -0
- package/package.json +46 -0
- package/scripts/setup.sh +92 -0
- package/scripts/uninstall.sh +36 -0
- package/src/cli/add.ts +40 -0
- package/src/cli/import.ts +76 -0
- package/src/cli/init.ts +325 -0
- package/src/cli/prune.ts +6 -0
- package/src/cli/search.ts +291 -0
- package/src/cli/stats.ts +42 -0
- package/src/cli/suggest.ts +60 -0
- package/src/daemon/client.ts +41 -0
- package/src/daemon/protocol.ts +89 -0
- package/src/daemon/server.ts +222 -0
- package/src/data/common-commands.ts +276 -0
- package/src/db/connection.ts +29 -0
- package/src/db/queries.ts +181 -0
- package/src/db/schema.ts +58 -0
- package/src/index.ts +207 -0
- package/src/search/fuzzy.ts +77 -0
- package/src/search/index.ts +59 -0
- package/src/search/scorer.ts +71 -0
- package/src/tui/components/result-list.ts +106 -0
- package/src/tui/components/search-box.ts +20 -0
- package/src/tui/components/status-bar.ts +10 -0
- package/src/tui/input.ts +71 -0
- package/src/tui/renderer.ts +45 -0
- package/src/tui/theme.ts +34 -0
- package/src/utils/paths.ts +27 -0
- package/src/utils/platform.ts +13 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getDb } from "../db/connection";
|
|
2
|
+
import { getCommonSuggestions } from "../data/common-commands";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Suggest: 5 history + 5 common commands, deduplicated.
|
|
6
|
+
*/
|
|
7
|
+
export function runSuggest(query: string, limit: number = 5): void {
|
|
8
|
+
if (!query || query.length < 2) return;
|
|
9
|
+
|
|
10
|
+
const db = getDb();
|
|
11
|
+
const historyResults: string[] = [];
|
|
12
|
+
|
|
13
|
+
// History: prefix matches
|
|
14
|
+
const prefixes = db
|
|
15
|
+
.query<{ command: string }, [string, number]>(
|
|
16
|
+
`SELECT command FROM command_stats
|
|
17
|
+
WHERE command LIKE ? || '%'
|
|
18
|
+
ORDER BY frecency_score DESC
|
|
19
|
+
LIMIT ?`
|
|
20
|
+
)
|
|
21
|
+
.all(query, limit);
|
|
22
|
+
|
|
23
|
+
for (const r of prefixes) {
|
|
24
|
+
if (r.command !== query) historyResults.push(r.command);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// History: contains matches (fill remaining)
|
|
28
|
+
if (historyResults.length < limit) {
|
|
29
|
+
const remaining = limit - historyResults.length;
|
|
30
|
+
const resultSet = new Set(historyResults);
|
|
31
|
+
|
|
32
|
+
const contains = db
|
|
33
|
+
.query<{ command: string }, [string, string, number]>(
|
|
34
|
+
`SELECT command FROM command_stats
|
|
35
|
+
WHERE command LIKE '%' || ? || '%' AND command != ?
|
|
36
|
+
ORDER BY frecency_score DESC
|
|
37
|
+
LIMIT ?`
|
|
38
|
+
)
|
|
39
|
+
.all(query, query, remaining + historyResults.length);
|
|
40
|
+
|
|
41
|
+
for (const r of contains) {
|
|
42
|
+
if (!resultSet.has(r.command) && r.command !== query) {
|
|
43
|
+
historyResults.push(r.command);
|
|
44
|
+
if (historyResults.length >= limit) break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Common commands (deduplicated)
|
|
50
|
+
const seen = new Set(historyResults);
|
|
51
|
+
const commonResults = getCommonSuggestions(query, 10)
|
|
52
|
+
.filter((cmd) => !seen.has(cmd) && cmd !== query)
|
|
53
|
+
.slice(0, 5);
|
|
54
|
+
|
|
55
|
+
const merged = [...historyResults, ...commonResults];
|
|
56
|
+
|
|
57
|
+
if (merged.length > 0) {
|
|
58
|
+
process.stdout.write(merged.join("\n"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getSocketPath } from "./protocol";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { connect } from "net";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send request to daemon via Unix socket.
|
|
7
|
+
* Returns response lines or null if daemon unavailable.
|
|
8
|
+
*/
|
|
9
|
+
export function daemonRequest(message: string, timeoutMs: number = 500): Promise<string[] | null> {
|
|
10
|
+
const socketPath = getSocketPath();
|
|
11
|
+
if (!existsSync(socketPath)) return Promise.resolve(null);
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const socket = connect(socketPath);
|
|
15
|
+
let data = "";
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
socket.destroy();
|
|
18
|
+
resolve(null);
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
|
|
21
|
+
socket.on("connect", () => {
|
|
22
|
+
socket.write(message);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
socket.on("data", (chunk) => {
|
|
26
|
+
data += chunk.toString();
|
|
27
|
+
// Response ends with double newline
|
|
28
|
+
if (data.includes("\n\n")) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
socket.destroy();
|
|
31
|
+
const lines = data.trim().split("\n").filter(Boolean);
|
|
32
|
+
resolve(lines);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
socket.on("error", () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve(null);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple text protocol over Unix socket.
|
|
3
|
+
* Request: COMMAND\targ1\targ2\n
|
|
4
|
+
* Response: line1\nline2\n\n (empty line = end)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type RequestType = "SUGGEST" | "ADD" | "STOP" | "PING";
|
|
8
|
+
|
|
9
|
+
export interface SuggestRequest {
|
|
10
|
+
type: "SUGGEST";
|
|
11
|
+
query: string;
|
|
12
|
+
limit: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AddRequest {
|
|
16
|
+
type: "ADD";
|
|
17
|
+
command: string;
|
|
18
|
+
cwd: string;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
duration: number;
|
|
21
|
+
session: string;
|
|
22
|
+
shell: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StopRequest {
|
|
26
|
+
type: "STOP";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PingRequest {
|
|
30
|
+
type: "PING";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Request = SuggestRequest | AddRequest | StopRequest | PingRequest;
|
|
34
|
+
|
|
35
|
+
export function serializeRequest(req: Request): string {
|
|
36
|
+
switch (req.type) {
|
|
37
|
+
case "SUGGEST":
|
|
38
|
+
return `SUGGEST\t${req.query}\t${req.limit}\n`;
|
|
39
|
+
case "ADD":
|
|
40
|
+
return `ADD\t${req.command}\t${req.cwd}\t${req.exitCode}\t${req.duration}\t${req.session}\t${req.shell}\n`;
|
|
41
|
+
case "STOP":
|
|
42
|
+
return `STOP\n`;
|
|
43
|
+
case "PING":
|
|
44
|
+
return `PING\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseRequest(raw: string): Request | null {
|
|
49
|
+
const line = raw.trim();
|
|
50
|
+
const parts = line.split("\t");
|
|
51
|
+
const type = parts[0] as RequestType;
|
|
52
|
+
|
|
53
|
+
switch (type) {
|
|
54
|
+
case "SUGGEST":
|
|
55
|
+
return { type: "SUGGEST", query: parts[1] || "", limit: parseInt(parts[2]) || 5 };
|
|
56
|
+
case "ADD":
|
|
57
|
+
return {
|
|
58
|
+
type: "ADD",
|
|
59
|
+
command: parts[1] || "",
|
|
60
|
+
cwd: parts[2] || "",
|
|
61
|
+
exitCode: parseInt(parts[3]) || 0,
|
|
62
|
+
duration: parseInt(parts[4]) || 0,
|
|
63
|
+
session: parts[5] || "",
|
|
64
|
+
shell: parts[6] || "",
|
|
65
|
+
};
|
|
66
|
+
case "STOP":
|
|
67
|
+
return { type: "STOP" };
|
|
68
|
+
case "PING":
|
|
69
|
+
return { type: "PING" };
|
|
70
|
+
default:
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getSocketPath(): string {
|
|
76
|
+
const uid = process.getuid?.() ?? process.pid;
|
|
77
|
+
return `/tmp/shellwise-${uid}.sock`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** TCP port = 19850 + (uid % 100) to avoid collisions */
|
|
81
|
+
export function getDaemonPort(): number {
|
|
82
|
+
const uid = process.getuid?.() ?? 501;
|
|
83
|
+
return 19850 + (uid % 100);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getPidPath(): string {
|
|
87
|
+
const uid = process.getuid?.() ?? process.pid;
|
|
88
|
+
return `/tmp/shellwise-${uid}.pid`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { getDb, closeDb } from "../db/connection";
|
|
2
|
+
import { insertCommand } from "../db/queries";
|
|
3
|
+
import { getHostname } from "../utils/platform";
|
|
4
|
+
import { getCommonSuggestions } from "../data/common-commands";
|
|
5
|
+
import { parseRequest, getSocketPath, getPidPath, getDaemonPort } from "./protocol";
|
|
6
|
+
import { unlinkSync, writeFileSync, existsSync } from "fs";
|
|
7
|
+
import type { Socket } from "bun";
|
|
8
|
+
|
|
9
|
+
const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);
|
|
10
|
+
const IDLE_TIMEOUT = 30 * 60_000; // 30 min
|
|
11
|
+
|
|
12
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
let server: ReturnType<typeof Bun.listen> | null = null;
|
|
14
|
+
|
|
15
|
+
// Pre-warm DB + prepared statements on start
|
|
16
|
+
let suggestPrefix: ReturnType<ReturnType<typeof getDb>["prepare"]>;
|
|
17
|
+
let suggestContains: ReturnType<ReturnType<typeof getDb>["prepare"]>;
|
|
18
|
+
|
|
19
|
+
function initPreparedStatements() {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
suggestPrefix = db.prepare(
|
|
22
|
+
`SELECT command FROM command_stats
|
|
23
|
+
WHERE command LIKE ?1 || '%'
|
|
24
|
+
ORDER BY frecency_score DESC
|
|
25
|
+
LIMIT ?2`
|
|
26
|
+
);
|
|
27
|
+
suggestContains = db.prepare(
|
|
28
|
+
`SELECT command FROM command_stats
|
|
29
|
+
WHERE command LIKE '%' || ?1 || '%' AND command != ?1
|
|
30
|
+
ORDER BY frecency_score DESC
|
|
31
|
+
LIMIT ?2`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resetIdleTimer() {
|
|
36
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
37
|
+
idleTimer = setTimeout(() => {
|
|
38
|
+
stopServer();
|
|
39
|
+
}, IDLE_TIMEOUT);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleRequest(raw: string): string {
|
|
43
|
+
const req = parseRequest(raw);
|
|
44
|
+
if (!req) return "\n";
|
|
45
|
+
|
|
46
|
+
resetIdleTimer();
|
|
47
|
+
|
|
48
|
+
switch (req.type) {
|
|
49
|
+
case "PING":
|
|
50
|
+
return "PONG\n\n";
|
|
51
|
+
|
|
52
|
+
case "SUGGEST": {
|
|
53
|
+
if (!req.query || req.query.length < 2) return "\n";
|
|
54
|
+
|
|
55
|
+
const historyResults: string[] = [];
|
|
56
|
+
const historyLimit = 5;
|
|
57
|
+
|
|
58
|
+
// History: prefix matches
|
|
59
|
+
const prefixes = suggestPrefix.all(req.query, historyLimit) as { command: string }[];
|
|
60
|
+
for (const r of prefixes) {
|
|
61
|
+
if (r.command !== req.query) historyResults.push(r.command);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// History: contains matches (fill remaining)
|
|
65
|
+
if (historyResults.length < historyLimit) {
|
|
66
|
+
const remaining = historyLimit - historyResults.length;
|
|
67
|
+
const resultSet = new Set(historyResults);
|
|
68
|
+
const contains = suggestContains.all(req.query, remaining + historyResults.length) as {
|
|
69
|
+
command: string;
|
|
70
|
+
}[];
|
|
71
|
+
for (const r of contains) {
|
|
72
|
+
if (!resultSet.has(r.command) && r.command !== req.query) {
|
|
73
|
+
historyResults.push(r.command);
|
|
74
|
+
if (historyResults.length >= historyLimit) break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Common commands: fill with suggestions not already in history
|
|
80
|
+
const seen = new Set(historyResults);
|
|
81
|
+
const commonResults = getCommonSuggestions(req.query, 10)
|
|
82
|
+
.filter((cmd) => !seen.has(cmd) && cmd !== req.query)
|
|
83
|
+
.slice(0, 5);
|
|
84
|
+
|
|
85
|
+
// Merge: history first, then common
|
|
86
|
+
const merged = [...historyResults, ...commonResults];
|
|
87
|
+
|
|
88
|
+
return merged.length > 0 ? merged.join("\n") + "\n\n" : "\n";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "ADD": {
|
|
92
|
+
const cmd = req.command.trim();
|
|
93
|
+
if (!cmd || cmd.length < 2 || cmd.startsWith(" ")) return "OK\n\n";
|
|
94
|
+
if (req.exitCode !== 0) return "OK\n\n"; // Only save successful commands
|
|
95
|
+
const baseCmd = cmd.split(/\s+/)[0];
|
|
96
|
+
if (IGNORED_COMMANDS.has(baseCmd)) return "OK\n\n";
|
|
97
|
+
|
|
98
|
+
insertCommand({
|
|
99
|
+
command: cmd,
|
|
100
|
+
cwd: req.cwd || undefined,
|
|
101
|
+
exit_code: req.exitCode,
|
|
102
|
+
duration_ms: req.duration,
|
|
103
|
+
hostname: getHostname(),
|
|
104
|
+
session_id: req.session || undefined,
|
|
105
|
+
shell: req.shell || undefined,
|
|
106
|
+
});
|
|
107
|
+
return "OK\n\n";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "STOP":
|
|
111
|
+
setTimeout(() => stopServer(), 50);
|
|
112
|
+
return "BYE\n\n";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function startServer(): void {
|
|
117
|
+
const socketPath = getSocketPath();
|
|
118
|
+
const pidPath = getPidPath();
|
|
119
|
+
|
|
120
|
+
// Cleanup stale socket
|
|
121
|
+
if (existsSync(socketPath)) {
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(socketPath);
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Init DB + prepared statements
|
|
128
|
+
initPreparedStatements();
|
|
129
|
+
|
|
130
|
+
const socketHandlers = {
|
|
131
|
+
data(socket: Socket, data: Buffer) {
|
|
132
|
+
const lines = data.toString().split("\n");
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
if (line.trim()) {
|
|
135
|
+
const response = handleRequest(line);
|
|
136
|
+
socket.write(response);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
open() {},
|
|
141
|
+
close() {},
|
|
142
|
+
error(_socket: Socket, error: Error) {
|
|
143
|
+
console.error("[shellwise daemon] socket error:", error.message);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Listen on both Unix socket and TCP (for ztcp from zsh)
|
|
148
|
+
server = Bun.listen({
|
|
149
|
+
unix: socketPath,
|
|
150
|
+
socket: socketHandlers,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const port = getDaemonPort();
|
|
154
|
+
Bun.listen({
|
|
155
|
+
hostname: "127.0.0.1",
|
|
156
|
+
port,
|
|
157
|
+
socket: socketHandlers,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Save PID + port
|
|
161
|
+
writeFileSync(pidPath, `${process.pid}\n${port}`);
|
|
162
|
+
|
|
163
|
+
resetIdleTimer();
|
|
164
|
+
|
|
165
|
+
// Cleanup on exit
|
|
166
|
+
const cleanup = () => {
|
|
167
|
+
stopServer();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
};
|
|
170
|
+
process.on("SIGTERM", cleanup);
|
|
171
|
+
process.on("SIGINT", cleanup);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function stopServer(): void {
|
|
175
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
176
|
+
if (server) {
|
|
177
|
+
server.stop(true);
|
|
178
|
+
server = null;
|
|
179
|
+
}
|
|
180
|
+
closeDb();
|
|
181
|
+
|
|
182
|
+
const socketPath = getSocketPath();
|
|
183
|
+
const pidPath = getPidPath();
|
|
184
|
+
try {
|
|
185
|
+
unlinkSync(socketPath);
|
|
186
|
+
} catch {}
|
|
187
|
+
try {
|
|
188
|
+
unlinkSync(pidPath);
|
|
189
|
+
} catch {}
|
|
190
|
+
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function isDaemonRunning(): boolean {
|
|
195
|
+
const pidPath = getPidPath();
|
|
196
|
+
if (!existsSync(pidPath)) return false;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const content = require("fs").readFileSync(pidPath, "utf-8").trim();
|
|
200
|
+
const pid = parseInt(content.split("\n")[0]);
|
|
201
|
+
process.kill(pid, 0); // Check if process exists
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
// Stale PID file
|
|
205
|
+
try {
|
|
206
|
+
unlinkSync(pidPath);
|
|
207
|
+
} catch {}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getDaemonInfo(): { pid: number; port: number } | null {
|
|
213
|
+
const pidPath = getPidPath();
|
|
214
|
+
if (!existsSync(pidPath)) return null;
|
|
215
|
+
try {
|
|
216
|
+
const content = require("fs").readFileSync(pidPath, "utf-8").trim();
|
|
217
|
+
const lines = content.split("\n");
|
|
218
|
+
return { pid: parseInt(lines[0]), port: parseInt(lines[1]) };
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common commands grouped by prefix.
|
|
3
|
+
* Used to suggest commands even when user has no history.
|
|
4
|
+
*/
|
|
5
|
+
const COMMANDS: Record<string, string[]> = {
|
|
6
|
+
git: [
|
|
7
|
+
"git status",
|
|
8
|
+
"git add .",
|
|
9
|
+
"git commit -m ''",
|
|
10
|
+
"git push",
|
|
11
|
+
"git pull",
|
|
12
|
+
"git branch",
|
|
13
|
+
"git checkout",
|
|
14
|
+
"git diff",
|
|
15
|
+
"git log --oneline",
|
|
16
|
+
"git stash",
|
|
17
|
+
"git merge",
|
|
18
|
+
"git rebase",
|
|
19
|
+
"git fetch",
|
|
20
|
+
"git clone",
|
|
21
|
+
"git reset --soft HEAD~1",
|
|
22
|
+
"git cherry-pick",
|
|
23
|
+
"git remote -v",
|
|
24
|
+
"git tag",
|
|
25
|
+
],
|
|
26
|
+
npm: [
|
|
27
|
+
"npm install",
|
|
28
|
+
"npm run dev",
|
|
29
|
+
"npm run build",
|
|
30
|
+
"npm run test",
|
|
31
|
+
"npm run start",
|
|
32
|
+
"npm init -y",
|
|
33
|
+
"npm update",
|
|
34
|
+
"npm outdated",
|
|
35
|
+
"npm list --depth=0",
|
|
36
|
+
"npm publish",
|
|
37
|
+
"npm link",
|
|
38
|
+
"npm uninstall",
|
|
39
|
+
"npm cache clean --force",
|
|
40
|
+
"npm audit fix",
|
|
41
|
+
],
|
|
42
|
+
npx: [
|
|
43
|
+
"npx create-next-app@latest",
|
|
44
|
+
"npx create-react-app",
|
|
45
|
+
"npx prisma generate",
|
|
46
|
+
"npx prisma migrate dev",
|
|
47
|
+
"npx tsc --noEmit",
|
|
48
|
+
"npx eslint .",
|
|
49
|
+
"npx prettier --write .",
|
|
50
|
+
],
|
|
51
|
+
bun: [
|
|
52
|
+
"bun install",
|
|
53
|
+
"bun run dev",
|
|
54
|
+
"bun run build",
|
|
55
|
+
"bun test",
|
|
56
|
+
"bun add",
|
|
57
|
+
"bun remove",
|
|
58
|
+
"bun init",
|
|
59
|
+
"bun upgrade",
|
|
60
|
+
"bun run start",
|
|
61
|
+
"bun link",
|
|
62
|
+
"bun build --compile",
|
|
63
|
+
],
|
|
64
|
+
docker: [
|
|
65
|
+
"docker compose up -d",
|
|
66
|
+
"docker compose down",
|
|
67
|
+
"docker compose logs -f",
|
|
68
|
+
"docker compose build",
|
|
69
|
+
"docker compose ps",
|
|
70
|
+
"docker ps",
|
|
71
|
+
"docker images",
|
|
72
|
+
"docker build -t",
|
|
73
|
+
"docker run -it",
|
|
74
|
+
"docker exec -it",
|
|
75
|
+
"docker stop",
|
|
76
|
+
"docker rm",
|
|
77
|
+
"docker rmi",
|
|
78
|
+
"docker system prune -f",
|
|
79
|
+
"docker volume ls",
|
|
80
|
+
"docker network ls",
|
|
81
|
+
],
|
|
82
|
+
yarn: [
|
|
83
|
+
"yarn install",
|
|
84
|
+
"yarn dev",
|
|
85
|
+
"yarn build",
|
|
86
|
+
"yarn test",
|
|
87
|
+
"yarn add",
|
|
88
|
+
"yarn remove",
|
|
89
|
+
"yarn upgrade",
|
|
90
|
+
"yarn start",
|
|
91
|
+
],
|
|
92
|
+
pnpm: [
|
|
93
|
+
"pnpm install",
|
|
94
|
+
"pnpm run dev",
|
|
95
|
+
"pnpm run build",
|
|
96
|
+
"pnpm add",
|
|
97
|
+
"pnpm remove",
|
|
98
|
+
"pnpm dlx",
|
|
99
|
+
],
|
|
100
|
+
brew: [
|
|
101
|
+
"brew install",
|
|
102
|
+
"brew update",
|
|
103
|
+
"brew upgrade",
|
|
104
|
+
"brew list",
|
|
105
|
+
"brew search",
|
|
106
|
+
"brew uninstall",
|
|
107
|
+
"brew cleanup",
|
|
108
|
+
"brew info",
|
|
109
|
+
"brew doctor",
|
|
110
|
+
"brew services list",
|
|
111
|
+
"brew services start",
|
|
112
|
+
"brew services stop",
|
|
113
|
+
],
|
|
114
|
+
curl: [
|
|
115
|
+
"curl -X GET",
|
|
116
|
+
"curl -X POST",
|
|
117
|
+
"curl -sL",
|
|
118
|
+
"curl -o",
|
|
119
|
+
"curl -I",
|
|
120
|
+
"curl -H 'Content-Type: application/json'",
|
|
121
|
+
"curl -d '{}'",
|
|
122
|
+
],
|
|
123
|
+
ssh: [
|
|
124
|
+
"ssh-keygen -t ed25519",
|
|
125
|
+
"ssh-copy-id",
|
|
126
|
+
"ssh -i",
|
|
127
|
+
"ssh -L",
|
|
128
|
+
"ssh -p",
|
|
129
|
+
],
|
|
130
|
+
python: [
|
|
131
|
+
"python -m venv venv",
|
|
132
|
+
"python -m pip install",
|
|
133
|
+
"python -m pytest",
|
|
134
|
+
"python manage.py runserver",
|
|
135
|
+
"python manage.py migrate",
|
|
136
|
+
],
|
|
137
|
+
pip: [
|
|
138
|
+
"pip install",
|
|
139
|
+
"pip install -r requirements.txt",
|
|
140
|
+
"pip freeze > requirements.txt",
|
|
141
|
+
"pip list",
|
|
142
|
+
"pip uninstall",
|
|
143
|
+
],
|
|
144
|
+
go: [
|
|
145
|
+
"go run .",
|
|
146
|
+
"go build",
|
|
147
|
+
"go test ./...",
|
|
148
|
+
"go mod tidy",
|
|
149
|
+
"go mod init",
|
|
150
|
+
"go get",
|
|
151
|
+
"go fmt ./...",
|
|
152
|
+
"go vet ./...",
|
|
153
|
+
],
|
|
154
|
+
cargo: [
|
|
155
|
+
"cargo build",
|
|
156
|
+
"cargo run",
|
|
157
|
+
"cargo test",
|
|
158
|
+
"cargo add",
|
|
159
|
+
"cargo fmt",
|
|
160
|
+
"cargo clippy",
|
|
161
|
+
"cargo init",
|
|
162
|
+
"cargo publish",
|
|
163
|
+
],
|
|
164
|
+
make: [
|
|
165
|
+
"make build",
|
|
166
|
+
"make test",
|
|
167
|
+
"make clean",
|
|
168
|
+
"make install",
|
|
169
|
+
"make all",
|
|
170
|
+
],
|
|
171
|
+
kubectl: [
|
|
172
|
+
"kubectl get pods",
|
|
173
|
+
"kubectl get services",
|
|
174
|
+
"kubectl get deployments",
|
|
175
|
+
"kubectl logs",
|
|
176
|
+
"kubectl describe pod",
|
|
177
|
+
"kubectl apply -f",
|
|
178
|
+
"kubectl delete",
|
|
179
|
+
"kubectl exec -it",
|
|
180
|
+
"kubectl port-forward",
|
|
181
|
+
"kubectl config use-context",
|
|
182
|
+
],
|
|
183
|
+
systemctl: [
|
|
184
|
+
"systemctl status",
|
|
185
|
+
"systemctl start",
|
|
186
|
+
"systemctl stop",
|
|
187
|
+
"systemctl restart",
|
|
188
|
+
"systemctl enable",
|
|
189
|
+
"systemctl disable",
|
|
190
|
+
"systemctl list-units",
|
|
191
|
+
],
|
|
192
|
+
pm2: [
|
|
193
|
+
"pm2 list",
|
|
194
|
+
"pm2 start",
|
|
195
|
+
"pm2 stop",
|
|
196
|
+
"pm2 restart",
|
|
197
|
+
"pm2 logs",
|
|
198
|
+
"pm2 delete",
|
|
199
|
+
"pm2 monit",
|
|
200
|
+
"pm2 save",
|
|
201
|
+
],
|
|
202
|
+
tar: [
|
|
203
|
+
"tar -czf archive.tar.gz",
|
|
204
|
+
"tar -xzf",
|
|
205
|
+
"tar -xvf",
|
|
206
|
+
"tar -tf",
|
|
207
|
+
],
|
|
208
|
+
find: [
|
|
209
|
+
"find . -name",
|
|
210
|
+
"find . -type f",
|
|
211
|
+
"find . -type d",
|
|
212
|
+
"find . -mtime",
|
|
213
|
+
"find . -size",
|
|
214
|
+
],
|
|
215
|
+
grep: [
|
|
216
|
+
"grep -r",
|
|
217
|
+
"grep -rn",
|
|
218
|
+
"grep -ri",
|
|
219
|
+
"grep -l",
|
|
220
|
+
"grep -v",
|
|
221
|
+
],
|
|
222
|
+
chmod: [
|
|
223
|
+
"chmod +x",
|
|
224
|
+
"chmod 755",
|
|
225
|
+
"chmod 644",
|
|
226
|
+
"chmod -R",
|
|
227
|
+
],
|
|
228
|
+
vercel: [
|
|
229
|
+
"vercel deploy",
|
|
230
|
+
"vercel dev",
|
|
231
|
+
"vercel env pull",
|
|
232
|
+
"vercel logs",
|
|
233
|
+
"vercel ls",
|
|
234
|
+
],
|
|
235
|
+
supabase: [
|
|
236
|
+
"supabase start",
|
|
237
|
+
"supabase stop",
|
|
238
|
+
"supabase db reset",
|
|
239
|
+
"supabase migration new",
|
|
240
|
+
"supabase gen types typescript",
|
|
241
|
+
"supabase db push",
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Get common command suggestions for a query */
|
|
246
|
+
export function getCommonSuggestions(query: string, limit: number = 5): string[] {
|
|
247
|
+
const q = query.toLowerCase();
|
|
248
|
+
const results: string[] = [];
|
|
249
|
+
|
|
250
|
+
// Try prefix match on command groups
|
|
251
|
+
for (const [prefix, commands] of Object.entries(COMMANDS)) {
|
|
252
|
+
if (prefix.startsWith(q) || q.startsWith(prefix)) {
|
|
253
|
+
for (const cmd of commands) {
|
|
254
|
+
if (cmd.toLowerCase().startsWith(q) || cmd.toLowerCase().includes(q)) {
|
|
255
|
+
results.push(cmd);
|
|
256
|
+
if (results.length >= limit) return results;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback: search all commands
|
|
263
|
+
if (results.length < limit) {
|
|
264
|
+
const seen = new Set(results);
|
|
265
|
+
for (const commands of Object.values(COMMANDS)) {
|
|
266
|
+
for (const cmd of commands) {
|
|
267
|
+
if (!seen.has(cmd) && cmd.toLowerCase().includes(q)) {
|
|
268
|
+
results.push(cmd);
|
|
269
|
+
if (results.length >= limit) return results;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { getDbPath } from "../utils/paths";
|
|
3
|
+
import { runMigrations } from "./schema";
|
|
4
|
+
|
|
5
|
+
let db: Database | null = null;
|
|
6
|
+
|
|
7
|
+
export function getDb(): Database {
|
|
8
|
+
if (db) return db;
|
|
9
|
+
|
|
10
|
+
const dbPath = getDbPath();
|
|
11
|
+
db = new Database(dbPath, { create: true });
|
|
12
|
+
|
|
13
|
+
// Performance: WAL mode for concurrent read/write
|
|
14
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
15
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
16
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
17
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
18
|
+
|
|
19
|
+
runMigrations(db);
|
|
20
|
+
|
|
21
|
+
return db;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function closeDb(): void {
|
|
25
|
+
if (db) {
|
|
26
|
+
db.close();
|
|
27
|
+
db = null;
|
|
28
|
+
}
|
|
29
|
+
}
|