@deaquinodev/querky 0.4.7 → 0.4.8

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.
Files changed (2) hide show
  1. package/dist/index.js +83 -97
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import { useState as useState5, useEffect as useEffect5 } from "react";
4
+ import { useState as useState6, useEffect as useEffect5 } from "react";
5
5
  import { parseArgs } from "util";
6
6
  import { render, Box as Box9, Text as Text9 } from "ink";
7
7
 
@@ -193,11 +193,11 @@ async function connectDsn(dsn) {
193
193
  }
194
194
 
195
195
  // src/ui/components/App.tsx
196
- import { useState as useState3, useEffect as useEffect4, useRef as useRef2, memo } from "react";
196
+ import { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
197
197
  import { writeFileSync as writeFileSync5 } from "fs";
198
198
  import { homedir as homedir4 } from "os";
199
- import { join as join6 } from "path";
200
- import { Box as Box7, Text as Text7, useApp, useInput as useInput2, useStdin as useStdin2 } from "ink";
199
+ import { join as join5 } from "path";
200
+ import { Box as Box7, Text as Text7, useApp, useInput as useInput2, useStdin as useStdin2, Static } from "ink";
201
201
 
202
202
  // src/db/query.ts
203
203
  async function runQuery(client, sql) {
@@ -1050,7 +1050,7 @@ async function fetchSchema(client, driver) {
1050
1050
  }
1051
1051
 
1052
1052
  // src/ui/components/QueryInput.tsx
1053
- import { useEffect as useEffect2 } from "react";
1053
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1054
1054
  import { Box, Text } from "ink";
1055
1055
 
1056
1056
  // src/ui/hooks/useVimInput.ts
@@ -1620,8 +1620,13 @@ function QueryInput({ onSubmit, isLoading, onModeChange, onShellModeChange, vimE
1620
1620
  const isShellMode = value.startsWith("!");
1621
1621
  const isCommand = !isShellMode && value.startsWith("/");
1622
1622
  const partial = isCommand ? value.slice(1) : "";
1623
- const suggestions = isCommand ? getCompletions(partial, aliases) : !isShellMode && schema ? getSqlCompletions(value, cursorPos, schema) : [];
1623
+ const liveSuggestions = isCommand ? getCompletions(partial, aliases) : !isShellMode && schema ? getSqlCompletions(value, cursorPos, schema) : [];
1624
1624
  const sqlToken = isCommand ? "" : getCurrentToken(value, cursorPos);
1625
+ const [suggestions, setSuggestions] = useState2([]);
1626
+ useEffect2(() => {
1627
+ const t = setTimeout(() => setSuggestions(liveSuggestions), 150);
1628
+ return () => clearTimeout(t);
1629
+ }, [value, cursorPos]);
1625
1630
  const descMap = {
1626
1631
  ...BUILTIN_DESC_MAP,
1627
1632
  ...Object.fromEntries(
@@ -1644,7 +1649,7 @@ function QueryInput({ onSubmit, isLoading, onModeChange, onShellModeChange, vimE
1644
1649
  const isMultiLine = !isEmpty && !isShellMode && !isCommand && value.includes("\n");
1645
1650
  const dOff = isShellMode ? 2 : 0;
1646
1651
  const placeholder = "Type a SQL query\u2026";
1647
- const previewResult = !isMultiLine && suggestionIndex >= 0 && suggestions.length > 0 ? handleSuggestionAccept(value, cursorPos, suggestions[suggestionIndex]) : null;
1652
+ const previewResult = !isMultiLine && suggestionIndex >= 0 && liveSuggestions.length > 0 ? handleSuggestionAccept(value, cursorPos, liveSuggestions[suggestionIndex]) : null;
1648
1653
  const renderValue = previewResult?.value ?? value;
1649
1654
  const renderCursor = previewResult?.cursor ?? cursorPos;
1650
1655
  const lineWidth = innerWidth - 4;
@@ -1667,6 +1672,17 @@ function QueryInput({ onSubmit, isLoading, onModeChange, onShellModeChange, vimE
1667
1672
  0
1668
1673
  );
1669
1674
  const hintsInline = totalHintsWidth <= termWidth;
1675
+ const STRIP_VISIBLE = 4;
1676
+ const stripPrefixW = isCommand ? 1 : 0;
1677
+ const stripAvailW = termWidth - 2 - 3 - (STRIP_VISIBLE - 1) * 3 - STRIP_VISIBLE * stripPrefixW;
1678
+ const stripMaxPerItem = Math.max(6, Math.floor(stripAvailW / STRIP_VISIBLE));
1679
+ const hasSuggestions = !isEmpty && !isShellMode && !isMultiLine && suggestions.length > 0;
1680
+ const stripWinStart = hasSuggestions ? Math.min(Math.max(0, suggestionIndex - 1), Math.max(0, suggestions.length - STRIP_VISIBLE)) : 0;
1681
+ const stripWindow = hasSuggestions ? suggestions.slice(stripWinStart, stripWinStart + STRIP_VISIBLE) : [];
1682
+ const stripNavHint = hasSuggestions ? `Tab/\u2191\u2193 navigate Enter select ${suggestionIndex + 1}/${suggestions.length}` : " ";
1683
+ function truncateSugg(s) {
1684
+ return s.length > stripMaxPerItem ? s.slice(0, stripMaxPerItem - 1) + "\u2026" : s;
1685
+ }
1670
1686
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1671
1687
  isMultiLine ? /* @__PURE__ */ jsxs(Fragment, { children: [
1672
1688
  /* @__PURE__ */ jsx(Text, { backgroundColor: BG, children: emptyLine }),
@@ -1707,50 +1723,30 @@ function QueryInput({ onSubmit, isLoading, onModeChange, onShellModeChange, vimE
1707
1723
  ] }),
1708
1724
  /* @__PURE__ */ jsx(Text, { backgroundColor: BG, children: emptyLine })
1709
1725
  ] }),
1710
- (() => {
1711
- const hasSuggestions = !isEmpty && !isShellMode && !isMultiLine && suggestions.length > 0;
1712
- const VISIBLE = 4;
1713
- const SEP_W = 3;
1714
- const ELLIPSIS_W = 3;
1715
- const prefixW = isCommand ? 1 : 0;
1716
- const availW = termWidth - 2 - ELLIPSIS_W - (VISIBLE - 1) * SEP_W - VISIBLE * prefixW;
1717
- const maxPerItem = Math.max(6, Math.floor(availW / VISIBLE));
1718
- const winStart = hasSuggestions ? Math.min(Math.max(0, suggestionIndex - 1), Math.max(0, suggestions.length - VISIBLE)) : 0;
1719
- const windowSlice = hasSuggestions ? suggestions.slice(winStart, winStart + VISIBLE) : [];
1720
- function truncate(s) {
1721
- return s.length > maxPerItem ? s.slice(0, maxPerItem - 1) + "\u2026" : s;
1722
- }
1723
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1724
- /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: hasSuggestions ? /* @__PURE__ */ jsxs(Fragment, { children: [
1725
- windowSlice.map((name, i) => {
1726
- const realIdx = winStart + i;
1727
- const selected = realIdx === suggestionIndex;
1728
- const displayName = truncate(name);
1729
- return /* @__PURE__ */ jsxs(Text, { children: [
1730
- i > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1731
- isCommand && /* @__PURE__ */ jsx(Text, { dimColor: !selected, color: selected ? ACCENT : void 0, children: "/" }),
1732
- /* @__PURE__ */ jsx(FuzzyHighlight, { text: displayName, token: isCommand ? partial : sqlToken, selected })
1733
- ] }, name);
1734
- }),
1735
- suggestions.length > VISIBLE && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2026" })
1736
- ] }) : /* @__PURE__ */ jsx(Box, { flexDirection: hintsInline ? "row" : "column", children: allHints.map(([key, desc], i) => /* @__PURE__ */ jsxs(Text, { children: [
1737
- hintsInline && i > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " | " }),
1738
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: desc }),
1739
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `: ${key}` })
1740
- ] }, desc)) }) }),
1741
- /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: hasSuggestions ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1742
- "Tab/\u2191\u2193 navigate Enter select ",
1743
- suggestionIndex + 1,
1744
- "/",
1745
- suggestions.length
1746
- ] }) : /* @__PURE__ */ jsx(Text, { children: " " }) })
1747
- ] });
1748
- })()
1726
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1727
+ /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: hasSuggestions ? /* @__PURE__ */ jsxs(Fragment, { children: [
1728
+ stripWindow.map((name, i) => {
1729
+ const realIdx = stripWinStart + i;
1730
+ const selected = realIdx === suggestionIndex;
1731
+ return /* @__PURE__ */ jsxs(Text, { children: [
1732
+ i > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1733
+ isCommand && /* @__PURE__ */ jsx(Text, { dimColor: !selected, color: selected ? ACCENT : void 0, children: "/" }),
1734
+ /* @__PURE__ */ jsx(FuzzyHighlight, { text: truncateSugg(name), token: isCommand ? partial : sqlToken, selected })
1735
+ ] }, name);
1736
+ }),
1737
+ suggestions.length > STRIP_VISIBLE && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2026" })
1738
+ ] }) : /* @__PURE__ */ jsx(Box, { flexDirection: hintsInline ? "row" : "column", children: allHints.map(([key, desc], i) => /* @__PURE__ */ jsxs(Text, { children: [
1739
+ hintsInline && i > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " | " }),
1740
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: desc }),
1741
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `: ${key}` })
1742
+ ] }, desc)) }) }),
1743
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: stripNavHint })
1744
+ ] })
1749
1745
  ] });
1750
1746
  }
1751
1747
 
1752
1748
  // src/ui/components/QueryResult.tsx
1753
- import { useState as useState2, useEffect as useEffect3 } from "react";
1749
+ import { useState as useState3, useEffect as useEffect3 } from "react";
1754
1750
  import { Box as Box3, Text as Text3 } from "ink";
1755
1751
 
1756
1752
  // src/ui/components/Table.tsx
@@ -1848,7 +1844,7 @@ function formatDuration(ms) {
1848
1844
  return `${(ms / 1e3).toFixed(2)}s`;
1849
1845
  }
1850
1846
  function useTerminalWidth() {
1851
- const [width, setWidth] = useState2(process.stdout.columns ?? 80);
1847
+ const [width, setWidth] = useState3(process.stdout.columns ?? 80);
1852
1848
  useEffect3(() => {
1853
1849
  const handler = () => setWidth(process.stdout.columns ?? 80);
1854
1850
  process.stdout.on("resize", handler);
@@ -1917,16 +1913,8 @@ function QueryResult({ state, elapsed, page, pageSize }) {
1917
1913
 
1918
1914
  // src/ui/components/Banner.tsx
1919
1915
  import { Box as Box4, Text as Text4 } from "ink";
1920
- import { readFileSync as readFileSync5 } from "fs";
1921
- import { fileURLToPath } from "url";
1922
- import { dirname, join as join5 } from "path";
1923
1916
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1924
- var __dirname = dirname(fileURLToPath(import.meta.url));
1925
- var pkg = { version: "unknown" };
1926
- try {
1927
- pkg = JSON.parse(readFileSync5(join5(__dirname, "../../../package.json"), "utf8"));
1928
- } catch {
1929
- }
1917
+ var version = true ? "0.4.8" : process.env.npm_package_version ?? "unknown";
1930
1918
  var LOGO = [
1931
1919
  " \u2597\u2584\u2584\u2584\u2584\u2584\u2596",
1932
1920
  "\u2590\u2591 \u25CF \u25CF \u2591\u258C",
@@ -1950,7 +1938,7 @@ function Banner({ connectionState }) {
1950
1938
  "Querky ",
1951
1939
  /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1952
1940
  "v",
1953
- pkg.version
1941
+ version
1954
1942
  ] })
1955
1943
  ] }),
1956
1944
  /* @__PURE__ */ jsx4(Text4, { children: " " }),
@@ -2192,7 +2180,7 @@ function QueryHeader({ label, query }) {
2192
2180
  ] })
2193
2181
  ] });
2194
2182
  }
2195
- var EntryView = memo(function EntryView2({ entry }) {
2183
+ function EntryView({ entry }) {
2196
2184
  const showAi = entry.aiResponse !== "" || entry.aiError !== null;
2197
2185
  const showErd = entry.erdData !== null;
2198
2186
  const isShell = entry.query.startsWith("!");
@@ -2211,34 +2199,33 @@ var EntryView = memo(function EntryView2({ entry }) {
2211
2199
  ] }) : !entry.commandMessage && /* @__PURE__ */ jsx7(QueryResult, { state: entry.queryState, elapsed: entry.elapsed, page: entry.page, pageSize: PAGE_SIZE })
2212
2200
  ] }) })
2213
2201
  ] });
2214
- });
2202
+ }
2215
2203
  function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2, onChangeDatabase }) {
2216
2204
  const { exit } = useApp();
2217
2205
  const { isRawModeSupported } = useStdin2();
2218
- const [queryState, setQueryState] = useState3({ status: "idle" });
2219
- const [lastQuery, setLastQuery] = useState3("");
2220
- const [lastSqlQuery, setLastSqlQuery] = useState3("");
2221
- const [lastResult, setLastResult] = useState3(null);
2222
- const [page, setPage] = useState3(0);
2223
- const [vimMode, setVimMode] = useState3("INSERT");
2224
- const [inputIsShell, setInputIsShell] = useState3(false);
2225
- const [elapsed, setElapsed] = useState3(null);
2226
- const [vimEnabled, setVimEnabled] = useState3(true);
2227
- const [commandMessage, setCommandMessage] = useState3(null);
2228
- const [history, setHistory] = useState3(() => loadHistory());
2229
- const [schema, setSchema] = useState3(null);
2230
- const [aiResponse, setAiResponse] = useState3("");
2231
- const [isStreaming, setIsStreaming] = useState3(false);
2232
- const [aiError, setAiError] = useState3(null);
2233
- const [shellOutput, setShellOutput] = useState3(null);
2234
- const [isShellRunning, setIsShellRunning] = useState3(false);
2235
- const [erdData, setErdData] = useState3(null);
2236
- const [isErdLoading, setIsErdLoading] = useState3(false);
2237
- const [completedEntries, setCompletedEntries] = useState3([]);
2238
- const [clearSeq, setClearSeq] = useState3(0);
2206
+ const [queryState, setQueryState] = useState4({ status: "idle" });
2207
+ const [lastQuery, setLastQuery] = useState4("");
2208
+ const [lastSqlQuery, setLastSqlQuery] = useState4("");
2209
+ const [lastResult, setLastResult] = useState4(null);
2210
+ const [page, setPage] = useState4(0);
2211
+ const [vimMode, setVimMode] = useState4("INSERT");
2212
+ const [inputIsShell, setInputIsShell] = useState4(false);
2213
+ const [elapsed, setElapsed] = useState4(null);
2214
+ const [vimEnabled, setVimEnabled] = useState4(true);
2215
+ const [commandMessage, setCommandMessage] = useState4(null);
2216
+ const [history, setHistory] = useState4(() => loadHistory());
2217
+ const [schema, setSchema] = useState4(null);
2218
+ const [aiResponse, setAiResponse] = useState4("");
2219
+ const [isStreaming, setIsStreaming] = useState4(false);
2220
+ const [aiError, setAiError] = useState4(null);
2221
+ const [shellOutput, setShellOutput] = useState4(null);
2222
+ const [isShellRunning, setIsShellRunning] = useState4(false);
2223
+ const [erdData, setErdData] = useState4(null);
2224
+ const [isErdLoading, setIsErdLoading] = useState4(false);
2225
+ const [completedEntries, setCompletedEntries] = useState4([]);
2239
2226
  const entryIdRef = useRef2(0);
2240
2227
  const aliasScope = connectionState.status === "connected" ? makeScope(connectionState.driver, connectionState.user, connectionState.host, connectionState.database) : "";
2241
- const [aliases, setAliases] = useState3(
2228
+ const [aliases, setAliases] = useState4(
2242
2229
  () => aliasScope ? getAllAliases(aliasScope) : {}
2243
2230
  );
2244
2231
  function handleSaveAlias(name, query) {
@@ -2266,10 +2253,6 @@ function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2,
2266
2253
  process.off("SIGCONT", handleCont);
2267
2254
  };
2268
2255
  }, [isRawModeSupported]);
2269
- useEffect4(() => {
2270
- if (clearSeq === 0) return;
2271
- process.stdout.write("\x1B[3J");
2272
- }, [clearSeq]);
2273
2256
  useEffect4(() => {
2274
2257
  if (!isRawModeSupported) return;
2275
2258
  process.stdout.write("\x1B[?25l");
@@ -2350,7 +2333,7 @@ function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2,
2350
2333
  return;
2351
2334
  }
2352
2335
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2353
- const filename = join6(homedir4(), `q-export-${ts}.${format}`);
2336
+ const filename = join5(homedir4(), `q-export-${ts}.${format}`);
2354
2337
  try {
2355
2338
  if (format === "json") {
2356
2339
  writeFileSync5(filename, JSON.stringify(lastResult.rows, null, 2));
@@ -2449,7 +2432,7 @@ function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2,
2449
2432
  setAiError(null);
2450
2433
  setElapsed(null);
2451
2434
  setPage(0);
2452
- setClearSeq((s) => s + 1);
2435
+ process.stdout.write("\x1B[2J\x1B[H");
2453
2436
  },
2454
2437
  aliases,
2455
2438
  onSaveAlias: handleSaveAlias,
@@ -2477,7 +2460,7 @@ function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2,
2477
2460
  const isCommand = !isShellEntry && (lastQuery.startsWith("/") || lastQuery.startsWith("\\"));
2478
2461
  const activeLabel = isShellEntry ? "Shell:" : isCommand ? "Command:" : "Query:";
2479
2462
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
2480
- completedEntries.map((entry) => /* @__PURE__ */ jsx7(EntryView, { entry }, entry.id)),
2463
+ /* @__PURE__ */ jsx7(Static, { items: completedEntries, children: (entry) => /* @__PURE__ */ jsx7(EntryView, { entry }, entry.id) }),
2481
2464
  /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, children: [
2482
2465
  lastQuery === "" && /* @__PURE__ */ jsx7(Banner, { connectionState }),
2483
2466
  lastQuery !== "" && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 2, children: [
@@ -2515,7 +2498,7 @@ function App({ connectionState, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2,
2515
2498
  }
2516
2499
 
2517
2500
  // src/ui/components/ConnectionWizard.tsx
2518
- import { useState as useState4 } from "react";
2501
+ import { useState as useState5 } from "react";
2519
2502
  import { Box as Box8, Text as Text8, useInput as useInput3, useStdin as useStdin3 } from "ink";
2520
2503
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2521
2504
  var DRIVERS = ["postgresql", "mysql", "sqlite"];
@@ -2526,8 +2509,8 @@ function fieldLabels(driver) {
2526
2509
  }
2527
2510
  function ConnectionWizard({ onConnect, initialError }) {
2528
2511
  const { isRawModeSupported } = useStdin3();
2529
- const [focus, setFocus] = useState4(0);
2530
- const [fields, setFields] = useState4({
2512
+ const [focus, setFocus] = useState5(0);
2513
+ const [fields, setFields] = useState5({
2531
2514
  driver: "postgresql",
2532
2515
  host: "localhost",
2533
2516
  port: "5432",
@@ -2535,9 +2518,9 @@ function ConnectionWizard({ onConnect, initialError }) {
2535
2518
  user: "",
2536
2519
  password: ""
2537
2520
  });
2538
- const [connecting, setConnecting] = useState4(false);
2539
- const [error, setError] = useState4(initialError ?? null);
2540
- const [keychainHint, setKeychainHint] = useState4(false);
2521
+ const [connecting, setConnecting] = useState5(false);
2522
+ const [error, setError] = useState5(initialError ?? null);
2523
+ const [keychainHint, setKeychainHint] = useState5(false);
2541
2524
  const labels = fieldLabels(fields.driver);
2542
2525
  const fieldCount = labels.length;
2543
2526
  function getTextValue(idx) {
@@ -2717,8 +2700,8 @@ var aiUrl = values["ai-url"];
2717
2700
  var aiModel = values["ai-model"];
2718
2701
  var aiKey = values["api-key"] || process.env.Q_CLI_API_KEY || "";
2719
2702
  function Root({ initialDsn: initialDsn2, aiUrl: aiUrl2, aiModel: aiModel2, aiKey: aiKey2 }) {
2720
- const [connectionState, setConnectionState] = useState5(null);
2721
- const [dsnError, setDsnError] = useState5(null);
2703
+ const [connectionState, setConnectionState] = useState6(null);
2704
+ const [dsnError, setDsnError] = useState6(null);
2722
2705
  useEffect5(() => {
2723
2706
  if (initialDsn2) {
2724
2707
  connectDsn(initialDsn2).then((state) => {
@@ -2757,6 +2740,9 @@ function Root({ initialDsn: initialDsn2, aiUrl: aiUrl2, aiModel: aiModel2, aiKey
2757
2740
  }
2758
2741
  );
2759
2742
  }
2743
+ process.stdout.write("\x1B[?1049h\x1B[H");
2744
+ process.on("exit", () => process.stdout.write("\x1B[?1049l"));
2745
+ process.on("SIGTERM", () => process.exit(0));
2760
2746
  render(
2761
2747
  /* @__PURE__ */ jsx9(Root, { initialDsn, aiUrl, aiModel, aiKey }),
2762
2748
  { exitOnCtrlC: true }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deaquinodev/querky",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "A quirky terminal SQL client with vim mode, AI features, and schema-aware autocomplete",
5
5
  "main": "dist/index.js",
6
6
  "files": [