ineedcodes 1.0.1 → 1.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/src/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // ui.js: terminal helpers. No dependencies, respects NO_COLOR and non-TTY.
2
2
 
3
- export const VERSION = '1.0.0';
3
+ export const VERSION = '1.1.0';
4
4
 
5
5
  const USE_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
6
6
  const wrap = (code, t) => USE_COLOR ? `\x1b[${code}m${t}\x1b[0m` : String(t);
@@ -18,17 +18,80 @@ export const trunc = (s, n = 120) => {
18
18
  return o.length > n ? o.slice(0, n - 1) + '...' : o;
19
19
  };
20
20
 
21
- export const LOGO = [
22
- '#### ## ',
23
- ' ## ## ',
24
- ' ## ## ',
25
- ' ## ## ',
26
- ' #### ### '
27
- ].join('\n');
21
+ const plain = s => String(s).replace(/\x1b\[[0-9;]*m/g, '');
22
+
23
+ // Rounded box around lines. Width follows the longest line, capped to the terminal.
24
+ export function box(lines, colorFn = t => t) {
25
+ const cols = process.stdout.columns || 80;
26
+ const inner = Math.min(cols - 4, Math.max(10, ...lines.map(l => plain(l).length)) + 2);
27
+ const top = colorFn('╭' + '─'.repeat(inner) + '');
28
+ const bot = colorFn('╰' + '─'.repeat(inner) + '╯');
29
+ const mid = lines.map(l => colorFn('│') + ' ' + l + ' '.repeat(Math.max(0, inner - plain(l).length - 1)) + colorFn('│'));
30
+ return [top, ...mid, bot].join('\n');
31
+ }
32
+
33
+ const SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
34
+
35
+ // Spinner for TTYs only. In pipes and tests it becomes a no-op.
36
+ export function startSpinner(text = 'thinking') {
37
+ if (!process.stdout.isTTY || process.env.NO_COLOR) return { stop: () => {} };
38
+ let i = 0;
39
+ let stopped = false;
40
+ const line = () => `\r${cyan(SPIN_FRAMES[i++ % SPIN_FRAMES.length])} ${dim(text + '...')} `;
41
+ process.stdout.write(line());
42
+ const iv = setInterval(() => process.stdout.write(line()), 90);
43
+ return {
44
+ stop() {
45
+ if (stopped) return;
46
+ stopped = true;
47
+ clearInterval(iv);
48
+ process.stdout.write('\r' + ' '.repeat(plain(text).length + 20) + '\r');
49
+ }
50
+ };
51
+ }
52
+
53
+ export const RULE = () => dim('─'.repeat(Math.min(process.stdout.columns || 80, 64)));
54
+
55
+ // user bubble: quoted, colored, compact
56
+ export const userBubble = text => {
57
+ const w = Math.min(process.stdout.columns || 80, 60);
58
+ const wrapped = [];
59
+ for (const raw of String(text).split('\n')) {
60
+ let line = raw;
61
+ while (line.length > w - 4) {
62
+ wrapped.push(' ' + yellow('│ ') + line.slice(0, w - 4));
63
+ line = line.slice(w - 4);
64
+ }
65
+ wrapped.push(' ' + yellow('│ ') + line);
66
+ }
67
+ return wrapped.join('\n') + '\n ' + yellow('╰' + '─'.repeat(w - 4) + '╯');
68
+ };
69
+
70
+ // ── raw screen plumbing for the full-screen chat layout ──
71
+ export const screen = {
72
+ enter() { process.stdout.write('\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J'); },
73
+ exit() { process.stdout.write('\x1b[r\x1b[?25h\x1b[?1049l'); },
74
+ region(top, bot) { process.stdout.write(`\x1b[${top};${bot}r`); },
75
+ resetRegion() { process.stdout.write('\x1b[r'); },
76
+ at(row, col = 1) { process.stdout.write(`\x1b[${row};${col}H`); },
77
+ clearLine() { process.stdout.write('\x1b[2K'); }
78
+ };
28
79
 
29
80
  export const BANNER = () =>
30
81
  bold(green('ineed')) + dim(` v${VERSION}`) + dim(' · your terminal, now autonomous');
31
82
 
83
+ // Big startup logo, ANSI-shadow style. Six fixed-width lines.
84
+ const LOGO_LINES = [
85
+ '██╗ ███╗ ██╗ ███████╗ ███████╗ ██████╗ ',
86
+ '██║ ████╗ ██║ ██╔════╝ ██╔════╝ ██╔══██╗',
87
+ '██║ ██╔██╗ ██║ █████╗ █████╗ ██║ ██║',
88
+ '██║ ██║╚██╗██║ ██╔══╝ ██╔══╝ ██║ ██║',
89
+ '██║ ██║ ╚████║ ███████╗ ███████╗ ██████╔╝',
90
+ '╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ '
91
+ ];
92
+ export const LOGO = LOGO_LINES.join('\n');
93
+ export const logo = () => LOGO_LINES.map(l => green(bold(l))).join('\n');
94
+
32
95
  // Ask a question and await one line. `secret` hides typed characters.
33
96
  // onLine: receiver for lines typed when no question is pending (REPL dispatch).
34
97
  export function makeInput(rl, onLine) {
package/src/wizard.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { fetchModels, chat } from './provider.js';
4
4
  import { saveConfig } from './config.js';
5
- import { bold, dim, red, yellow, green, cyan, trunc } from './ui.js';
5
+ import { bold, dim, red, yellow, green, cyan, trunc, logo, BANNER, startSpinner } from './ui.js';
6
6
 
7
7
  export async function testConnection(cfg, signal) {
8
8
  // proves reachability + auth + a working chat in one shot
@@ -15,7 +15,10 @@ export async function wizard(ask, { fromCommand = false } = {}) {
15
15
  const abortIfEnded = () => { if (inputEnded()) throw Object.assign(new Error('Setup aborted: input ended.'), { aborted: true }); };
16
16
 
17
17
  console.log('');
18
- console.log(bold('Welcome to ineed') + dim(' - let\'s connect you to an AI provider. You only do this once.'));
18
+ console.log(logo());
19
+ console.log('');
20
+ console.log(BANNER());
21
+ console.log(bold('Welcome!') + dim(' Let\'s connect you to an AI provider. You only do this once.'));
19
22
  console.log(dim('Any OpenAI-compatible API works: OpenAI, OmniRoute, LM Studio, Ollama, vLLM, and more.'));
20
23
  console.log('');
21
24
 
@@ -39,9 +42,11 @@ export async function wizard(ask, { fromCommand = false } = {}) {
39
42
  const probe = { baseUrl, apiKey, model: 'x' };
40
43
  console.log(dim('\n Checking connection...'));
41
44
  let models = [];
45
+ const connSpin = startSpinner('connecting');
42
46
  try {
43
47
  models = await fetchModels(probe);
44
48
  } catch {}
49
+ connSpin.stop();
45
50
  if (models.length > 0) {
46
51
  console.log(green(` Connected. ${models.length} models available.`));
47
52
  } else {
@@ -65,11 +70,15 @@ export async function wizard(ask, { fromCommand = false } = {}) {
65
70
  console.log(dim(` Testing ${model}...`));
66
71
  let saved = false;
67
72
  while (!saved) {
73
+ let testSpin = null;
68
74
  try {
75
+ testSpin = startSpinner('testing ' + model);
69
76
  const reply = await testConnection({ baseUrl, apiKey, model });
77
+ testSpin.stop();
70
78
  console.log(green(' Works.') + dim(` Replied: ${trunc(reply, 40)}`));
71
79
  saved = true;
72
80
  } catch (err) {
81
+ testSpin?.stop();
73
82
  console.log(red(' Test failed: ' + err.message));
74
83
  const choice = await ask(' [r]etry key, [m]odel, [l]ist models, [b]ase url, or [s]ave anyway? ');
75
84
  if (choice === '') abortIfEnded();