@kud/foxhop-cli 1.0.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 ADDED
@@ -0,0 +1,33 @@
1
+ # @kud/foxhop-cli
2
+
3
+ The CLI and native-messaging host for [foxhop](https://github.com/kud/foxhop) — focus a
4
+ specific Firefox tab from anywhere on macOS.
5
+
6
+ > Requires the foxhop Firefox extension. See the
7
+ > [project README](https://github.com/kud/foxhop#readme) for the full setup.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g @kud/foxhop-cli
13
+ foxhop install # register the native messaging host with Firefox
14
+ foxhop init # seed ~/.config/foxhop/tabs.json
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```sh
20
+ foxhop focus chatgpt # focus a saved target, foregrounding Firefox
21
+ foxhop focus --match figma.com --url https://figma.com
22
+ foxhop list # saved targets (--json for machine output)
23
+ foxhop tabs # currently open Firefox tabs (--json)
24
+ foxhop add gmail --match mail.google.com --url https://mail.google.com --title Gmail
25
+ foxhop remove gmail
26
+ foxhop sync # generate per-tab Raycast hotkey scripts
27
+ ```
28
+
29
+ Targets live in `~/.config/foxhop/tabs.json` (the source of truth). Each has `name`,
30
+ `match`, optional `url`, `strategy` (hostname·prefix·exact·search) and `pick`
31
+ (recent·first·pinned). Set `FOXHOP_BROWSER` if you don't run Firefox Nightly.
32
+
33
+ MIT © Erwann Mest
package/dist/cli.js ADDED
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { defineCommand, runMain } from "citty";
5
+ import { spawn } from "child_process";
6
+
7
+ // src/config.ts
8
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+ var xdgConfigHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
12
+ var CONFIG_DIR = join(xdgConfigHome, "foxhop");
13
+ var CONFIG_PATH = join(CONFIG_DIR, "tabs.json");
14
+ var SCHEMA_PATH = join(CONFIG_DIR, "tabs.schema.json");
15
+ var readConfig = () => {
16
+ if (!existsSync(CONFIG_PATH)) return { targets: [] };
17
+ try {
18
+ const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
19
+ return { targets: Array.isArray(parsed?.targets) ? parsed.targets : [] };
20
+ } catch {
21
+ return { targets: [] };
22
+ }
23
+ };
24
+ var findTarget = (config, name) => config.targets.find((target) => target.name === name);
25
+ var SCHEMA = {
26
+ $schema: "http://json-schema.org/draft-07/schema#",
27
+ title: "foxhop targets",
28
+ type: "object",
29
+ properties: {
30
+ targets: {
31
+ type: "array",
32
+ items: {
33
+ type: "object",
34
+ required: ["name", "match"],
35
+ additionalProperties: false,
36
+ properties: {
37
+ name: {
38
+ type: "string",
39
+ description: "Identifier used by `foxhop focus <name>`"
40
+ },
41
+ title: { type: "string", description: "Human-friendly label" },
42
+ match: {
43
+ type: "string",
44
+ description: "Substring matched against tab URLs/titles"
45
+ },
46
+ url: { type: "string", description: "Opened when no tab matches" },
47
+ strategy: {
48
+ enum: ["hostname", "prefix", "exact", "search"],
49
+ default: "hostname"
50
+ },
51
+ pick: {
52
+ enum: ["recent", "first", "pinned"],
53
+ default: "recent",
54
+ description: "Which tab to focus when several match"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ };
61
+ var EXAMPLE = {
62
+ targets: [
63
+ {
64
+ name: "chatgpt",
65
+ title: "ChatGPT",
66
+ match: "chatgpt.com",
67
+ url: "https://chatgpt.com"
68
+ },
69
+ {
70
+ name: "todoist",
71
+ title: "Todoist",
72
+ match: "todoist.com",
73
+ url: "https://app.todoist.com"
74
+ }
75
+ ]
76
+ };
77
+ var writeSchema = () => {
78
+ mkdirSync(CONFIG_DIR, { recursive: true });
79
+ writeFileSync(SCHEMA_PATH, JSON.stringify(SCHEMA, null, 2) + "\n");
80
+ };
81
+ var writeConfig = (config) => {
82
+ writeSchema();
83
+ writeFileSync(
84
+ CONFIG_PATH,
85
+ JSON.stringify(
86
+ { $schema: "./tabs.schema.json", targets: config.targets },
87
+ null,
88
+ 2
89
+ ) + "\n"
90
+ );
91
+ };
92
+ var writeExampleConfig = () => {
93
+ writeSchema();
94
+ if (!existsSync(CONFIG_PATH)) writeConfig(EXAMPLE);
95
+ return CONFIG_PATH;
96
+ };
97
+ var upsertTarget = (target) => {
98
+ const { targets } = readConfig();
99
+ const next = {
100
+ targets: [
101
+ ...targets.filter((existing) => existing.name !== target.name),
102
+ target
103
+ ]
104
+ };
105
+ writeConfig(next);
106
+ return next;
107
+ };
108
+ var removeTarget = (name) => {
109
+ const { targets } = readConfig();
110
+ const filtered = targets.filter((target) => target.name !== name);
111
+ const removed = filtered.length !== targets.length;
112
+ if (removed) writeConfig({ targets: filtered });
113
+ return { targets: filtered, removed };
114
+ };
115
+
116
+ // src/client.ts
117
+ import net from "net";
118
+
119
+ // src/constants.ts
120
+ import { homedir as homedir2 } from "os";
121
+ import { join as join2 } from "path";
122
+ var HOST_NAME = "io.kud.foxhop";
123
+ var EXTENSION_ID = "foxhop@kud.io";
124
+ var SOCKET_PATH = join2(homedir2(), ".foxhop.sock");
125
+ var MANIFEST_DIR = join2(
126
+ homedir2(),
127
+ "Library",
128
+ "Application Support",
129
+ "Mozilla",
130
+ "NativeMessagingHosts"
131
+ );
132
+ var MANIFEST_PATH = join2(MANIFEST_DIR, `${HOST_NAME}.json`);
133
+
134
+ // src/client.ts
135
+ var sendToHost = (request, timeoutMs = 5e3) => new Promise((resolve, reject) => {
136
+ const socket = net.createConnection(SOCKET_PATH);
137
+ let response = "";
138
+ const timer = setTimeout(() => {
139
+ socket.destroy();
140
+ reject(new Error("timeout"));
141
+ }, timeoutMs);
142
+ socket.setEncoding("utf8");
143
+ socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
144
+ socket.on("data", (chunk) => response += chunk);
145
+ socket.on("end", () => {
146
+ clearTimeout(timer);
147
+ try {
148
+ resolve(JSON.parse(response));
149
+ } catch {
150
+ resolve(null);
151
+ }
152
+ });
153
+ socket.on("error", (error) => {
154
+ clearTimeout(timer);
155
+ reject(error);
156
+ });
157
+ });
158
+
159
+ // src/install.ts
160
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, chmodSync } from "fs";
161
+ import { fileURLToPath } from "url";
162
+ import { dirname, join as join3 } from "path";
163
+ var install = () => {
164
+ const distDir = dirname(fileURLToPath(import.meta.url));
165
+ const hostEntry = join3(distDir, "host-cli.js");
166
+ chmodSync(hostEntry, 493);
167
+ mkdirSync2(MANIFEST_DIR, { recursive: true });
168
+ const manifest = {
169
+ name: HOST_NAME,
170
+ description: "foxhop native messaging host",
171
+ path: hostEntry,
172
+ type: "stdio",
173
+ allowed_extensions: [EXTENSION_ID]
174
+ };
175
+ writeFileSync2(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
176
+ process.stdout.write(
177
+ `foxhop: installed native host manifest
178
+ manifest \u2192 ${MANIFEST_PATH}
179
+ host \u2192 ${hostEntry}
180
+ `
181
+ );
182
+ };
183
+
184
+ // src/sync.ts
185
+ import {
186
+ mkdirSync as mkdirSync3,
187
+ writeFileSync as writeFileSync3,
188
+ chmodSync as chmodSync2,
189
+ existsSync as existsSync2,
190
+ readdirSync,
191
+ readFileSync as readFileSync2,
192
+ rmSync
193
+ } from "fs";
194
+ import { join as join4 } from "path";
195
+ var MARKER = "@foxhop.generated";
196
+ var defaultScriptsDir = () => join4(CONFIG_DIR, "scripts");
197
+ var stableNode = (node) => {
198
+ const index = node.indexOf("/installs/node/");
199
+ if (index === -1) return node;
200
+ const shim = node.slice(0, index) + "/shims/node";
201
+ return existsSync2(shim) ? shim : node;
202
+ };
203
+ var scriptBody = (node, cli, target) => {
204
+ const title = target.title ?? target.name;
205
+ return `#!/bin/bash
206
+
207
+ # @raycast.schemaVersion 1
208
+ # @raycast.title Focus ${title}
209
+ # @raycast.mode silent
210
+ # @raycast.packageName foxhop
211
+ # @raycast.icon \u{1F98A}
212
+
213
+ # Documentation:
214
+ # @raycast.description Focus the ${title} tab in Firefox (opens it if not already open).
215
+ # @raycast.author kud
216
+ # ${MARKER}
217
+
218
+ exec "${stableNode(node)}" "${cli}" focus ${target.name}
219
+ `;
220
+ };
221
+ var isGenerated = (path) => {
222
+ try {
223
+ return readFileSync2(path, "utf8").includes(MARKER);
224
+ } catch {
225
+ return false;
226
+ }
227
+ };
228
+ var sync = (node, cli, dir = defaultScriptsDir()) => {
229
+ const { targets } = readConfig();
230
+ mkdirSync3(dir, { recursive: true });
231
+ const wanted = new Set(targets.map((target) => `focus-${target.name}.sh`));
232
+ const removed = (existsSync2(dir) ? readdirSync(dir) : []).filter((file) => file.startsWith("focus-") && file.endsWith(".sh")).filter((file) => !wanted.has(file) && isGenerated(join4(dir, file)));
233
+ for (const file of removed) rmSync(join4(dir, file));
234
+ for (const target of targets) {
235
+ const file = join4(dir, `focus-${target.name}.sh`);
236
+ writeFileSync3(file, scriptBody(node, cli, target));
237
+ chmodSync2(file, 493);
238
+ }
239
+ return { dir, written: targets.length, removed: removed.length };
240
+ };
241
+
242
+ // src/cli.ts
243
+ import { fileURLToPath as fileURLToPath2 } from "url";
244
+ var browserApp = () => process.env.FOXHOP_BROWSER ?? "Firefox Nightly";
245
+ var foreground = () => spawn("open", ["-a", browserApp()], {
246
+ stdio: "ignore",
247
+ detached: true
248
+ }).unref();
249
+ var openUrl = (url) => spawn("open", [url], { stdio: "ignore", detached: true }).unref();
250
+ var focus = defineCommand({
251
+ meta: {
252
+ name: "focus",
253
+ description: "Focus a Firefox tab by saved target name, or an ad-hoc match"
254
+ },
255
+ args: {
256
+ name: {
257
+ type: "positional",
258
+ required: false,
259
+ description: "Saved target name (see `foxhop list`)"
260
+ },
261
+ match: {
262
+ type: "string",
263
+ description: "Ad-hoc substring to match against tab URLs"
264
+ },
265
+ url: { type: "string", description: "URL to open when no tab matches" },
266
+ strategy: {
267
+ type: "string",
268
+ description: "hostname | prefix | exact | search"
269
+ },
270
+ pick: {
271
+ type: "string",
272
+ description: "recent | first | pinned (which tab when several match)"
273
+ }
274
+ },
275
+ run: async ({ args }) => {
276
+ const request = args.match ? {
277
+ op: "focus",
278
+ match: args.match,
279
+ url: args.url,
280
+ strategy: args.strategy ?? "hostname",
281
+ pick: args.pick ?? "recent"
282
+ } : resolveNamed(String(args.name ?? ""));
283
+ if (!request) {
284
+ console.error(
285
+ `foxhop: no target named "${args.name}". Edit ${CONFIG_PATH}, or run \`foxhop list\` / \`foxhop init\`.`
286
+ );
287
+ process.exit(1);
288
+ }
289
+ try {
290
+ const ack = await sendToHost(request);
291
+ if (ack?.ok && ack.action !== "not-found") foreground();
292
+ else
293
+ console.error(
294
+ `foxhop: ${ack?.error ?? "no matching tab and no url to open"}`
295
+ );
296
+ } catch {
297
+ if ("url" in request && request.url) {
298
+ openUrl(request.url);
299
+ } else {
300
+ console.error(
301
+ "foxhop: cannot reach the host \u2014 is Firefox running with the foxhop extension?"
302
+ );
303
+ process.exit(1);
304
+ }
305
+ }
306
+ }
307
+ });
308
+ var resolveNamed = (name) => {
309
+ const target = findTarget(readConfig(), name);
310
+ if (!target) return null;
311
+ return {
312
+ op: "focus",
313
+ match: target.match,
314
+ url: target.url,
315
+ strategy: target.strategy ?? "hostname",
316
+ pick: target.pick ?? "recent"
317
+ };
318
+ };
319
+ var list = defineCommand({
320
+ meta: { name: "list", description: "List saved focus targets" },
321
+ args: { json: { type: "boolean", description: "Output JSON" } },
322
+ run: ({ args }) => {
323
+ const { targets } = readConfig();
324
+ if (args.json) {
325
+ process.stdout.write(JSON.stringify(targets, null, 2) + "\n");
326
+ return;
327
+ }
328
+ if (!targets.length) {
329
+ console.log(
330
+ `No targets yet. Run \`foxhop init\` for an example, or edit ${CONFIG_PATH}.`
331
+ );
332
+ return;
333
+ }
334
+ for (const target of targets) {
335
+ console.log(`${target.name.padEnd(16)} ${target.match}`);
336
+ }
337
+ }
338
+ });
339
+ var tabs = defineCommand({
340
+ meta: { name: "tabs", description: "List currently open Firefox tabs" },
341
+ args: { json: { type: "boolean", description: "Output JSON" } },
342
+ run: async ({ args }) => {
343
+ const ack = await sendToHost({ op: "list" }).catch(() => null);
344
+ const openTabs = ack?.tabs ?? [];
345
+ if (args.json) {
346
+ process.stdout.write(JSON.stringify(openTabs, null, 2) + "\n");
347
+ return;
348
+ }
349
+ if (!openTabs.length) {
350
+ console.error(
351
+ "foxhop: no tabs (is Firefox running with the foxhop extension?)"
352
+ );
353
+ process.exit(1);
354
+ }
355
+ for (const tab of openTabs) {
356
+ console.log(`${tab.title}
357
+ ${tab.url}`);
358
+ }
359
+ }
360
+ });
361
+ var init = defineCommand({
362
+ meta: {
363
+ name: "init",
364
+ description: "Write an example config to ~/.config/foxhop/tabs.json"
365
+ },
366
+ run: () => {
367
+ const path = writeExampleConfig();
368
+ console.log(`foxhop: wrote example config and schema next to ${path}`);
369
+ }
370
+ });
371
+ var installCommand = defineCommand({
372
+ meta: {
373
+ name: "install",
374
+ description: "Register the native messaging host manifest with Firefox"
375
+ },
376
+ run: () => install()
377
+ });
378
+ var syncCommand = defineCommand({
379
+ meta: {
380
+ name: "sync",
381
+ description: "Generate a Raycast script command per saved target (for per-tab hotkeys)"
382
+ },
383
+ args: {
384
+ dir: {
385
+ type: "string",
386
+ description: `Output directory (default: ${defaultScriptsDir()})`
387
+ }
388
+ },
389
+ run: ({ args }) => {
390
+ const result = sync(
391
+ process.execPath,
392
+ fileURLToPath2(import.meta.url),
393
+ args.dir
394
+ );
395
+ console.log(
396
+ `foxhop: wrote ${result.written} script(s)` + (result.removed ? `, removed ${result.removed} stale` : "") + ` in ${result.dir}`
397
+ );
398
+ console.log(
399
+ "Add that folder in Raycast \u2192 Extensions \u2192 Script Commands \u2192 Add Directories, then assign hotkeys."
400
+ );
401
+ }
402
+ });
403
+ var add = defineCommand({
404
+ meta: { name: "add", description: "Add or update a target in tabs.json" },
405
+ args: {
406
+ name: {
407
+ type: "positional",
408
+ required: true,
409
+ description: "Target id (used by `foxhop focus <name>`)"
410
+ },
411
+ match: {
412
+ type: "string",
413
+ required: true,
414
+ description: "Substring matched against tab URLs"
415
+ },
416
+ url: { type: "string", description: "URL opened when no tab matches" },
417
+ title: { type: "string", description: "Human-friendly label" },
418
+ strategy: {
419
+ type: "string",
420
+ description: "hostname | prefix | exact | search"
421
+ },
422
+ pick: {
423
+ type: "string",
424
+ description: "recent | first | pinned (which tab when several match)"
425
+ }
426
+ },
427
+ run: ({ args }) => {
428
+ upsertTarget({
429
+ name: String(args.name),
430
+ match: String(args.match),
431
+ url: args.url,
432
+ title: args.title,
433
+ strategy: args.strategy,
434
+ pick: args.pick
435
+ });
436
+ console.log(`foxhop: saved "${args.name}"`);
437
+ }
438
+ });
439
+ var remove = defineCommand({
440
+ meta: { name: "remove", description: "Remove a target from tabs.json" },
441
+ args: {
442
+ name: {
443
+ type: "positional",
444
+ required: true,
445
+ description: "Target id to remove"
446
+ }
447
+ },
448
+ run: ({ args }) => {
449
+ const { removed } = removeTarget(String(args.name));
450
+ if (!removed) {
451
+ console.error(`foxhop: no target named "${args.name}"`);
452
+ process.exit(1);
453
+ }
454
+ console.log(`foxhop: removed "${args.name}"`);
455
+ }
456
+ });
457
+ runMain(
458
+ defineCommand({
459
+ meta: {
460
+ name: "foxhop",
461
+ description: "Focus specific Firefox tabs from anywhere on macOS"
462
+ },
463
+ subCommands: {
464
+ focus,
465
+ list,
466
+ tabs,
467
+ add,
468
+ remove,
469
+ init,
470
+ sync: syncCommand,
471
+ install: installCommand
472
+ }
473
+ })
474
+ );
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/host.ts
4
+ import net from "net";
5
+ import { existsSync, unlinkSync } from "fs";
6
+
7
+ // src/framing.ts
8
+ import { endianness } from "os";
9
+ var littleEndian = endianness() === "LE";
10
+ var encodeMessage = (message) => {
11
+ const json = Buffer.from(JSON.stringify(message), "utf8");
12
+ const header = Buffer.allocUnsafe(4);
13
+ if (littleEndian) header.writeUInt32LE(json.length, 0);
14
+ else header.writeUInt32BE(json.length, 0);
15
+ return Buffer.concat([header, json]);
16
+ };
17
+ var createMessageReader = (onMessage) => {
18
+ let buffer = Buffer.alloc(0);
19
+ return (chunk) => {
20
+ buffer = Buffer.concat([buffer, chunk]);
21
+ while (buffer.length >= 4) {
22
+ const length = littleEndian ? buffer.readUInt32LE(0) : buffer.readUInt32BE(0);
23
+ if (buffer.length < 4 + length) break;
24
+ const json = buffer.subarray(4, 4 + length).toString("utf8");
25
+ buffer = buffer.subarray(4 + length);
26
+ onMessage(JSON.parse(json));
27
+ }
28
+ };
29
+ };
30
+
31
+ // src/constants.ts
32
+ import { homedir } from "os";
33
+ import { join } from "path";
34
+ var HOST_NAME = "io.kud.foxhop";
35
+ var EXTENSION_ID = "foxhop@kud.io";
36
+ var SOCKET_PATH = join(homedir(), ".foxhop.sock");
37
+ var MANIFEST_DIR = join(
38
+ homedir(),
39
+ "Library",
40
+ "Application Support",
41
+ "Mozilla",
42
+ "NativeMessagingHosts"
43
+ );
44
+ var MANIFEST_PATH = join(MANIFEST_DIR, `${HOST_NAME}.json`);
45
+ var REQUEST_TIMEOUT_MS = 3e3;
46
+
47
+ // src/host.ts
48
+ var runHost = () => {
49
+ const pending = /* @__PURE__ */ new Map();
50
+ let sequence = 0;
51
+ const sendToExtension = (message) => process.stdout.write(encodeMessage(message));
52
+ process.stdin.on(
53
+ "data",
54
+ createMessageReader((ack) => {
55
+ const resolve = pending.get(ack.reqId);
56
+ if (!resolve) return;
57
+ pending.delete(ack.reqId);
58
+ resolve(ack);
59
+ })
60
+ );
61
+ process.stdin.on("end", () => process.exit(0));
62
+ if (existsSync(SOCKET_PATH)) {
63
+ try {
64
+ unlinkSync(SOCKET_PATH);
65
+ } catch {
66
+ }
67
+ }
68
+ const server = net.createServer((socket) => {
69
+ socket.setEncoding("utf8");
70
+ let raw = "";
71
+ socket.on("data", (chunk) => {
72
+ raw += chunk;
73
+ const newline = raw.indexOf("\n");
74
+ if (newline === -1) return;
75
+ let request;
76
+ try {
77
+ request = JSON.parse(raw.slice(0, newline));
78
+ } catch {
79
+ socket.end(JSON.stringify({ ok: false, error: "bad-request" }));
80
+ return;
81
+ }
82
+ const reqId = ++sequence;
83
+ const timeout = setTimeout(() => {
84
+ if (!pending.has(reqId)) return;
85
+ pending.delete(reqId);
86
+ socket.end(JSON.stringify({ ok: false, error: "timeout" }));
87
+ }, REQUEST_TIMEOUT_MS);
88
+ pending.set(reqId, (ack) => {
89
+ clearTimeout(timeout);
90
+ socket.end(JSON.stringify(ack));
91
+ });
92
+ sendToExtension({ reqId, ...request });
93
+ });
94
+ });
95
+ server.on("error", (error) => {
96
+ process.stderr.write(`foxhop-host: socket error: ${error.message}
97
+ `);
98
+ });
99
+ server.listen(SOCKET_PATH);
100
+ };
101
+
102
+ // src/install.ts
103
+ import { mkdirSync, writeFileSync, chmodSync } from "fs";
104
+ import { fileURLToPath } from "url";
105
+ import { dirname, join as join2 } from "path";
106
+ var install = () => {
107
+ const distDir = dirname(fileURLToPath(import.meta.url));
108
+ const hostEntry = join2(distDir, "host-cli.js");
109
+ chmodSync(hostEntry, 493);
110
+ mkdirSync(MANIFEST_DIR, { recursive: true });
111
+ const manifest = {
112
+ name: HOST_NAME,
113
+ description: "foxhop native messaging host",
114
+ path: hostEntry,
115
+ type: "stdio",
116
+ allowed_extensions: [EXTENSION_ID]
117
+ };
118
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
119
+ process.stdout.write(
120
+ `foxhop: installed native host manifest
121
+ manifest \u2192 ${MANIFEST_PATH}
122
+ host \u2192 ${hostEntry}
123
+ `
124
+ );
125
+ };
126
+
127
+ // src/host-cli.ts
128
+ var command = process.argv[2];
129
+ if (command === "install") install();
130
+ else runHost();
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@kud/foxhop-cli",
3
+ "version": "1.0.0",
4
+ "description": "foxhop CLI and native messaging host — focus specific Firefox tabs from macOS",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Erwann Mest <m@kud.io>",
8
+ "homepage": "https://github.com/kud/foxhop#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kud/foxhop.git",
12
+ "directory": "cli"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/kud/foxhop/issues"
16
+ },
17
+ "keywords": [
18
+ "firefox",
19
+ "macos",
20
+ "raycast",
21
+ "cli",
22
+ "tabs",
23
+ "native-messaging",
24
+ "focus"
25
+ ],
26
+ "bin": {
27
+ "foxhop": "./dist/cli.js",
28
+ "foxhop-host": "./dist/host-cli.js"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsx src/cli.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "dependencies": {
47
+ "citty": "0.2.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "26.0.0",
51
+ "tsup": "8.5.1",
52
+ "tsx": "4.22.4",
53
+ "typescript": "6.0.3",
54
+ "vitest": "4.1.9"
55
+ }
56
+ }