offgrid-ai 0.15.3 → 0.15.4

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/package.json +1 -1
  2. package/src/ui.mjs +42 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
4
4
  "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
5
5
  "author": "Eeshan Srivastava (https://eeshans.com)",
6
6
  "type": "module",
package/src/ui.mjs CHANGED
@@ -159,6 +159,43 @@ export function statusText(kind, text) {
159
159
  return color(text);
160
160
  }
161
161
 
162
+ // ── Escape-to-cancel helper ─────────────────────────────────────────────────
163
+
164
+ function withEscape() {
165
+ const controller = new AbortController();
166
+ let escapeTimer = null;
167
+ const onData = (data) => {
168
+ // Standalone Escape = 0x1b byte alone (arrow keys send 0x1b + more bytes)
169
+ if (data.length === 1 && data[0] === 0x1b) {
170
+ escapeTimer = setTimeout(() => controller.abort(), 50);
171
+ } else if (escapeTimer) {
172
+ clearTimeout(escapeTimer);
173
+ escapeTimer = null;
174
+ }
175
+ };
176
+ process.stdin.on("data", onData);
177
+ const cleanup = () => {
178
+ process.stdin.removeListener("data", onData);
179
+ if (escapeTimer) clearTimeout(escapeTimer);
180
+ };
181
+ return { signal: controller.signal, cleanup };
182
+ }
183
+
184
+ async function runPrompt(fn, config) {
185
+ const { signal, cleanup } = withEscape();
186
+ try {
187
+ return await fn(config, { signal });
188
+ } catch (err) {
189
+ if (err.name === "AbortPromptError") {
190
+ console.log(pc.dim("\nCancelled."));
191
+ process.exit(0);
192
+ }
193
+ throw err;
194
+ } finally {
195
+ cleanup();
196
+ }
197
+ }
198
+
162
199
  // ── Interactive prompt factory ──────────────────────────────────────────────
163
200
 
164
201
  export function startInteractive(title = "offgrid-ai") {
@@ -169,7 +206,7 @@ export function startInteractive(title = "offgrid-ai") {
169
206
  export function createPrompt() {
170
207
  return {
171
208
  async text(label, defaultValue) {
172
- const value = await input({
209
+ const value = await runPrompt(input, {
173
210
  message: label,
174
211
  default: defaultValue === undefined ? undefined : String(defaultValue),
175
212
  });
@@ -177,7 +214,7 @@ export function createPrompt() {
177
214
  },
178
215
 
179
216
  async number(label, defaultValue, min, max) {
180
- const value = await number({
217
+ const value = await runPrompt(number, {
181
218
  message: label,
182
219
  default: defaultValue,
183
220
  validate(input) {
@@ -190,7 +227,7 @@ export function createPrompt() {
190
227
  },
191
228
 
192
229
  async yesNo(label, defaultValue) {
193
- return await confirm({ message: label, default: defaultValue });
230
+ return await runPrompt(confirm, { message: label, default: defaultValue });
194
231
  },
195
232
 
196
233
  async choice(label, choices, defaultValue) {
@@ -203,7 +240,7 @@ export function createPrompt() {
203
240
  disabled: c.disabled || undefined,
204
241
  };
205
242
  });
206
- return await inquirerSelect({
243
+ return await runPrompt(inquirerSelect, {
207
244
  message: label,
208
245
  default: defaultValue,
209
246
  choices: mapped,
@@ -241,7 +278,7 @@ export async function modelSelect(label, groups, { defaultKey, pageSize = 20 } =
241
278
  });
242
279
  }
243
280
  }
244
- return await inquirerSelect({
281
+ return await runPrompt(inquirerSelect, {
245
282
  message: label,
246
283
  default: defaultKey,
247
284
  choices,