refactor(v2): move setup/groups.ts off trunk + drop pino dep

setup/groups.ts is whatsapp-only — its inline syncScript imports baileys
and pino to fetch group metadata via Baileys.groupFetchAllParticipating.
On trunk it was a no-op for non-whatsapp users (returned early without
auth) and the only thing keeping pino alive.

Removed:
- setup/groups.ts (lives on `channels` branch; restored by /add-whatsapp-v2)
- `groups` STEPS entry from setup/index.ts
- pino from package.json (no longer used outside the moved file)

/add-whatsapp-v2 skill updated to copy setup/groups.ts and register both
groups + whatsapp-auth in setup/index.ts STEPS, install pino@9.6.0 along
with baileys + qrcode.

Verified: build clean, 326 host tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-17 14:34:16 +03:00
parent 712720eef4
commit 4857512267
5 changed files with 10 additions and 332 deletions
+9 -8
View File
@@ -17,8 +17,9 @@ Skip to **Credentials** if all of these are already in place:
- `src/channels/whatsapp.ts` exists
- `src/channels/index.ts` contains `import './whatsapp.js';`
- `setup/whatsapp-auth.ts` exists and `setup/index.ts`'s `STEPS` map contains `'whatsapp-auth':`
- `@whiskeysockets/baileys`, `qrcode` are listed in `package.json` dependencies
- `setup/whatsapp-auth.ts` and `setup/groups.ts` both exist
- `setup/index.ts`'s `STEPS` map contains both `'whatsapp-auth':` and `groups:`
- `@whiskeysockets/baileys`, `qrcode`, `pino` are listed in `package.json` dependencies
Otherwise continue. Every step below is safe to re-run.
@@ -28,11 +29,12 @@ Otherwise continue. Every step below is safe to re-run.
git fetch origin channels
```
### 2. Copy the adapter and setup step
### 2. Copy the adapter and setup steps
```bash
git show origin/channels:src/channels/whatsapp.ts > src/channels/whatsapp.ts
git show origin/channels:setup/whatsapp-auth.ts > setup/whatsapp-auth.ts
git show origin/channels:setup/groups.ts > setup/groups.ts
```
### 3. Append the self-registration import
@@ -43,22 +45,21 @@ Append to `src/channels/index.ts` (skip if already present):
import './whatsapp.js';
```
### 4. Register the setup step
### 4. Register the setup steps
In `setup/index.ts`, add this entry to the `STEPS` map (skip if already present):
In `setup/index.ts`, add these entries to the `STEPS` map (skip lines already present):
```typescript
groups: () => import('./groups.js'),
'whatsapp-auth': () => import('./whatsapp-auth.js'),
```
### 5. Install the adapter packages (pinned)
```bash
pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6
pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0
```
`pino` is already a v2 trunk dependency — no separate install needed.
### 6. Build
```bash
+1 -2
View File
@@ -25,8 +25,7 @@
"@onecli-sh/sdk": "^0.3.1",
"better-sqlite3": "11.10.0",
"chat": "^4.24.0",
"cron-parser": "5.5.0",
"pino": "^9.6.0"
"cron-parser": "5.5.0"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
-92
View File
@@ -20,9 +20,6 @@ importers:
cron-parser:
specifier: 5.5.0
version: 5.5.0
pino:
specifier: ^9.6.0
version: 9.14.0
devDependencies:
'@eslint/js':
specifier: ^9.35.0
@@ -298,9 +295,6 @@ packages:
'@oxc-project/types@0.124.0':
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@rolldown/binding-android-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -550,10 +544,6 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -1129,10 +1119,6 @@ packages:
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -1170,16 +1156,6 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
pino-std-serializers@7.1.0:
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
pino@9.14.0:
resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==}
hasBin: true
postcss@8.5.9:
resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
engines: {node: ^10 || ^12 || >=14}
@@ -1199,9 +1175,6 @@ packages:
engines: {node: '>=14'}
hasBin: true
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -1209,9 +1182,6 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@@ -1220,10 +1190,6 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -1251,10 +1217,6 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
@@ -1277,17 +1239,10 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -1316,9 +1271,6 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1674,8 +1626,6 @@ snapshots:
'@oxc-project/types@0.124.0': {}
'@pinojs/redact@0.4.0': {}
'@rolldown/binding-android-arm64@1.0.0-rc.15':
optional: true
@@ -1920,8 +1870,6 @@ snapshots:
assertion-error@2.0.1: {}
atomic-sleep@1.0.0: {}
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -2634,8 +2582,6 @@ snapshots:
obug@2.1.1: {}
on-exit-leak-free@2.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -2671,26 +2617,6 @@ snapshots:
picomatch@4.0.4: {}
pino-abstract-transport@2.0.0:
dependencies:
split2: 4.2.0
pino-std-serializers@7.1.0: {}
pino@9.14.0:
dependencies:
'@pinojs/redact': 0.4.0
atomic-sleep: 1.0.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 2.0.0
pino-std-serializers: 7.1.0
process-warning: 5.0.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.2.1
thread-stream: 3.1.0
postcss@8.5.9:
dependencies:
nanoid: 3.3.11
@@ -2716,8 +2642,6 @@ snapshots:
prettier@3.8.2: {}
process-warning@5.0.0: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
@@ -2725,8 +2649,6 @@ snapshots:
punycode@2.3.1: {}
quick-format-unescaped@4.0.4: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
@@ -2740,8 +2662,6 @@ snapshots:
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {}
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -2797,8 +2717,6 @@ snapshots:
safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
semver@7.7.4: {}
shebang-command@2.0.0:
@@ -2817,14 +2735,8 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
sonic-boom@4.2.1:
dependencies:
atomic-sleep: 1.0.0
source-map-js@1.2.1: {}
split2@4.2.0: {}
stackback@0.0.2: {}
std-env@4.0.0: {}
@@ -2856,10 +2768,6 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
tinybench@2.9.0: {}
tinyexec@1.1.1: {}
-229
View File
@@ -1,229 +0,0 @@
/**
* Step: groups — Fetch group metadata from messaging platforms, write to DB.
* WhatsApp requires an upfront sync (Baileys groupFetchAllParticipating).
* Other channels discover group names at runtime — this step auto-skips for them.
* Replaces 05-sync-groups.sh + 05b-list-groups.sh
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import Database from 'better-sqlite3';
import { STORE_DIR } from '../src/config.js';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
function parseArgs(args: string[]): { list: boolean; limit: number } {
let list = false;
let limit = 30;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--list') list = true;
if (args[i] === '--limit' && args[i + 1]) {
limit = parseInt(args[i + 1], 10);
i++;
}
}
return { list, limit };
}
export async function run(args: string[]): Promise<void> {
const projectRoot = process.cwd();
const { list, limit } = parseArgs(args);
if (list) {
await listGroups(limit);
return;
}
await syncGroups(projectRoot);
}
async function listGroups(limit: number): Promise<void> {
const dbPath = path.join(STORE_DIR, 'messages.db');
if (!fs.existsSync(dbPath)) {
console.error('ERROR: database not found');
process.exit(1);
}
const db = new Database(dbPath, { readonly: true });
const rows = db
.prepare(
`SELECT jid, name FROM chats
WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__' AND name <> jid
ORDER BY last_message_time DESC
LIMIT ?`,
)
.all(limit) as Array<{ jid: string; name: string }>;
db.close();
for (const row of rows) {
console.log(`${row.jid}|${row.name}`);
}
}
async function syncGroups(projectRoot: string): Promise<void> {
// Only WhatsApp needs an upfront group sync; other channels resolve names at runtime.
// Detect WhatsApp by checking for auth credentials on disk.
const authDir = path.join(projectRoot, 'store', 'auth');
const hasWhatsAppAuth =
fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0;
if (!hasWhatsAppAuth) {
log.info('WhatsApp auth not found — skipping group sync');
emitStatus('SYNC_GROUPS', {
BUILD: 'skipped',
SYNC: 'skipped',
GROUPS_IN_DB: 0,
REASON: 'whatsapp_not_configured',
STATUS: 'success',
LOG: 'logs/setup.log',
});
return;
}
// Build TypeScript first
log.info('Building TypeScript');
let buildOk = false;
try {
execSync('pnpm run build', {
cwd: projectRoot,
stdio: ['ignore', 'pipe', 'pipe'],
});
buildOk = true;
log.info('Build succeeded');
} catch {
log.error('Build failed');
emitStatus('SYNC_GROUPS', {
BUILD: 'failed',
SYNC: 'skipped',
GROUPS_IN_DB: 0,
STATUS: 'failed',
ERROR: 'build_failed',
LOG: 'logs/setup.log',
});
process.exit(1);
}
// Run sync script via a temp file to avoid shell escaping issues with node -e
log.info('Fetching group metadata');
let syncOk = false;
try {
const syncScript = `
import makeWASocket, { useMultiFileAuthState, makeCacheableSignalKeyStore, Browsers } from '@whiskeysockets/baileys';
import pino from 'pino';
import path from 'path';
import fs from 'fs';
import Database from 'better-sqlite3';
const logger = pino({ level: 'silent' });
const authDir = path.join('store', 'auth');
const dbPath = path.join('store', 'messages.db');
if (!fs.existsSync(authDir)) {
console.error('NO_AUTH');
process.exit(1);
}
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.exec('CREATE TABLE IF NOT EXISTS chats (jid TEXT PRIMARY KEY, name TEXT, last_message_time TEXT)');
const upsert = db.prepare(
'INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?) ON CONFLICT(jid) DO UPDATE SET name = excluded.name'
);
const { state, saveCreds } = await useMultiFileAuthState(authDir);
const sock = makeWASocket({
auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) },
printQRInTerminal: false,
logger,
browser: Browsers.macOS('Chrome'),
});
const timeout = setTimeout(() => {
console.error('TIMEOUT');
process.exit(1);
}, 30000);
sock.ev.on('creds.update', saveCreds);
sock.ev.on('connection.update', async (update) => {
if (update.connection === 'open') {
try {
const groups = await sock.groupFetchAllParticipating();
const now = new Date().toISOString();
let count = 0;
for (const [jid, metadata] of Object.entries(groups)) {
if (metadata.subject) {
upsert.run(jid, metadata.subject, now);
count++;
}
}
console.log('SYNCED:' + count);
} catch (err) {
console.error('FETCH_ERROR:' + err.message);
} finally {
clearTimeout(timeout);
sock.end(undefined);
db.close();
process.exit(0);
}
} else if (update.connection === 'close') {
clearTimeout(timeout);
console.error('CONNECTION_CLOSED');
process.exit(1);
}
});
`;
const tmpScript = path.join(projectRoot, '.tmp-group-sync.mjs');
fs.writeFileSync(tmpScript, syncScript, 'utf-8');
try {
const output = execSync(`node ${tmpScript}`, {
cwd: projectRoot,
encoding: 'utf-8',
timeout: 45000,
stdio: ['ignore', 'pipe', 'pipe'],
});
syncOk = output.includes('SYNCED:');
log.info('Sync output', { output: output.trim() });
} finally {
try { fs.unlinkSync(tmpScript); } catch { /* ignore cleanup errors */ }
}
} catch (err) {
log.error('Sync failed', { err });
}
// Count groups in DB using better-sqlite3 (no sqlite3 CLI)
let groupsInDb = 0;
const dbPath = path.join(STORE_DIR, 'messages.db');
if (fs.existsSync(dbPath)) {
try {
const db = new Database(dbPath, { readonly: true });
const row = db
.prepare(
"SELECT COUNT(*) as count FROM chats WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__'",
)
.get() as { count: number };
groupsInDb = row.count;
db.close();
} catch {
// DB may not exist yet
}
}
const status = syncOk ? 'success' : 'failed';
emitStatus('SYNC_GROUPS', {
BUILD: buildOk ? 'success' : 'failed',
SYNC: syncOk ? 'success' : 'failed',
GROUPS_IN_DB: groupsInDb,
STATUS: status,
LOG: 'logs/setup.log',
});
if (status === 'failed') process.exit(1);
}
-1
View File
@@ -12,7 +12,6 @@ const STEPS: Record<
timezone: () => import('./timezone.js'),
environment: () => import('./environment.js'),
container: () => import('./container.js'),
groups: () => import('./groups.js'),
register: () => import('./register.js'),
mounts: () => import('./mounts.js'),
service: () => import('./service.js'),