kennel 0.2.25 → 0.2.26
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/dist/cli/dispatch.js +2 -2
- package/dist/cli/usage.js +7 -1
- package/dist/package/preview.d.ts +4 -0
- package/dist/package/preview.js +391 -46
- package/dist/site/terminal.d.ts +9 -1
- package/dist/site/terminal.js +162 -20
- package/package.json +1 -1
package/dist/cli/dispatch.js
CHANGED
|
@@ -54,8 +54,8 @@ export async function dispatch(argv) {
|
|
|
54
54
|
await viewer(['--static', ...args]);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
if (command === 'share') {
|
|
58
|
-
// Deliberate remote
|
|
57
|
+
if (command === 'share' || command === 'display') {
|
|
58
|
+
// Deliberate remote display (LAN default; --tailscale for outside the
|
|
59
59
|
// house). Runs a second remote-scoped static server; the plain 5180
|
|
60
60
|
// viewer stays loopback-only.
|
|
61
61
|
const { share } = await import('../package/preview.js');
|
package/dist/cli/usage.js
CHANGED
|
@@ -13,7 +13,7 @@ export function usage() {
|
|
|
13
13
|
kennel open|o [folder] [--open] [--host <host>] [--port <port>] [--dry-run]
|
|
14
14
|
knl <plugin-id|alias> [folder] [--item <id>] [--open] [--dry-run]
|
|
15
15
|
kennel serve [folder] [--host <host>] [--port <port>] [--lan|--tailscale]
|
|
16
|
-
kennel share [folder] [--lan|--tailscale] [--port <port>] [--dry-run]
|
|
16
|
+
kennel share|display [folder] [--lan|--tailscale] [--port <port>] [--dry-run]
|
|
17
17
|
kennel dev [path] [--config <json-file>] [--port <port>] [--script <script>] [--dry-run] [-- <args...>]
|
|
18
18
|
kennel extension install
|
|
19
19
|
kennel mcp generate [path] [--config <json-file>] [--data-only|--function-only]
|
|
@@ -25,6 +25,12 @@ export function usage() {
|
|
|
25
25
|
kennel deploy [path] [--config <json-file>] [legacy alias for publish]
|
|
26
26
|
kennel dp [path] [--config <json-file>] [legacy alias for publish]
|
|
27
27
|
|
|
28
|
+
In a terminal, the viewer shows the folder name, repository name, and an action menu.
|
|
29
|
+
Use Up/Down to move and Enter to choose.
|
|
30
|
+
Shortcuts: o open, s share to mobile, q exit.
|
|
31
|
+
Esc or Ctrl+C also exits.
|
|
32
|
+
Without an interactive terminal, the viewer prints its URL without a menu.
|
|
33
|
+
|
|
28
34
|
Installed plugin ids are commands, for example knl tasks or knl calendar.
|
|
29
35
|
Plugins can declare extra aliases, for example knl mail for plugin id email.
|
|
30
36
|
Use --item <id> for a plugin detail view. Built-in commands take priority.
|
|
@@ -7,3 +7,7 @@
|
|
|
7
7
|
* sources, while staying type-checked against that source of truth.
|
|
8
8
|
*/
|
|
9
9
|
export declare function share(input: string[]): Promise<void>;
|
|
10
|
+
export declare function startLanShare(target: {
|
|
11
|
+
root: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
}): Promise<import("kennel-preview/terminal").PreviewShare>;
|
package/dist/package/preview.js
CHANGED
|
@@ -211,9 +211,9 @@ var init_document_kinds = __esm(() => {
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
// packages/preview/src/terminal.ts
|
|
214
|
-
import { execFile } from "node:child_process";
|
|
214
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
215
215
|
import { basename } from "node:path";
|
|
216
|
-
import { emitKeypressEvents } from "node:readline";
|
|
216
|
+
import { clearScreenDown, cursorTo, emitKeypressEvents } from "node:readline";
|
|
217
217
|
function openPreview(url) {
|
|
218
218
|
const command = process.platform === "darwin" ? "/usr/bin/open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
219
219
|
execFile(command, [url], (error) => {
|
|
@@ -221,17 +221,39 @@ function openPreview(url) {
|
|
|
221
221
|
console.error(`Could not open browser: ${error.message}`);
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
|
-
function
|
|
224
|
+
function copyPreviewLink(url) {
|
|
225
|
+
const command = process.platform === "darwin" ? "/usr/bin/pbcopy" : process.platform === "win32" ? "clip" : process.env.WAYLAND_DISPLAY ? "wl-copy" : "xclip";
|
|
226
|
+
const args = command === "xclip" ? ["-selection", "clipboard"] : [];
|
|
227
|
+
return new Promise((done, fail) => {
|
|
228
|
+
const child = execFile(command, args, (error) => error ? fail(error) : done());
|
|
229
|
+
child.stdin?.on("error", fail);
|
|
230
|
+
child.stdin?.end(url);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function repositoryName(folder) {
|
|
234
|
+
try {
|
|
235
|
+
const root = execFileSync("git", ["-C", folder, "rev-parse", "--show-toplevel"], {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
238
|
+
timeout: 1000
|
|
239
|
+
});
|
|
240
|
+
return terminalText(basename(root.trim()));
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function previewTerminal(url, target, startShare) {
|
|
225
246
|
url = new URL(url).href;
|
|
226
247
|
const input = process.stdin;
|
|
227
248
|
const output = process.stdout;
|
|
228
249
|
const terminal = Boolean(output.isTTY && process.env.TERM !== "dumb");
|
|
229
250
|
const interactive = terminal && Boolean(input.isTTY);
|
|
230
|
-
const name = (basename(target.target ?? target.root) || target.root)
|
|
231
|
-
const
|
|
251
|
+
const name = terminalText(basename(target.target ?? target.root) || target.root);
|
|
252
|
+
const title = terminalText(basename(target.root) || target.root);
|
|
253
|
+
const repository = interactive ? repositoryName(target.root) : undefined;
|
|
232
254
|
const color = terminal && process.env.NO_COLOR === undefined && output.hasColors();
|
|
233
|
-
const accent = (text) => color ? `\x1B[
|
|
234
|
-
const link = terminal ? `\x1B]8;;${url}\x1B\\${accent(label)}\x1B]8;;\x1B\\` : `${label}
|
|
255
|
+
const accent = (text) => color ? `\x1B[32m${text}\x1B[39m` : text;
|
|
256
|
+
const link = (label) => terminal ? `\x1B]8;;${url}\x1B\\${accent(label)}\x1B]8;;\x1B\\` : `${label}
|
|
235
257
|
${url}`;
|
|
236
258
|
const wasRaw = Boolean(input.isRaw);
|
|
237
259
|
const wasFlowing = input.readableFlowing === true;
|
|
@@ -240,6 +262,74 @@ function previewTerminal(url, target) {
|
|
|
240
262
|
resolveClosed = resolve;
|
|
241
263
|
});
|
|
242
264
|
let finished = false;
|
|
265
|
+
let busy = false;
|
|
266
|
+
let selected = 0;
|
|
267
|
+
let status = "";
|
|
268
|
+
let share;
|
|
269
|
+
const render = () => {
|
|
270
|
+
if (!interactive || finished)
|
|
271
|
+
return;
|
|
272
|
+
cursorTo(output, 0, 0);
|
|
273
|
+
clearScreenDown(output);
|
|
274
|
+
const width = Math.max(1, (output.columns || 80) - 5);
|
|
275
|
+
const fit = (text) => {
|
|
276
|
+
const characters = Array.from(terminalText(text));
|
|
277
|
+
return characters.length > width ? characters.slice(0, width - 1).join("") + "…" : characters.join("");
|
|
278
|
+
};
|
|
279
|
+
const count = Math.max(1, Math.min(8, (output.rows || 24) - 12));
|
|
280
|
+
const start = Math.max(0, Math.min(selected - Math.floor(count / 2), actions.length - count));
|
|
281
|
+
const choices = actions.slice(start, start + count).map((item, index) => {
|
|
282
|
+
const label = fit(item.shortcut ? `[${item.shortcut}] ${item.label}` : item.label);
|
|
283
|
+
return start + index === selected ? accent(` › ${label}`) : ` ${label}`;
|
|
284
|
+
});
|
|
285
|
+
output.write([
|
|
286
|
+
` \x1B[1m${link(fit(title))}\x1B[22m`,
|
|
287
|
+
...repository ? [` ${color ? "\x1B[2m" : ""}${fit(repository)}${color ? "\x1B[22m" : ""}`] : [],
|
|
288
|
+
...target.target ? [` File: ${fit(name)}`] : [],
|
|
289
|
+
"",
|
|
290
|
+
...choices,
|
|
291
|
+
"",
|
|
292
|
+
" ↑ ↓ to move · Enter to choose · Esc or Ctrl+C to exit",
|
|
293
|
+
...actions.length > count ? [` ${selected + 1} / ${actions.length}`] : [],
|
|
294
|
+
...status ? ["", ` ${status}`] : [],
|
|
295
|
+
""
|
|
296
|
+
].join(`
|
|
297
|
+
`));
|
|
298
|
+
};
|
|
299
|
+
const shareFolder = async () => {
|
|
300
|
+
status = "Preparing a password-protected Wi-Fi link…";
|
|
301
|
+
render();
|
|
302
|
+
try {
|
|
303
|
+
share ??= startShare();
|
|
304
|
+
let active;
|
|
305
|
+
try {
|
|
306
|
+
active = await share;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
share = undefined;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
if (finished)
|
|
312
|
+
return;
|
|
313
|
+
status = `Wi-Fi link: ${active.url}
|
|
314
|
+
Username: ${active.username} · Password: ${active.password}
|
|
315
|
+
Password required. Send the password separately. Same Wi-Fi only.
|
|
316
|
+
Self-signed HTTPS (24 hours): expect a certificate warning.
|
|
317
|
+
Verify SHA-256 before trusting the certificate:
|
|
318
|
+
${active.fingerprint}
|
|
319
|
+
Keep this command running. Ctrl+C stops sharing.`;
|
|
320
|
+
render();
|
|
321
|
+
try {
|
|
322
|
+
await copyPreviewLink(active.url);
|
|
323
|
+
status += `
|
|
324
|
+
Wi-Fi link copied to clipboard (without the password).`;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
status += `
|
|
327
|
+
Could not copy link: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
status = `Could not share folder: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
243
333
|
const close = () => {
|
|
244
334
|
if (finished)
|
|
245
335
|
return;
|
|
@@ -250,39 +340,83 @@ function previewTerminal(url, target) {
|
|
|
250
340
|
if (interactive) {
|
|
251
341
|
input.off("keypress", keypress);
|
|
252
342
|
input.off("end", close);
|
|
343
|
+
output.off("resize", render);
|
|
344
|
+
output.write("\x1B[?25h\x1B[?1049l");
|
|
253
345
|
input.setRawMode(wasRaw);
|
|
254
346
|
if (!wasFlowing)
|
|
255
347
|
input.pause();
|
|
256
348
|
}
|
|
257
|
-
|
|
349
|
+
if (share) {
|
|
350
|
+
share.then((active) => active.close()).catch((error) => {
|
|
351
|
+
console.error(` Wi-Fi share stopped: ${error instanceof Error ? error.message : error}`);
|
|
352
|
+
}).finally(() => resolveClosed());
|
|
353
|
+
} else
|
|
354
|
+
resolveClosed();
|
|
355
|
+
};
|
|
356
|
+
const actions = [
|
|
357
|
+
{ label: "Open in browser", shortcut: "o", run: () => {
|
|
358
|
+
openPreview(url);
|
|
359
|
+
status = "Browser open requested.";
|
|
360
|
+
} },
|
|
361
|
+
{ label: "Share to another device", shortcut: "s", run: shareFolder },
|
|
362
|
+
{ label: "Exit", shortcut: "q", run: close }
|
|
363
|
+
];
|
|
364
|
+
const activate = async () => {
|
|
365
|
+
busy = true;
|
|
366
|
+
try {
|
|
367
|
+
await actions[selected].run();
|
|
368
|
+
} catch (error) {
|
|
369
|
+
status = `Could not complete action: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
370
|
+
} finally {
|
|
371
|
+
busy = false;
|
|
372
|
+
render();
|
|
373
|
+
}
|
|
258
374
|
};
|
|
259
375
|
const keypress = (_text, key) => {
|
|
260
|
-
if (key.ctrl && (key.name === "c" || key.name === "d"))
|
|
376
|
+
if (key.name === "escape" || key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
261
377
|
close();
|
|
262
|
-
|
|
263
|
-
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (busy || finished)
|
|
381
|
+
return;
|
|
382
|
+
const shortcut = !key.ctrl && !key.meta ? actions.findIndex((item) => item.shortcut === key.name) : -1;
|
|
383
|
+
if (shortcut !== -1) {
|
|
384
|
+
selected = shortcut;
|
|
385
|
+
activate();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (key.name === "up")
|
|
389
|
+
selected = (selected - 1 + actions.length) % actions.length;
|
|
390
|
+
else if (key.name === "down")
|
|
391
|
+
selected = (selected + 1) % actions.length;
|
|
392
|
+
else if (key.name === "return") {
|
|
393
|
+
activate();
|
|
394
|
+
return;
|
|
395
|
+
} else
|
|
396
|
+
return;
|
|
397
|
+
render();
|
|
264
398
|
};
|
|
265
399
|
process.once("SIGINT", close);
|
|
266
400
|
process.once("SIGTERM", close);
|
|
267
401
|
process.once("exit", close);
|
|
268
402
|
if (interactive) {
|
|
403
|
+
output.write("\x1B[?1049h\x1B[?25l");
|
|
404
|
+
output.on("resize", render);
|
|
269
405
|
emitKeypressEvents(input);
|
|
270
406
|
input.setRawMode(true);
|
|
271
407
|
input.on("keypress", keypress);
|
|
272
408
|
input.once("end", close);
|
|
273
409
|
input.resume();
|
|
274
410
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
${accent("o")} - open in browser
|
|
281
|
-
ctrl+c - exit
|
|
411
|
+
if (interactive)
|
|
412
|
+
render();
|
|
413
|
+
else
|
|
414
|
+
console.log(`
|
|
415
|
+
${link(name)}
|
|
282
416
|
`);
|
|
283
|
-
}
|
|
284
417
|
return { close, closed, interactive };
|
|
285
418
|
}
|
|
419
|
+
var terminalText = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
286
420
|
var init_terminal = () => {};
|
|
287
421
|
|
|
288
422
|
// node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
|
|
@@ -6090,7 +6224,7 @@ var init_src = __esm(() => {
|
|
|
6090
6224
|
});
|
|
6091
6225
|
|
|
6092
6226
|
// packages/preview/src/hot-viewer.ts
|
|
6093
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
6227
|
+
import { execFileSync as execFileSync2, spawn } from "node:child_process";
|
|
6094
6228
|
import { randomUUID } from "node:crypto";
|
|
6095
6229
|
import { existsSync } from "node:fs";
|
|
6096
6230
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -6139,13 +6273,13 @@ function startupLock(host, port) {
|
|
|
6139
6273
|
return resolve(tmpdir(), `kennel-hot-viewer-${name}.lock`);
|
|
6140
6274
|
}
|
|
6141
6275
|
function lanHost() {
|
|
6142
|
-
const route =
|
|
6276
|
+
const route = execFileSync2("/sbin/route", ["-n", "get", "default"], {
|
|
6143
6277
|
encoding: "utf8"
|
|
6144
6278
|
});
|
|
6145
6279
|
const device = route.match(/interface:\s+(\S+)/)?.[1];
|
|
6146
6280
|
if (!device)
|
|
6147
6281
|
throw new Error("kennel could not find the local network interface");
|
|
6148
|
-
const host =
|
|
6282
|
+
const host = execFileSync2("/usr/sbin/ipconfig", ["getifaddr", device], {
|
|
6149
6283
|
encoding: "utf8"
|
|
6150
6284
|
}).trim();
|
|
6151
6285
|
if (!host)
|
|
@@ -6206,7 +6340,10 @@ async function hotViewer(options) {
|
|
|
6206
6340
|
return;
|
|
6207
6341
|
}
|
|
6208
6342
|
const reuse = async () => {
|
|
6209
|
-
const terminal = previewTerminal(viewerUrl, options)
|
|
6343
|
+
const terminal = previewTerminal(viewerUrl, options, async () => {
|
|
6344
|
+
await Promise.resolve().then(() => init_share());
|
|
6345
|
+
return startLanShare(options);
|
|
6346
|
+
});
|
|
6210
6347
|
if (options.open)
|
|
6211
6348
|
openPreview(viewerUrl);
|
|
6212
6349
|
try {
|
|
@@ -6262,7 +6399,10 @@ async function hotViewer(options) {
|
|
|
6262
6399
|
},
|
|
6263
6400
|
stdio: ["ignore", "inherit", "inherit"]
|
|
6264
6401
|
});
|
|
6265
|
-
const terminal = previewTerminal(viewerUrl, options)
|
|
6402
|
+
const terminal = previewTerminal(viewerUrl, options, async () => {
|
|
6403
|
+
await Promise.resolve().then(() => init_share());
|
|
6404
|
+
return startLanShare(options);
|
|
6405
|
+
});
|
|
6266
6406
|
terminal.closed.then(() => {
|
|
6267
6407
|
if (child.exitCode === null && child.signalCode === null)
|
|
6268
6408
|
child.kill("SIGTERM");
|
|
@@ -40336,24 +40476,12 @@ var init_server4 = __esm(() => {
|
|
|
40336
40476
|
};
|
|
40337
40477
|
});
|
|
40338
40478
|
|
|
40339
|
-
// packages/preview/src/share.ts
|
|
40340
|
-
import { resolve as resolve15 } from "node:path";
|
|
40341
|
-
import { existsSync as existsSync7 } from "node:fs";
|
|
40342
|
-
|
|
40343
40479
|
// packages/preview/src/viewer.ts
|
|
40344
|
-
|
|
40345
|
-
init_terminal();
|
|
40346
|
-
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
40480
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
40347
40481
|
import { existsSync as existsSync6, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
40348
40482
|
import { createServer as createServer2 } from "node:net";
|
|
40349
40483
|
import { dirname as dirname11, isAbsolute as isAbsolute8, relative as relative13, resolve as resolve14 } from "node:path";
|
|
40350
40484
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
40351
|
-
var loopbackHosts = new Set([
|
|
40352
|
-
"127.0.0.1",
|
|
40353
|
-
"::1",
|
|
40354
|
-
"::ffff:127.0.0.1",
|
|
40355
|
-
"localhost"
|
|
40356
|
-
]);
|
|
40357
40485
|
function isLoopbackHost(host) {
|
|
40358
40486
|
return loopbackHosts.has(host) || host.startsWith("127.");
|
|
40359
40487
|
}
|
|
@@ -40528,13 +40656,13 @@ async function availableViewerPort(start, host) {
|
|
|
40528
40656
|
throw new Error(`Kennel could not find a free port at or above ${start}`);
|
|
40529
40657
|
}
|
|
40530
40658
|
function lanHost2() {
|
|
40531
|
-
const route =
|
|
40659
|
+
const route = execFileSync3("/sbin/route", ["-n", "get", "default"], {
|
|
40532
40660
|
encoding: "utf8"
|
|
40533
40661
|
});
|
|
40534
40662
|
const device = route.match(/interface:\s+(\S+)/)?.[1];
|
|
40535
40663
|
if (!device)
|
|
40536
40664
|
throw new Error("kennel could not find the local network interface");
|
|
40537
|
-
const host =
|
|
40665
|
+
const host = execFileSync3("/usr/sbin/ipconfig", ["getifaddr", device], {
|
|
40538
40666
|
encoding: "utf8"
|
|
40539
40667
|
}).trim();
|
|
40540
40668
|
if (!host)
|
|
@@ -40542,7 +40670,7 @@ function lanHost2() {
|
|
|
40542
40670
|
return host;
|
|
40543
40671
|
}
|
|
40544
40672
|
function tailscaleHost() {
|
|
40545
|
-
const status = JSON.parse(
|
|
40673
|
+
const status = JSON.parse(execFileSync3("tailscale", ["status", "--json"], { encoding: "utf8" }));
|
|
40546
40674
|
const host = typeof status.Self?.DNSName === "string" ? status.Self.DNSName.replace(/\.$/, "") : "";
|
|
40547
40675
|
if (host)
|
|
40548
40676
|
return host;
|
|
@@ -40573,7 +40701,10 @@ async function viewer(input) {
|
|
|
40573
40701
|
const existing = await runningViewer(initialViewerUrl);
|
|
40574
40702
|
if (existing && isSameOrDescendant(existing.root, options.root)) {
|
|
40575
40703
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
40576
|
-
const terminal = previewTerminal(viewerUrl, options)
|
|
40704
|
+
const terminal = previewTerminal(viewerUrl, options, async () => {
|
|
40705
|
+
await Promise.resolve().then(() => init_share());
|
|
40706
|
+
return startLanShare(options);
|
|
40707
|
+
});
|
|
40577
40708
|
if (options.open)
|
|
40578
40709
|
openPreview(viewerUrl);
|
|
40579
40710
|
try {
|
|
@@ -40598,10 +40729,13 @@ async function viewer(input) {
|
|
|
40598
40729
|
try {
|
|
40599
40730
|
await api.listen(Number(options.port));
|
|
40600
40731
|
if (options.tailscale) {
|
|
40601
|
-
|
|
40732
|
+
execFileSync3("tailscale", ["serve", "--bg", "--yes", options.port]);
|
|
40602
40733
|
}
|
|
40603
40734
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
40604
|
-
const terminal = previewTerminal(viewerUrl, options)
|
|
40735
|
+
const terminal = previewTerminal(viewerUrl, options, async () => {
|
|
40736
|
+
await Promise.resolve().then(() => init_share());
|
|
40737
|
+
return startLanShare(options);
|
|
40738
|
+
});
|
|
40605
40739
|
if (options.open)
|
|
40606
40740
|
openPreview(viewerUrl);
|
|
40607
40741
|
try {
|
|
@@ -40613,12 +40747,132 @@ async function viewer(input) {
|
|
|
40613
40747
|
await api.close();
|
|
40614
40748
|
}
|
|
40615
40749
|
if (options.tailscale) {
|
|
40616
|
-
|
|
40750
|
+
execFileSync3("tailscale", ["serve", "--https=443", "off"]);
|
|
40751
|
+
}
|
|
40752
|
+
}
|
|
40753
|
+
var loopbackHosts;
|
|
40754
|
+
var init_viewer = __esm(() => {
|
|
40755
|
+
init_document_kinds();
|
|
40756
|
+
init_terminal();
|
|
40757
|
+
loopbackHosts = new Set([
|
|
40758
|
+
"127.0.0.1",
|
|
40759
|
+
"::1",
|
|
40760
|
+
"::ffff:127.0.0.1",
|
|
40761
|
+
"localhost"
|
|
40762
|
+
]);
|
|
40763
|
+
});
|
|
40764
|
+
|
|
40765
|
+
// packages/preview/src/protected-share.ts
|
|
40766
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
40767
|
+
import { createHash as createHash6, randomBytes, timingSafeEqual, X509Certificate } from "node:crypto";
|
|
40768
|
+
import { mkdtemp, readFile as readFile11, rm as rm2 } from "node:fs/promises";
|
|
40769
|
+
import { createServer as createServer3 } from "node:https";
|
|
40770
|
+
import { isIP as isIP2 } from "node:net";
|
|
40771
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
40772
|
+
import { join as join5 } from "node:path";
|
|
40773
|
+
import { promisify } from "node:util";
|
|
40774
|
+
async function previewCertificate(host) {
|
|
40775
|
+
if (!isIP2(host))
|
|
40776
|
+
throw new Error("The Wi-Fi share requires an IP address");
|
|
40777
|
+
const directory = await mkdtemp(join5(tmpdir2(), "kennel-preview-tls-"));
|
|
40778
|
+
const keyFile = join5(directory, "key.pem");
|
|
40779
|
+
const certFile = join5(directory, "cert.pem");
|
|
40780
|
+
try {
|
|
40781
|
+
await run("openssl", [
|
|
40782
|
+
"req",
|
|
40783
|
+
"-x509",
|
|
40784
|
+
"-newkey",
|
|
40785
|
+
"rsa:2048",
|
|
40786
|
+
"-nodes",
|
|
40787
|
+
"-days",
|
|
40788
|
+
"1",
|
|
40789
|
+
"-keyout",
|
|
40790
|
+
keyFile,
|
|
40791
|
+
"-out",
|
|
40792
|
+
certFile,
|
|
40793
|
+
"-subj",
|
|
40794
|
+
"/CN=Kennel Wi-Fi preview",
|
|
40795
|
+
"-addext",
|
|
40796
|
+
`subjectAltName=IP:${host}`
|
|
40797
|
+
]);
|
|
40798
|
+
const [key, cert] = await Promise.all([readFile11(keyFile), readFile11(certFile)]);
|
|
40799
|
+
return { key, cert, fingerprint: new X509Certificate(cert).fingerprint256 };
|
|
40800
|
+
} catch (error) {
|
|
40801
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
40802
|
+
throw new Error("Password-protected sharing requires OpenSSL on PATH");
|
|
40803
|
+
}
|
|
40804
|
+
throw error;
|
|
40805
|
+
} finally {
|
|
40806
|
+
await rm2(directory, { recursive: true, force: true });
|
|
40807
|
+
}
|
|
40808
|
+
}
|
|
40809
|
+
async function startProtectedShare(host, handle) {
|
|
40810
|
+
const { key, cert, fingerprint } = await previewCertificate(host);
|
|
40811
|
+
const username = "kennel";
|
|
40812
|
+
const password = randomBytes(12).toString("base64url");
|
|
40813
|
+
const authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
40814
|
+
const expected = createHash6("sha256").update(authorization).digest();
|
|
40815
|
+
const attempts = new Map;
|
|
40816
|
+
const server = createServer3({ key, cert, minVersion: "TLSv1.2" }, (request, response) => {
|
|
40817
|
+
response.setHeader("Cache-Control", "no-store");
|
|
40818
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
40819
|
+
const address = request.socket.remoteAddress ?? "unknown";
|
|
40820
|
+
const now = Date.now();
|
|
40821
|
+
for (const [ip, attempt] of attempts) {
|
|
40822
|
+
if (attempt.expires <= now)
|
|
40823
|
+
attempts.delete(ip);
|
|
40824
|
+
}
|
|
40825
|
+
const attempt = attempts.get(address);
|
|
40826
|
+
if (attempt && attempt.count >= 10 || !attempt && attempts.size >= 1024) {
|
|
40827
|
+
response.writeHead(429, { "Retry-After": "60" });
|
|
40828
|
+
response.end("Too many sign-in attempts. Try again in one minute.");
|
|
40829
|
+
return;
|
|
40830
|
+
}
|
|
40831
|
+
const supplied = createHash6("sha256").update(request.headers.authorization ?? "").digest();
|
|
40832
|
+
if (!timingSafeEqual(supplied, expected)) {
|
|
40833
|
+
attempts.set(address, { count: (attempt?.count ?? 0) + 1, expires: attempt?.expires ?? now + 60000 });
|
|
40834
|
+
response.writeHead(401, {
|
|
40835
|
+
"WWW-Authenticate": 'Basic realm="Kennel shared preview", charset="UTF-8"'
|
|
40836
|
+
});
|
|
40837
|
+
response.end("Sign in to the shared preview.");
|
|
40838
|
+
return;
|
|
40839
|
+
}
|
|
40840
|
+
handle(request, response);
|
|
40841
|
+
});
|
|
40842
|
+
const close = async () => {
|
|
40843
|
+
attempts.clear();
|
|
40844
|
+
await new Promise((done) => {
|
|
40845
|
+
server.close(() => done());
|
|
40846
|
+
server.closeAllConnections();
|
|
40847
|
+
});
|
|
40848
|
+
};
|
|
40849
|
+
try {
|
|
40850
|
+
await new Promise((done, fail) => {
|
|
40851
|
+
server.once("error", fail);
|
|
40852
|
+
server.listen(0, host, () => {
|
|
40853
|
+
server.off("error", fail);
|
|
40854
|
+
done();
|
|
40855
|
+
});
|
|
40856
|
+
});
|
|
40857
|
+
const address = server.address();
|
|
40858
|
+
if (!address || typeof address === "string")
|
|
40859
|
+
throw new Error("No Wi-Fi server address");
|
|
40860
|
+
return { port: address.port, username, password, authorization, cert, fingerprint, close };
|
|
40861
|
+
} catch (error) {
|
|
40862
|
+
await close();
|
|
40863
|
+
throw error;
|
|
40617
40864
|
}
|
|
40618
40865
|
}
|
|
40866
|
+
var run;
|
|
40867
|
+
var init_protected_share = __esm(() => {
|
|
40868
|
+
run = promisify(execFile6);
|
|
40869
|
+
});
|
|
40619
40870
|
|
|
40620
40871
|
// packages/preview/src/share.ts
|
|
40621
|
-
|
|
40872
|
+
import { dirname as dirname12, resolve as resolve15 } from "node:path";
|
|
40873
|
+
import { existsSync as existsSync7, realpathSync as realpathSync4 } from "node:fs";
|
|
40874
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
40875
|
+
import { get } from "node:https";
|
|
40622
40876
|
function splitShareArgs(input) {
|
|
40623
40877
|
const options = {
|
|
40624
40878
|
folder: undefined,
|
|
@@ -40687,6 +40941,89 @@ async function runningViewerRoot(port = plainViewerPort) {
|
|
|
40687
40941
|
}
|
|
40688
40942
|
return;
|
|
40689
40943
|
}
|
|
40944
|
+
async function startLanShare(target) {
|
|
40945
|
+
const root = realpathSync4(target.root);
|
|
40946
|
+
const web = resolve15(dirname12(fileURLToPath4(import.meta.url)), "../../web");
|
|
40947
|
+
if (!existsSync7(resolve15(web, "index.html"))) {
|
|
40948
|
+
throw new Error("Kennel web app not found. Run the CLI build first.");
|
|
40949
|
+
}
|
|
40950
|
+
const host = lanHost2();
|
|
40951
|
+
await Promise.resolve().then(() => init_server4());
|
|
40952
|
+
process.env.KENNEL_VIEWER_HOSTS = [process.env.KENNEL_VIEWER_HOSTS, host].filter(Boolean).join(",");
|
|
40953
|
+
const api = createKennelApi({
|
|
40954
|
+
root,
|
|
40955
|
+
host: "0.0.0.0",
|
|
40956
|
+
remoteAccess: true,
|
|
40957
|
+
restrictToRoot: true,
|
|
40958
|
+
lockedFile: target.target ? realpathSync4(target.target) : undefined,
|
|
40959
|
+
webRoot: web
|
|
40960
|
+
});
|
|
40961
|
+
let shared;
|
|
40962
|
+
const close = async () => {
|
|
40963
|
+
try {
|
|
40964
|
+
await api.close();
|
|
40965
|
+
} finally {
|
|
40966
|
+
await shared?.close();
|
|
40967
|
+
}
|
|
40968
|
+
};
|
|
40969
|
+
try {
|
|
40970
|
+
shared = await startProtectedShare(host, (request, response) => {
|
|
40971
|
+
api.handle(request, response);
|
|
40972
|
+
});
|
|
40973
|
+
const url = new URL(popUrl(host, String(shared.port), {
|
|
40974
|
+
dryRun: false,
|
|
40975
|
+
host: "0.0.0.0",
|
|
40976
|
+
hot: false,
|
|
40977
|
+
lan: true,
|
|
40978
|
+
open: false,
|
|
40979
|
+
root,
|
|
40980
|
+
port: String(shared.port),
|
|
40981
|
+
tailscale: false,
|
|
40982
|
+
watch: false,
|
|
40983
|
+
target: target.target
|
|
40984
|
+
}));
|
|
40985
|
+
url.protocol = "https:";
|
|
40986
|
+
const { cert, authorization } = shared;
|
|
40987
|
+
const health = await new Promise((done, fail) => {
|
|
40988
|
+
const request = get(new URL("/api/health", url), {
|
|
40989
|
+
ca: cert,
|
|
40990
|
+
agent: false,
|
|
40991
|
+
headers: { Authorization: authorization }
|
|
40992
|
+
}, (response) => {
|
|
40993
|
+
let body = "";
|
|
40994
|
+
response.setEncoding("utf8");
|
|
40995
|
+
response.on("error", fail);
|
|
40996
|
+
response.on("data", (chunk) => {
|
|
40997
|
+
body += chunk;
|
|
40998
|
+
});
|
|
40999
|
+
response.on("end", () => {
|
|
41000
|
+
try {
|
|
41001
|
+
if (response.statusCode !== 200)
|
|
41002
|
+
throw new Error("The Wi-Fi server did not respond");
|
|
41003
|
+
done(JSON.parse(body));
|
|
41004
|
+
} catch (error) {
|
|
41005
|
+
fail(error);
|
|
41006
|
+
}
|
|
41007
|
+
});
|
|
41008
|
+
});
|
|
41009
|
+
request.setTimeout(5000, () => request.destroy(new Error("The Wi-Fi server did not respond")));
|
|
41010
|
+
request.on("error", fail);
|
|
41011
|
+
});
|
|
41012
|
+
if (health.viewer !== "kennel-core" || health.root !== root) {
|
|
41013
|
+
throw new Error("The Wi-Fi server is not serving the selected folder");
|
|
41014
|
+
}
|
|
41015
|
+
return {
|
|
41016
|
+
url: url.href,
|
|
41017
|
+
username: shared.username,
|
|
41018
|
+
password: shared.password,
|
|
41019
|
+
fingerprint: shared.fingerprint,
|
|
41020
|
+
close
|
|
41021
|
+
};
|
|
41022
|
+
} catch (error) {
|
|
41023
|
+
await close();
|
|
41024
|
+
throw error;
|
|
41025
|
+
}
|
|
41026
|
+
}
|
|
40690
41027
|
async function share(input) {
|
|
40691
41028
|
const options = splitShareArgs(input);
|
|
40692
41029
|
const folder = options.folder ?? await runningViewerRoot();
|
|
@@ -40708,9 +41045,17 @@ async function share(input) {
|
|
|
40708
41045
|
folder
|
|
40709
41046
|
]);
|
|
40710
41047
|
}
|
|
41048
|
+
var plainViewerPort = "5180";
|
|
41049
|
+
var init_share = __esm(() => {
|
|
41050
|
+
init_viewer();
|
|
41051
|
+
init_protected_share();
|
|
41052
|
+
});
|
|
41053
|
+
init_share();
|
|
41054
|
+
|
|
40711
41055
|
export {
|
|
40712
41056
|
plainViewerPort,
|
|
40713
41057
|
runningViewerRoot,
|
|
40714
41058
|
share,
|
|
40715
|
-
splitShareArgs
|
|
41059
|
+
splitShareArgs,
|
|
41060
|
+
startLanShare
|
|
40716
41061
|
};
|
package/dist/site/terminal.d.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export { openPreview
|
|
1
|
+
export { openPreview } from "kennel-preview/terminal";
|
|
2
|
+
export declare function previewTerminal(url: string, target: {
|
|
3
|
+
root: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
}): {
|
|
6
|
+
close: () => void;
|
|
7
|
+
closed: Promise<void>;
|
|
8
|
+
interactive: boolean;
|
|
9
|
+
};
|
package/dist/site/terminal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/preview/src/terminal.ts
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
2
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
|
-
import { emitKeypressEvents } from "node:readline";
|
|
4
|
+
import { clearScreenDown, cursorTo, emitKeypressEvents } from "node:readline";
|
|
5
5
|
function openPreview(url) {
|
|
6
6
|
const command = process.platform === "darwin" ? "/usr/bin/open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
7
7
|
execFile(command, [url], (error) => {
|
|
@@ -9,17 +9,40 @@ function openPreview(url) {
|
|
|
9
9
|
console.error(`Could not open browser: ${error.message}`);
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function copyPreviewLink(url) {
|
|
13
|
+
const command = process.platform === "darwin" ? "/usr/bin/pbcopy" : process.platform === "win32" ? "clip" : process.env.WAYLAND_DISPLAY ? "wl-copy" : "xclip";
|
|
14
|
+
const args = command === "xclip" ? ["-selection", "clipboard"] : [];
|
|
15
|
+
return new Promise((done, fail) => {
|
|
16
|
+
const child = execFile(command, args, (error) => error ? fail(error) : done());
|
|
17
|
+
child.stdin?.on("error", fail);
|
|
18
|
+
child.stdin?.end(url);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
var terminalText = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
22
|
+
function repositoryName(folder) {
|
|
23
|
+
try {
|
|
24
|
+
const root = execFileSync("git", ["-C", folder, "rev-parse", "--show-toplevel"], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
timeout: 1000
|
|
28
|
+
});
|
|
29
|
+
return terminalText(basename(root.trim()));
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function previewTerminal(url, target, startShare) {
|
|
13
35
|
url = new URL(url).href;
|
|
14
36
|
const input = process.stdin;
|
|
15
37
|
const output = process.stdout;
|
|
16
38
|
const terminal = Boolean(output.isTTY && process.env.TERM !== "dumb");
|
|
17
39
|
const interactive = terminal && Boolean(input.isTTY);
|
|
18
|
-
const name = (basename(target.target ?? target.root) || target.root)
|
|
19
|
-
const
|
|
40
|
+
const name = terminalText(basename(target.target ?? target.root) || target.root);
|
|
41
|
+
const title = terminalText(basename(target.root) || target.root);
|
|
42
|
+
const repository = interactive ? repositoryName(target.root) : undefined;
|
|
20
43
|
const color = terminal && process.env.NO_COLOR === undefined && output.hasColors();
|
|
21
|
-
const accent = (text) => color ? `\x1B[
|
|
22
|
-
const link = terminal ? `\x1B]8;;${url}\x1B\\${accent(label)}\x1B]8;;\x1B\\` : `${label}
|
|
44
|
+
const accent = (text) => color ? `\x1B[32m${text}\x1B[39m` : text;
|
|
45
|
+
const link = (label) => terminal ? `\x1B]8;;${url}\x1B\\${accent(label)}\x1B]8;;\x1B\\` : `${label}
|
|
23
46
|
${url}`;
|
|
24
47
|
const wasRaw = Boolean(input.isRaw);
|
|
25
48
|
const wasFlowing = input.readableFlowing === true;
|
|
@@ -28,6 +51,74 @@ function previewTerminal(url, target) {
|
|
|
28
51
|
resolveClosed = resolve;
|
|
29
52
|
});
|
|
30
53
|
let finished = false;
|
|
54
|
+
let busy = false;
|
|
55
|
+
let selected = 0;
|
|
56
|
+
let status = "";
|
|
57
|
+
let share;
|
|
58
|
+
const render = () => {
|
|
59
|
+
if (!interactive || finished)
|
|
60
|
+
return;
|
|
61
|
+
cursorTo(output, 0, 0);
|
|
62
|
+
clearScreenDown(output);
|
|
63
|
+
const width = Math.max(1, (output.columns || 80) - 5);
|
|
64
|
+
const fit = (text) => {
|
|
65
|
+
const characters = Array.from(terminalText(text));
|
|
66
|
+
return characters.length > width ? characters.slice(0, width - 1).join("") + "…" : characters.join("");
|
|
67
|
+
};
|
|
68
|
+
const count = Math.max(1, Math.min(8, (output.rows || 24) - 12));
|
|
69
|
+
const start = Math.max(0, Math.min(selected - Math.floor(count / 2), actions.length - count));
|
|
70
|
+
const choices = actions.slice(start, start + count).map((item, index) => {
|
|
71
|
+
const label = fit(item.shortcut ? `[${item.shortcut}] ${item.label}` : item.label);
|
|
72
|
+
return start + index === selected ? accent(` › ${label}`) : ` ${label}`;
|
|
73
|
+
});
|
|
74
|
+
output.write([
|
|
75
|
+
` \x1B[1m${link(fit(title))}\x1B[22m`,
|
|
76
|
+
...repository ? [` ${color ? "\x1B[2m" : ""}${fit(repository)}${color ? "\x1B[22m" : ""}`] : [],
|
|
77
|
+
...target.target ? [` File: ${fit(name)}`] : [],
|
|
78
|
+
"",
|
|
79
|
+
...choices,
|
|
80
|
+
"",
|
|
81
|
+
" ↑ ↓ to move · Enter to choose · Esc or Ctrl+C to exit",
|
|
82
|
+
...actions.length > count ? [` ${selected + 1} / ${actions.length}`] : [],
|
|
83
|
+
...status ? ["", ` ${status}`] : [],
|
|
84
|
+
""
|
|
85
|
+
].join(`
|
|
86
|
+
`));
|
|
87
|
+
};
|
|
88
|
+
const shareFolder = async () => {
|
|
89
|
+
status = "Preparing a password-protected Wi-Fi link…";
|
|
90
|
+
render();
|
|
91
|
+
try {
|
|
92
|
+
share ??= startShare();
|
|
93
|
+
let active;
|
|
94
|
+
try {
|
|
95
|
+
active = await share;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
share = undefined;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (finished)
|
|
101
|
+
return;
|
|
102
|
+
status = `Wi-Fi link: ${active.url}
|
|
103
|
+
Username: ${active.username} · Password: ${active.password}
|
|
104
|
+
Password required. Send the password separately. Same Wi-Fi only.
|
|
105
|
+
Self-signed HTTPS (24 hours): expect a certificate warning.
|
|
106
|
+
Verify SHA-256 before trusting the certificate:
|
|
107
|
+
${active.fingerprint}
|
|
108
|
+
Keep this command running. Ctrl+C stops sharing.`;
|
|
109
|
+
render();
|
|
110
|
+
try {
|
|
111
|
+
await copyPreviewLink(active.url);
|
|
112
|
+
status += `
|
|
113
|
+
Wi-Fi link copied to clipboard (without the password).`;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
status += `
|
|
116
|
+
Could not copy link: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
status = `Could not share folder: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
31
122
|
const close = () => {
|
|
32
123
|
if (finished)
|
|
33
124
|
return;
|
|
@@ -38,40 +129,91 @@ function previewTerminal(url, target) {
|
|
|
38
129
|
if (interactive) {
|
|
39
130
|
input.off("keypress", keypress);
|
|
40
131
|
input.off("end", close);
|
|
132
|
+
output.off("resize", render);
|
|
133
|
+
output.write("\x1B[?25h\x1B[?1049l");
|
|
41
134
|
input.setRawMode(wasRaw);
|
|
42
135
|
if (!wasFlowing)
|
|
43
136
|
input.pause();
|
|
44
137
|
}
|
|
45
|
-
|
|
138
|
+
if (share) {
|
|
139
|
+
share.then((active) => active.close()).catch((error) => {
|
|
140
|
+
console.error(` Wi-Fi share stopped: ${error instanceof Error ? error.message : error}`);
|
|
141
|
+
}).finally(() => resolveClosed());
|
|
142
|
+
} else
|
|
143
|
+
resolveClosed();
|
|
144
|
+
};
|
|
145
|
+
const actions = [
|
|
146
|
+
{ label: "Open in browser", shortcut: "o", run: () => {
|
|
147
|
+
openPreview(url);
|
|
148
|
+
status = "Browser open requested.";
|
|
149
|
+
} },
|
|
150
|
+
{ label: "Share to another device", shortcut: "s", run: shareFolder },
|
|
151
|
+
{ label: "Exit", shortcut: "q", run: close }
|
|
152
|
+
];
|
|
153
|
+
const activate = async () => {
|
|
154
|
+
busy = true;
|
|
155
|
+
try {
|
|
156
|
+
await actions[selected].run();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
status = `Could not complete action: ${terminalText(error instanceof Error ? error.message : String(error))}`;
|
|
159
|
+
} finally {
|
|
160
|
+
busy = false;
|
|
161
|
+
render();
|
|
162
|
+
}
|
|
46
163
|
};
|
|
47
164
|
const keypress = (_text, key) => {
|
|
48
|
-
if (key.ctrl && (key.name === "c" || key.name === "d"))
|
|
165
|
+
if (key.name === "escape" || key.ctrl && (key.name === "c" || key.name === "d")) {
|
|
49
166
|
close();
|
|
50
|
-
|
|
51
|
-
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (busy || finished)
|
|
170
|
+
return;
|
|
171
|
+
const shortcut = !key.ctrl && !key.meta ? actions.findIndex((item) => item.shortcut === key.name) : -1;
|
|
172
|
+
if (shortcut !== -1) {
|
|
173
|
+
selected = shortcut;
|
|
174
|
+
activate();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.name === "up")
|
|
178
|
+
selected = (selected - 1 + actions.length) % actions.length;
|
|
179
|
+
else if (key.name === "down")
|
|
180
|
+
selected = (selected + 1) % actions.length;
|
|
181
|
+
else if (key.name === "return") {
|
|
182
|
+
activate();
|
|
183
|
+
return;
|
|
184
|
+
} else
|
|
185
|
+
return;
|
|
186
|
+
render();
|
|
52
187
|
};
|
|
53
188
|
process.once("SIGINT", close);
|
|
54
189
|
process.once("SIGTERM", close);
|
|
55
190
|
process.once("exit", close);
|
|
56
191
|
if (interactive) {
|
|
192
|
+
output.write("\x1B[?1049h\x1B[?25l");
|
|
193
|
+
output.on("resize", render);
|
|
57
194
|
emitKeypressEvents(input);
|
|
58
195
|
input.setRawMode(true);
|
|
59
196
|
input.on("keypress", keypress);
|
|
60
197
|
input.once("end", close);
|
|
61
198
|
input.resume();
|
|
62
199
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
${accent("o")} - open in browser
|
|
69
|
-
ctrl+c - exit
|
|
200
|
+
if (interactive)
|
|
201
|
+
render();
|
|
202
|
+
else
|
|
203
|
+
console.log(`
|
|
204
|
+
${link(name)}
|
|
70
205
|
`);
|
|
71
|
-
}
|
|
72
206
|
return { close, closed, interactive };
|
|
73
207
|
}
|
|
208
|
+
|
|
209
|
+
// packages/cli/src/site/terminal.ts
|
|
210
|
+
function previewTerminal2(url, target) {
|
|
211
|
+
return previewTerminal(url, target, async () => {
|
|
212
|
+
const { startLanShare } = await import(new URL("../package/preview.js", import.meta.url).href);
|
|
213
|
+
return startLanShare(target);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
74
216
|
export {
|
|
75
217
|
openPreview,
|
|
76
|
-
previewTerminal
|
|
218
|
+
previewTerminal2 as previewTerminal
|
|
77
219
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kennel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.26",
|
|
4
4
|
"description": "Open-source pop-up previews for AI output: markdown, HTML artifacts, email, images, code, tables, notes, and calendars, from the terminal or your agent session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|