cli-whatsapp 0.1.4 → 0.1.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.
- package/assets/wa-icon.png +0 -0
- package/dist/cli/commands/media.js +42 -21
- package/dist/cli/commands/shell.js +71 -13
- package/dist/cli/index.js +1 -0
- package/dist/db/sqlite.js +19 -13
- package/dist/utils/notify.js +64 -14
- package/dist/utils/open.js +17 -0
- package/package.json +2 -1
|
Binary file
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
1
|
+
import { writeFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { proto, downloadMediaMessage } from '@whiskeysockets/baileys';
|
|
4
4
|
import { runWithConnection } from "../../whatsapp/client.js";
|
|
5
5
|
import { logger } from "../../whatsapp/logger.js";
|
|
6
|
+
import { openFile } from "../../utils/open.js";
|
|
6
7
|
import { listMedia, getMessageById, setMediaPath, findChat, } from "../../db/sqlite.js";
|
|
7
8
|
import { PATHS, ensureDirs, loadConfig } from "../../config/index.js";
|
|
8
9
|
import { c, formatTime, displayName, truncate } from "../../utils/format.js";
|
|
@@ -22,7 +23,8 @@ const TYPE_ALIASES = {
|
|
|
22
23
|
stickers: ['sticker'],
|
|
23
24
|
all: [],
|
|
24
25
|
};
|
|
25
|
-
/** wa media [type] [--limit N] — list media messages from the cache.
|
|
26
|
+
/** wa media [type] [--limit N] — list media messages from the cache. In the
|
|
27
|
+
* interactive app, `opts.chat` scopes it to the open chat. */
|
|
26
28
|
export function media(type, opts) {
|
|
27
29
|
const limit = Math.min(parseInt(opts.limit ?? '30', 10) || 30, 1000);
|
|
28
30
|
let types = null;
|
|
@@ -33,11 +35,15 @@ export function media(type, opts) {
|
|
|
33
35
|
}
|
|
34
36
|
types = mapped.length ? mapped : null;
|
|
35
37
|
}
|
|
36
|
-
const rows = listMedia(types, limit);
|
|
38
|
+
const rows = listMedia(types, limit, opts.chat?.jid);
|
|
37
39
|
if (rows.length === 0) {
|
|
38
|
-
print(c.dim(
|
|
40
|
+
print(c.dim(opts.chat
|
|
41
|
+
? `No media in this chat with ${opts.chat.name}.`
|
|
42
|
+
: 'No media cached. Run `wa sync` after logging in.'));
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
45
|
+
if (opts.chat)
|
|
46
|
+
print(c.dim(`Media with ${c.bold(opts.chat.name)}:`));
|
|
41
47
|
const cfg = loadConfig();
|
|
42
48
|
const width = String(rows[rows.length - 1].id).length;
|
|
43
49
|
for (const m of rows) {
|
|
@@ -52,7 +58,32 @@ export function media(type, opts) {
|
|
|
52
58
|
print();
|
|
53
59
|
info('Download one with `wa download <id>`.');
|
|
54
60
|
}
|
|
55
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Fetch & decrypt a cached media message over an already-open socket, save it,
|
|
63
|
+
* and return the file path. Reused by the CLI `download` and the interactive
|
|
64
|
+
* `/download` (which reuses the live connection). Returns null if #id has no
|
|
65
|
+
* downloadable media; caches an already-downloaded file's path.
|
|
66
|
+
*/
|
|
67
|
+
export async function downloadWithSocket(sock, id, out) {
|
|
68
|
+
const row = getMessageById(id);
|
|
69
|
+
if (!row || !row.raw)
|
|
70
|
+
return null;
|
|
71
|
+
if (!out && row.mediaPath && existsSync(row.mediaPath))
|
|
72
|
+
return row.mediaPath; // already saved
|
|
73
|
+
ensureDirs();
|
|
74
|
+
const content = proto.Message.decode(Buffer.from(row.raw, 'base64'));
|
|
75
|
+
const waMsg = {
|
|
76
|
+
key: { remoteJid: row.chatJid, id: row.messageId, fromMe: !!row.fromMe },
|
|
77
|
+
message: content,
|
|
78
|
+
};
|
|
79
|
+
const buffer = (await downloadMediaMessage(waMsg, 'buffer', {}, { logger: logger, reuploadRequest: sock.updateMediaMessage }));
|
|
80
|
+
const ext = extensionFor(content, row.type);
|
|
81
|
+
const file = out ?? join(PATHS.media, `${id}${ext}`);
|
|
82
|
+
writeFileSync(file, buffer);
|
|
83
|
+
setMediaPath(id, file);
|
|
84
|
+
return file;
|
|
85
|
+
}
|
|
86
|
+
/** wa download <id> [--out FILE] [--open] — fetch & decrypt a cached media message. */
|
|
56
87
|
export async function download(idRaw, opts) {
|
|
57
88
|
const id = parseInt(idRaw, 10);
|
|
58
89
|
if (!Number.isFinite(id))
|
|
@@ -62,23 +93,13 @@ export async function download(idRaw, opts) {
|
|
|
62
93
|
fail(`No cached message with id #${id}.`);
|
|
63
94
|
if (!row.raw)
|
|
64
95
|
fail(`Message #${id} has no downloadable media (text message?).`);
|
|
65
|
-
ensureDirs();
|
|
66
96
|
info(`Downloading media from #${id}…`);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
message: content,
|
|
75
|
-
};
|
|
76
|
-
const buffer = await runWithConnection(async (conn) => (await downloadMediaMessage(waMsg, 'buffer', {}, { logger: logger, reuploadRequest: conn.sock.updateMediaMessage })), { requireSession: true }).catch((err) => fail(`Download failed: ${err.message}`));
|
|
77
|
-
const ext = extensionFor(content, row.type);
|
|
78
|
-
const file = opts.out ?? join(PATHS.media, `${id}${ext}`);
|
|
79
|
-
writeFileSync(file, buffer);
|
|
80
|
-
setMediaPath(id, file);
|
|
81
|
-
ok(`Saved → ${file} (${(buffer.length / 1024).toFixed(1)} KB)`);
|
|
97
|
+
const file = await runWithConnection((conn) => downloadWithSocket(conn.sock, id, opts.out), { requireSession: true }).catch((err) => fail(`Download failed: ${err.message}`));
|
|
98
|
+
if (!file)
|
|
99
|
+
fail('Nothing to download.');
|
|
100
|
+
ok(`Saved → ${file}`);
|
|
101
|
+
if (opts.open)
|
|
102
|
+
openFile(file);
|
|
82
103
|
}
|
|
83
104
|
function extensionFor(content, type) {
|
|
84
105
|
const mime = content.imageMessage?.mimetype ||
|
|
@@ -4,13 +4,14 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { chats, contacts, search, history } from "./read.js";
|
|
6
6
|
import { reply, buildMediaContent, sendContent } from "./send.js";
|
|
7
|
-
import { media } from "./media.js";
|
|
7
|
+
import { media, downloadWithSocket } from "./media.js";
|
|
8
8
|
import { pickFile, pickFromList } from "./filepicker.js";
|
|
9
|
+
import { openFile } from "../../utils/open.js";
|
|
9
10
|
import { recent, pin, unpin, archive, unarchive } from "./manage.js";
|
|
10
11
|
import { status } from "./auth.js";
|
|
11
12
|
import { connect, hasSession } from "../../whatsapp/client.js";
|
|
12
|
-
import { registerPersistence, extractText, isGroupJid } from "../../whatsapp/events.js";
|
|
13
|
-
import { listChats, listContacts, findChat, upsertChat, getMessages, countMessages, } from "../../db/sqlite.js";
|
|
13
|
+
import { registerPersistence, extractText, isGroupJid, messageType } from "../../whatsapp/events.js";
|
|
14
|
+
import { listChats, listContacts, findChat, upsertChat, getMessages, getMessageById, localIdForWaMessage, countMessages, } from "../../db/sqlite.js";
|
|
14
15
|
import { resolveTarget } from "../../utils/jid.js";
|
|
15
16
|
import { loadConfig, saveConfig } from "../../config/index.js";
|
|
16
17
|
import { c, displayName, chatNumber, formatTime, truncate, renderMessages } from "../../utils/format.js";
|
|
@@ -162,6 +163,19 @@ function printAbove(state, text) {
|
|
|
162
163
|
process.stdout.write(text + '\n');
|
|
163
164
|
state.rl.prompt(true);
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Replace the line the user just submitted (their echoed input) with `text`, so
|
|
168
|
+
* an outgoing message shows once as a bubble instead of appearing twice. Called
|
|
169
|
+
* synchronously while processing the line, when the cursor sits just below the
|
|
170
|
+
* submitted input.
|
|
171
|
+
*/
|
|
172
|
+
function replaceSubmitted(state, text) {
|
|
173
|
+
readline.moveCursor(process.stdout, 0, -1); // up onto the submitted-input line
|
|
174
|
+
readline.cursorTo(process.stdout, 0);
|
|
175
|
+
readline.clearLine(process.stdout, 0);
|
|
176
|
+
process.stdout.write(text + '\n');
|
|
177
|
+
state.rl.prompt(true);
|
|
178
|
+
}
|
|
165
179
|
function bubble(who, text, incoming) {
|
|
166
180
|
const time = c.gray(formatTime(Date.now(), loadConfig().preferences.time24h));
|
|
167
181
|
const name = incoming ? c.cyan(who) : c.green('me');
|
|
@@ -174,7 +188,16 @@ function onIncoming(state, msg) {
|
|
|
174
188
|
const jid = msg.key.remoteJid;
|
|
175
189
|
if (!jid || jid === 'status@broadcast')
|
|
176
190
|
return;
|
|
177
|
-
|
|
191
|
+
let text = extractText(msg.message);
|
|
192
|
+
if (!text) {
|
|
193
|
+
// Media: show its kind and a one-liner to download & open it.
|
|
194
|
+
const kind = messageType(msg.message);
|
|
195
|
+
const localId = msg.key.id ? localIdForWaMessage(jid, msg.key.id) : null;
|
|
196
|
+
text =
|
|
197
|
+
localId != null
|
|
198
|
+
? c.dim(`[${kind}] `) + c.green(`/dl ${localId}`) + c.dim(' to open')
|
|
199
|
+
: c.dim(`[${kind}]`);
|
|
200
|
+
}
|
|
178
201
|
const chat = findChat(jid);
|
|
179
202
|
const who = isGroupJid(jid) && msg.pushName
|
|
180
203
|
? `${chat ? displayName(chat) : jid.split('@')[0]}/${msg.pushName}`
|
|
@@ -233,20 +256,51 @@ async function ensureConnected(state) {
|
|
|
233
256
|
}
|
|
234
257
|
}
|
|
235
258
|
// ---- sending ---------------------------------------------------------------
|
|
236
|
-
async function sendTo(state, jid, name, content, meta) {
|
|
259
|
+
async function sendTo(state, jid, name, content, meta, replaceInput = false) {
|
|
260
|
+
const wasLive = state.status === 'live';
|
|
237
261
|
const conn = await ensureConnected(state);
|
|
238
262
|
if (!conn)
|
|
239
263
|
return;
|
|
240
264
|
try {
|
|
241
265
|
await sendContent(conn.sock, jid, content, meta);
|
|
242
266
|
const echo = meta.type === 'text' ? meta.previewText : `[${meta.type}] ${meta.previewText}`;
|
|
243
|
-
|
|
267
|
+
// Replace the just-typed input with the bubble so it shows once — but only
|
|
268
|
+
// when we were already live (no "connecting…" lines were printed in between).
|
|
269
|
+
if (replaceInput && wasLive)
|
|
270
|
+
replaceSubmitted(state, bubble(name, echo, false));
|
|
271
|
+
else
|
|
272
|
+
printAbove(state, bubble(name, echo, false));
|
|
244
273
|
refreshNames();
|
|
245
274
|
}
|
|
246
275
|
catch (err) {
|
|
247
276
|
warn(`Send failed: ${err.message}`);
|
|
248
277
|
}
|
|
249
278
|
}
|
|
279
|
+
/** Download a media message by #id over the live connection, then open it. */
|
|
280
|
+
async function downloadAndOpen(state, idRaw) {
|
|
281
|
+
const id = parseInt(idRaw, 10);
|
|
282
|
+
if (!Number.isFinite(id))
|
|
283
|
+
return warn(`Invalid id "${idRaw}".`);
|
|
284
|
+
const row = getMessageById(id);
|
|
285
|
+
if (!row)
|
|
286
|
+
return warn(`No message #${id}.`);
|
|
287
|
+
if (!row.raw)
|
|
288
|
+
return warn(`#${id} isn't a downloadable media message.`);
|
|
289
|
+
const conn = await ensureConnected(state);
|
|
290
|
+
if (!conn)
|
|
291
|
+
return;
|
|
292
|
+
printAbove(state, c.dim(`Downloading #${id}…`));
|
|
293
|
+
try {
|
|
294
|
+
const file = await downloadWithSocket(conn.sock, id);
|
|
295
|
+
if (!file)
|
|
296
|
+
return warn(`Couldn't download #${id}.`);
|
|
297
|
+
openFile(file);
|
|
298
|
+
printAbove(state, `${c.green('✓')} Opened ${c.dim(file)}`);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
warn(`Download failed: ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
250
304
|
// ---- chat context ----------------------------------------------------------
|
|
251
305
|
function openChat(state, target) {
|
|
252
306
|
// Resolve against chats, contacts, or a raw number/jid.
|
|
@@ -323,6 +377,7 @@ const HELP = `${c.bold('Commands')} ${c.dim('(slash optional for the first word)
|
|
|
323
377
|
${c.cyan('/sendfile')} <chat> <path> [caption] send media
|
|
324
378
|
${c.cyan('/reply')} <id> <msg> reply to a message by #id
|
|
325
379
|
${c.cyan('/media')} [type] list media
|
|
380
|
+
${c.cyan('/dl')} <id> download a media message and open it
|
|
326
381
|
${c.cyan('/pin')} · ${c.cyan('/unpin')} · ${c.cyan('/archive')} · ${c.cyan('/unarchive')} <chat>
|
|
327
382
|
${c.cyan('/connect')} · ${c.cyan('/disconnect')} go live (receive) / drop connection
|
|
328
383
|
${c.cyan('/status')} · ${c.cyan('/settings')} · ${c.cyan('/set')} <k> <v> status / settings
|
|
@@ -349,11 +404,7 @@ async function handle(state, raw) {
|
|
|
349
404
|
if (at) {
|
|
350
405
|
try {
|
|
351
406
|
const built = buildMediaContent(at.file, at.caption || undefined);
|
|
352
|
-
await sendTo(state, jid, name, built.content, {
|
|
353
|
-
previewText: at.caption || `[${built.kind}]`,
|
|
354
|
-
type: built.kind,
|
|
355
|
-
mediaPath: built.path,
|
|
356
|
-
});
|
|
407
|
+
await sendTo(state, jid, name, built.content, { previewText: at.caption || `[${built.kind}]`, type: built.kind, mediaPath: built.path }, true);
|
|
357
408
|
}
|
|
358
409
|
catch (err) {
|
|
359
410
|
printError(err);
|
|
@@ -361,7 +412,7 @@ async function handle(state, raw) {
|
|
|
361
412
|
return;
|
|
362
413
|
}
|
|
363
414
|
// Otherwise plain text.
|
|
364
|
-
await sendTo(state, jid, name, { text: line }, { previewText: line, type: 'text' });
|
|
415
|
+
await sendTo(state, jid, name, { text: line }, { previewText: line, type: 'text' }, true);
|
|
365
416
|
return;
|
|
366
417
|
}
|
|
367
418
|
const [cmdRaw, ...args] = tokenize(line);
|
|
@@ -420,7 +471,14 @@ async function handle(state, raw) {
|
|
|
420
471
|
search(args.join(' '), {});
|
|
421
472
|
return;
|
|
422
473
|
case 'media':
|
|
423
|
-
media
|
|
474
|
+
// Inside a chat → only that chat's media; outside → everything.
|
|
475
|
+
media(args[0], state.active ? { chat: { jid: state.active.jid, name: displayName(state.active) } } : {});
|
|
476
|
+
return;
|
|
477
|
+
case 'download':
|
|
478
|
+
case 'dl':
|
|
479
|
+
case 'open-media':
|
|
480
|
+
if (need(args[0], 'id'))
|
|
481
|
+
await downloadAndOpen(state, args[0]);
|
|
424
482
|
return;
|
|
425
483
|
case 'status':
|
|
426
484
|
status();
|
package/dist/cli/index.js
CHANGED
|
@@ -126,6 +126,7 @@ program
|
|
|
126
126
|
.command('download <id>')
|
|
127
127
|
.description('Download & decrypt a cached media message by #id')
|
|
128
128
|
.option('-o, --out <file>', 'output file')
|
|
129
|
+
.option('--open', 'open the file after downloading')
|
|
129
130
|
.action(run(download));
|
|
130
131
|
// ---- Organize (local) ------------------------------------------------------
|
|
131
132
|
program
|
package/dist/db/sqlite.js
CHANGED
|
@@ -249,20 +249,19 @@ export function importLidMappings() {
|
|
|
249
249
|
return n;
|
|
250
250
|
}
|
|
251
251
|
/** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
|
|
252
|
-
export function listMedia(types, limit = 50) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
export function listMedia(types, limit = 50, chatJid) {
|
|
253
|
+
const kinds = types && types.length ? types : ['image', 'video', 'audio', 'document', 'sticker'];
|
|
254
|
+
const placeholders = kinds.map(() => '?').join(',');
|
|
255
|
+
const params = [...kinds];
|
|
256
|
+
let where = `type IN (${placeholders})`;
|
|
257
|
+
if (chatJid) {
|
|
258
|
+
where += ' AND chatJid = ?';
|
|
259
|
+
params.push(chatJid);
|
|
260
260
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
.all(limit);
|
|
261
|
+
params.push(limit);
|
|
262
|
+
return getDb()
|
|
263
|
+
.prepare(`SELECT * FROM messages WHERE ${where} ORDER BY timestamp DESC LIMIT ?`)
|
|
264
|
+
.all(...params);
|
|
266
265
|
}
|
|
267
266
|
/** Recent chats by activity, ignoring pin priority. */
|
|
268
267
|
export function recentChats(limit = 15) {
|
|
@@ -286,6 +285,13 @@ export function findContact(query) {
|
|
|
286
285
|
.get(likePattern(query), query);
|
|
287
286
|
return row ?? null;
|
|
288
287
|
}
|
|
288
|
+
/** Local numeric id for a WhatsApp message (by chat + WA message id). */
|
|
289
|
+
export function localIdForWaMessage(chatJid, waId) {
|
|
290
|
+
const row = getDb()
|
|
291
|
+
.prepare('SELECT id FROM messages WHERE chatJid = ? AND messageId = ?')
|
|
292
|
+
.get(chatJid, waId);
|
|
293
|
+
return row?.id ?? null;
|
|
294
|
+
}
|
|
289
295
|
export function getMessageById(id) {
|
|
290
296
|
const row = getDb()
|
|
291
297
|
.prepare('SELECT * FROM messages WHERE id = ?')
|
package/dist/utils/notify.js
CHANGED
|
@@ -1,23 +1,73 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
2
|
import { platform } from 'node:os';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
// The branded "wa" icon, shipped in assets/. Two levels up works from both
|
|
6
|
+
// dist/utils (published) and src/utils (dev).
|
|
7
|
+
const ICON = (() => {
|
|
8
|
+
try {
|
|
9
|
+
const p = fileURLToPath(new URL('../../assets/wa-icon.png', import.meta.url));
|
|
10
|
+
return existsSync(p) ? p : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
let cached = null;
|
|
17
|
+
/** Pick the best available notifier once. terminal-notifier lets us show the
|
|
18
|
+
* branded icon on macOS; osascript is the zero-dependency fallback. */
|
|
19
|
+
function detect() {
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
const p = platform();
|
|
23
|
+
if (p === 'darwin') {
|
|
24
|
+
try {
|
|
25
|
+
execFileSync('which', ['terminal-notifier'], { stdio: 'ignore' });
|
|
26
|
+
cached = 'terminal-notifier';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
cached = 'osascript';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (p === 'linux') {
|
|
33
|
+
cached = 'notify-send';
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
cached = 'none';
|
|
37
|
+
}
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
3
40
|
/**
|
|
4
|
-
* Fire a native desktop notification. Best-effort and non-blocking
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
41
|
+
* Fire a native desktop notification. Best-effort and non-blocking. Uses the
|
|
42
|
+
* branded icon where the platform allows it (terminal-notifier on macOS,
|
|
43
|
+
* notify-send on Linux); plain macOS `osascript` can't set a custom icon and
|
|
44
|
+
* shows the terminal's.
|
|
8
45
|
*/
|
|
9
46
|
export function notify(title, body) {
|
|
10
47
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
const t = clip(title);
|
|
49
|
+
const b = clip(body);
|
|
50
|
+
switch (detect()) {
|
|
51
|
+
case 'terminal-notifier': {
|
|
52
|
+
const args = ['-title', t, '-message', b, '-group', 'cli-whatsapp'];
|
|
53
|
+
if (ICON)
|
|
54
|
+
args.push('-appIcon', ICON, '-contentImage', ICON);
|
|
55
|
+
execFile('terminal-notifier', args, () => { });
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'osascript': {
|
|
59
|
+
const esc = (s) => s.replace(/[\\"]/g, '\\$&');
|
|
60
|
+
execFile('osascript', ['-e', `display notification "${esc(b)}" with title "${esc(t)}"`], () => { });
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'notify-send': {
|
|
64
|
+
const args = ['-a', 'WhatsApp CLI'];
|
|
65
|
+
if (ICON)
|
|
66
|
+
args.push('-i', ICON);
|
|
67
|
+
execFile('notify-send', [...args, t, b], () => { });
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
19
70
|
}
|
|
20
|
-
// Windows: no reliable dependency-free path; skipped intentionally.
|
|
21
71
|
}
|
|
22
72
|
catch {
|
|
23
73
|
/* notifications are best-effort */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { platform } from 'node:os';
|
|
3
|
+
/** Open a file with the OS default application (best-effort, non-blocking). */
|
|
4
|
+
export function openFile(path) {
|
|
5
|
+
try {
|
|
6
|
+
const p = platform();
|
|
7
|
+
if (p === 'darwin')
|
|
8
|
+
execFile('open', [path], () => { });
|
|
9
|
+
else if (p === 'win32')
|
|
10
|
+
execFile('cmd', ['/c', 'start', '', path], () => { });
|
|
11
|
+
else
|
|
12
|
+
execFile('xdg-open', [path], () => { });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* best-effort */
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-whatsapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "A lightweight, fast, terminal-first WhatsApp client. Read, search, send, media, groups, and live notifications — from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
11
|
"dist/",
|
|
12
|
+
"assets/",
|
|
12
13
|
"README.md",
|
|
13
14
|
"COMMANDS.md",
|
|
14
15
|
"LICENSE"
|