refactor: remove legacy container and non-discord remnants
This commit is contained in:
@@ -6,7 +6,7 @@ import Database from 'better-sqlite3';
|
||||
/**
|
||||
* Tests for the environment check step.
|
||||
*
|
||||
* Verifies: config detection, Docker/AC detection, DB queries.
|
||||
* Verifies: config detection, platform helpers, DB queries.
|
||||
*/
|
||||
|
||||
describe('environment detection', () => {
|
||||
@@ -28,7 +28,7 @@ describe('registered groups DB query', () => {
|
||||
folder TEXT NOT NULL UNIQUE,
|
||||
trigger_pattern TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
container_config TEXT,
|
||||
agent_config TEXT,
|
||||
requires_trigger INTEGER DEFAULT 1
|
||||
)`);
|
||||
});
|
||||
@@ -96,7 +96,7 @@ describe('credentials detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Docker detection logic', () => {
|
||||
describe('platform command detection', () => {
|
||||
it('commandExists returns boolean', async () => {
|
||||
const { commandExists } = await import('./platform.js');
|
||||
expect(typeof commandExists('docker')).toBe('boolean');
|
||||
@@ -104,18 +104,3 @@ describe('Docker detection logic', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('channel auth detection', () => {
|
||||
it('detects non-empty auth directory', () => {
|
||||
const hasAuth = (authDir: string) => {
|
||||
try {
|
||||
return fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Non-existent directory
|
||||
expect(hasAuth('/tmp/nonexistent_auth_dir_xyz')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
174
setup/groups.ts
174
setup/groups.ts
@@ -1,10 +1,7 @@
|
||||
/**
|
||||
* 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
|
||||
* Step: groups — List known Discord groups from the local chat metadata store.
|
||||
* Discord channel names are discovered at runtime, so the sync path is a no-op.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
@@ -28,7 +25,6 @@ function parseArgs(args: string[]): { list: boolean; limit: number } {
|
||||
}
|
||||
|
||||
export async function run(args: string[]): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const { list, limit } = parseArgs(args);
|
||||
|
||||
if (list) {
|
||||
@@ -36,7 +32,7 @@ export async function run(args: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
await syncGroups(projectRoot);
|
||||
await syncGroups();
|
||||
}
|
||||
|
||||
async function listGroups(limit: number): Promise<void> {
|
||||
@@ -51,9 +47,9 @@ async function listGroups(limit: number): Promise<void> {
|
||||
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 ?`,
|
||||
WHERE jid LIKE 'dc:%' AND is_group = 1 AND jid <> '__group_sync__' AND name <> jid
|
||||
ORDER BY last_message_time DESC
|
||||
LIMIT ?`,
|
||||
)
|
||||
.all(limit) as Array<{ jid: string; name: string }>;
|
||||
db.close();
|
||||
@@ -63,167 +59,33 @@ async function listGroups(limit: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.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
|
||||
logger.info('Building TypeScript');
|
||||
let buildOk = false;
|
||||
try {
|
||||
execSync('npm run build', {
|
||||
cwd: projectRoot,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
buildOk = true;
|
||||
logger.info('Build succeeded');
|
||||
} catch {
|
||||
logger.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
|
||||
logger.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:');
|
||||
logger.info({ output: output.trim() }, 'Sync output');
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmpScript); } catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Sync failed');
|
||||
}
|
||||
|
||||
// Count groups in DB using better-sqlite3 (no sqlite3 CLI)
|
||||
let groupsInDb = 0;
|
||||
async function syncGroups(): Promise<void> {
|
||||
const dbPath = path.join(STORE_DIR, 'messages.db');
|
||||
let groupsInDb = 0;
|
||||
|
||||
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__'",
|
||||
`SELECT COUNT(*) as count FROM chats
|
||||
WHERE jid LIKE 'dc:%' AND is_group = 1 AND jid <> '__group_sync__' AND name <> jid`,
|
||||
)
|
||||
.get() as { count: number };
|
||||
groupsInDb = row.count;
|
||||
db.close();
|
||||
} catch {
|
||||
// DB may not exist yet
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Failed to count Discord groups during setup');
|
||||
}
|
||||
}
|
||||
|
||||
const status = syncOk ? 'success' : 'failed';
|
||||
|
||||
logger.info({ groupsInDb }, 'Discord groups are discovered at runtime');
|
||||
emitStatus('SYNC_GROUPS', {
|
||||
BUILD: buildOk ? 'success' : 'failed',
|
||||
SYNC: syncOk ? 'success' : 'failed',
|
||||
BUILD: 'skipped',
|
||||
SYNC: 'skipped',
|
||||
GROUPS_IN_DB: groupsInDb,
|
||||
STATUS: status,
|
||||
REASON: 'discord_runtime_sync',
|
||||
STATUS: 'success',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
|
||||
if (status === 'failed') process.exit(1);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ const STEPS: Record<
|
||||
() => Promise<{ run: (args: string[]) => Promise<void> }>
|
||||
> = {
|
||||
environment: () => import('./environment.js'),
|
||||
runners: () => import('./container.js'),
|
||||
runners: () => import('./runners.js'),
|
||||
groups: () => import('./groups.js'),
|
||||
register: () => import('./register.js'),
|
||||
mounts: () => import('./mounts.js'),
|
||||
service: () => import('./service.js'),
|
||||
verify: () => import('./verify.js'),
|
||||
};
|
||||
|
||||
115
setup/mounts.ts
115
setup/mounts.ts
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* Step: mounts — Write mount allowlist config file.
|
||||
* Replaces 07-configure-mounts.sh
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import { logger } from '../src/logger.js';
|
||||
import { isRoot } from './platform.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
function parseArgs(args: string[]): { empty: boolean; json: string } {
|
||||
let empty = false;
|
||||
let json = '';
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--empty') empty = true;
|
||||
if (args[i] === '--json' && args[i + 1]) {
|
||||
json = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return { empty, json };
|
||||
}
|
||||
|
||||
export async function run(args: string[]): Promise<void> {
|
||||
const { empty, json } = parseArgs(args);
|
||||
const homeDir = os.homedir();
|
||||
const configDir = path.join(homeDir, '.config', 'nanoclaw');
|
||||
const configFile = path.join(configDir, 'mount-allowlist.json');
|
||||
|
||||
if (isRoot()) {
|
||||
logger.warn(
|
||||
'Running as root — mount allowlist will be written to root home directory',
|
||||
);
|
||||
}
|
||||
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
|
||||
let allowedRoots = 0;
|
||||
let nonMainReadOnly = 'true';
|
||||
|
||||
if (empty) {
|
||||
logger.info('Writing empty mount allowlist');
|
||||
const emptyConfig = {
|
||||
allowedRoots: [],
|
||||
blockedPatterns: [],
|
||||
nonMainReadOnly: true,
|
||||
};
|
||||
fs.writeFileSync(configFile, JSON.stringify(emptyConfig, null, 2) + '\n');
|
||||
} else if (json) {
|
||||
// Validate JSON with JSON.parse (not piped through shell)
|
||||
let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
|
||||
try {
|
||||
parsed = JSON.parse(json);
|
||||
} catch {
|
||||
logger.error('Invalid JSON input');
|
||||
emitStatus('CONFIGURE_MOUNTS', {
|
||||
PATH: configFile,
|
||||
ALLOWED_ROOTS: 0,
|
||||
NON_MAIN_READ_ONLY: 'unknown',
|
||||
STATUS: 'failed',
|
||||
ERROR: 'invalid_json',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
process.exit(4);
|
||||
return; // unreachable but satisfies TS
|
||||
}
|
||||
|
||||
fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
|
||||
allowedRoots = Array.isArray(parsed.allowedRoots)
|
||||
? parsed.allowedRoots.length
|
||||
: 0;
|
||||
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
|
||||
} else {
|
||||
// Read from stdin
|
||||
logger.info('Reading mount allowlist from stdin');
|
||||
const input = fs.readFileSync(0, 'utf-8');
|
||||
let parsed: { allowedRoots?: unknown[]; nonMainReadOnly?: boolean };
|
||||
try {
|
||||
parsed = JSON.parse(input);
|
||||
} catch {
|
||||
logger.error('Invalid JSON from stdin');
|
||||
emitStatus('CONFIGURE_MOUNTS', {
|
||||
PATH: configFile,
|
||||
ALLOWED_ROOTS: 0,
|
||||
NON_MAIN_READ_ONLY: 'unknown',
|
||||
STATUS: 'failed',
|
||||
ERROR: 'invalid_json',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
process.exit(4);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(configFile, JSON.stringify(parsed, null, 2) + '\n');
|
||||
allowedRoots = Array.isArray(parsed.allowedRoots)
|
||||
? parsed.allowedRoots.length
|
||||
: 0;
|
||||
nonMainReadOnly = parsed.nonMainReadOnly === false ? 'false' : 'true';
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ configFile, allowedRoots, nonMainReadOnly },
|
||||
'Allowlist configured',
|
||||
);
|
||||
|
||||
emitStatus('CONFIGURE_MOUNTS', {
|
||||
PATH: configFile,
|
||||
ALLOWED_ROOTS: allowedRoots,
|
||||
NON_MAIN_READ_ONLY: nonMainReadOnly,
|
||||
STATUS: 'success',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ function createTestDb(): Database.Database {
|
||||
folder TEXT NOT NULL UNIQUE,
|
||||
trigger_pattern TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
container_config TEXT,
|
||||
agent_config TEXT,
|
||||
requires_trigger INTEGER DEFAULT 1,
|
||||
is_main INTEGER DEFAULT 0
|
||||
)`);
|
||||
@@ -34,7 +34,7 @@ describe('parameterized SQL registration', () => {
|
||||
it('registers a group with parameterized query', () => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
).run(
|
||||
'123@g.us',
|
||||
@@ -67,7 +67,7 @@ describe('parameterized SQL registration', () => {
|
||||
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
).run(
|
||||
'456@g.us',
|
||||
@@ -92,7 +92,7 @@ describe('parameterized SQL registration', () => {
|
||||
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
).run(maliciousJid, 'Evil', 'evil', '@Andy', '2024-01-01T00:00:00.000Z', 1);
|
||||
|
||||
@@ -113,10 +113,10 @@ describe('parameterized SQL registration', () => {
|
||||
it('handles requiresTrigger=false', () => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
).run(
|
||||
'789@s.whatsapp.net',
|
||||
'dc:789',
|
||||
'Personal',
|
||||
'main',
|
||||
'@Andy',
|
||||
@@ -126,7 +126,7 @@ describe('parameterized SQL registration', () => {
|
||||
|
||||
const row = db
|
||||
.prepare('SELECT requires_trigger FROM registered_groups WHERE jid = ?')
|
||||
.get('789@s.whatsapp.net') as { requires_trigger: number };
|
||||
.get('dc:789') as { requires_trigger: number };
|
||||
|
||||
expect(row.requires_trigger).toBe(0);
|
||||
});
|
||||
@@ -134,12 +134,12 @@ describe('parameterized SQL registration', () => {
|
||||
it('stores is_main flag', () => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
||||
).run(
|
||||
'789@s.whatsapp.net',
|
||||
'dc:789',
|
||||
'Personal',
|
||||
'whatsapp_main',
|
||||
'discord_main',
|
||||
'@Andy',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
0,
|
||||
@@ -148,7 +148,7 @@ describe('parameterized SQL registration', () => {
|
||||
|
||||
const row = db
|
||||
.prepare('SELECT is_main FROM registered_groups WHERE jid = ?')
|
||||
.get('789@s.whatsapp.net') as { is_main: number };
|
||||
.get('dc:789') as { is_main: number };
|
||||
|
||||
expect(row.is_main).toBe(1);
|
||||
});
|
||||
@@ -156,12 +156,12 @@ describe('parameterized SQL registration', () => {
|
||||
it('defaults is_main to 0', () => {
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
).run(
|
||||
'123@g.us',
|
||||
'Some Group',
|
||||
'whatsapp_some-group',
|
||||
'discord_some-group',
|
||||
'@Andy',
|
||||
'2024-01-01T00:00:00.000Z',
|
||||
1,
|
||||
@@ -177,7 +177,7 @@ describe('parameterized SQL registration', () => {
|
||||
it('upserts on conflict', () => {
|
||||
const stmt = db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger)
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?)`,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Step: register — Write channel registration config, create group folders.
|
||||
*
|
||||
* Accepts --channel to specify the messaging platform (whatsapp, telegram, slack, discord).
|
||||
* NanoClaw is Discord-only, so registrations must target Discord channel IDs.
|
||||
* Uses parameterized SQL queries to prevent injection.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
@@ -9,7 +9,7 @@ import path from 'path';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
import { STORE_DIR } from '../src/config.js';
|
||||
import { SERVICE_AGENT_TYPE, STORE_DIR } from '../src/config.js';
|
||||
import { isValidGroupFolder } from '../src/group-folder.js';
|
||||
import { logger } from '../src/logger.js';
|
||||
import { emitStatus } from './status.js';
|
||||
@@ -31,7 +31,7 @@ function parseArgs(args: string[]): RegisterArgs {
|
||||
name: '',
|
||||
trigger: '',
|
||||
folder: '',
|
||||
channel: 'whatsapp', // backward-compat: pre-refactor installs omit --channel
|
||||
channel: 'discord',
|
||||
requiresTrigger: true,
|
||||
isMain: false,
|
||||
assistantName: 'Andy',
|
||||
@@ -91,10 +91,18 @@ export async function run(args: string[]): Promise<void> {
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
if (parsed.channel !== 'discord') {
|
||||
emitStatus('REGISTER_CHANNEL', {
|
||||
STATUS: 'failed',
|
||||
ERROR: 'unsupported_channel',
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
logger.info(parsed, 'Registering channel');
|
||||
|
||||
// Ensure data and store directories exist (store/ may not exist on
|
||||
// fresh installs that skip WhatsApp auth, which normally creates it)
|
||||
// Ensure data and store directories exist for fresh installs.
|
||||
fs.mkdirSync(path.join(projectRoot, 'data'), { recursive: true });
|
||||
fs.mkdirSync(STORE_DIR, { recursive: true });
|
||||
|
||||
@@ -106,22 +114,40 @@ export async function run(args: string[]): Promise<void> {
|
||||
const db = new Database(dbPath);
|
||||
// Ensure schema exists
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS registered_groups (
|
||||
jid TEXT PRIMARY KEY,
|
||||
jid TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
folder TEXT NOT NULL UNIQUE,
|
||||
folder TEXT NOT NULL,
|
||||
trigger_pattern TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
container_config TEXT,
|
||||
agent_config TEXT,
|
||||
requires_trigger INTEGER DEFAULT 1,
|
||||
is_main INTEGER DEFAULT 0
|
||||
is_main INTEGER DEFAULT 0,
|
||||
agent_type TEXT NOT NULL DEFAULT 'claude-code',
|
||||
work_dir TEXT,
|
||||
PRIMARY KEY (jid, agent_type),
|
||||
UNIQUE (folder, agent_type)
|
||||
)`);
|
||||
|
||||
try {
|
||||
db.exec(
|
||||
`ALTER TABLE registered_groups ADD COLUMN agent_type TEXT DEFAULT 'claude-code'`,
|
||||
);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(`ALTER TABLE registered_groups ADD COLUMN work_dir TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
const isMainInt = parsed.isMain ? 1 : 0;
|
||||
|
||||
db.prepare(
|
||||
`INSERT OR REPLACE INTO registered_groups
|
||||
(jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger, is_main)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)`,
|
||||
(jid, name, folder, trigger_pattern, added_at, agent_config, requires_trigger, is_main, agent_type, work_dir)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, NULL)`,
|
||||
).run(
|
||||
parsed.jid,
|
||||
parsed.name,
|
||||
@@ -130,6 +156,7 @@ export async function run(args: string[]): Promise<void> {
|
||||
timestamp,
|
||||
requiresTriggerInt,
|
||||
isMainInt,
|
||||
SERVICE_AGENT_TYPE,
|
||||
);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Step: runners — Build agent runners (no container needed).
|
||||
* Agents run as direct host processes.
|
||||
* Step: runners — Build the Claude and Codex runner packages.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
@@ -29,14 +28,14 @@ export async function run(_args: string[]): Promise<void> {
|
||||
// Verify runner entry points exist
|
||||
const agentRunner = path.join(
|
||||
projectRoot,
|
||||
'container',
|
||||
'runners',
|
||||
'agent-runner',
|
||||
'dist',
|
||||
'index.js',
|
||||
);
|
||||
const codexRunner = path.join(
|
||||
projectRoot,
|
||||
'container',
|
||||
'runners',
|
||||
'codex-runner',
|
||||
'dist',
|
||||
'index.js',
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
@@ -14,18 +13,12 @@ import Database from 'better-sqlite3';
|
||||
import { STORE_DIR } from '../src/config.js';
|
||||
import { readEnvFile } from '../src/env.js';
|
||||
import { logger } from '../src/logger.js';
|
||||
import {
|
||||
getPlatform,
|
||||
getServiceManager,
|
||||
hasSystemd,
|
||||
isRoot,
|
||||
} from './platform.js';
|
||||
import { getPlatform, getServiceManager, isRoot } from './platform.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
export async function run(_args: string[]): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const platform = getPlatform();
|
||||
const homeDir = os.homedir();
|
||||
|
||||
logger.info('Starting verification');
|
||||
|
||||
@@ -93,31 +86,10 @@ export async function run(_args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
// 3. Check channel auth (detect configured channels by credentials)
|
||||
const envVars = readEnvFile([
|
||||
'TELEGRAM_BOT_TOKEN',
|
||||
'SLACK_BOT_TOKEN',
|
||||
'SLACK_APP_TOKEN',
|
||||
'DISCORD_BOT_TOKEN',
|
||||
]);
|
||||
const envVars = readEnvFile(['DISCORD_BOT_TOKEN']);
|
||||
|
||||
const channelAuth: Record<string, string> = {};
|
||||
|
||||
// WhatsApp: check for auth credentials on disk
|
||||
const authDir = path.join(projectRoot, 'store', 'auth');
|
||||
if (fs.existsSync(authDir) && fs.readdirSync(authDir).length > 0) {
|
||||
channelAuth.whatsapp = 'authenticated';
|
||||
}
|
||||
|
||||
// Token-based channels: check .env
|
||||
if (process.env.TELEGRAM_BOT_TOKEN || envVars.TELEGRAM_BOT_TOKEN) {
|
||||
channelAuth.telegram = 'configured';
|
||||
}
|
||||
if (
|
||||
(process.env.SLACK_BOT_TOKEN || envVars.SLACK_BOT_TOKEN) &&
|
||||
(process.env.SLACK_APP_TOKEN || envVars.SLACK_APP_TOKEN)
|
||||
) {
|
||||
channelAuth.slack = 'configured';
|
||||
}
|
||||
if (process.env.DISCORD_BOT_TOKEN || envVars.DISCORD_BOT_TOKEN) {
|
||||
channelAuth.discord = 'configured';
|
||||
}
|
||||
@@ -141,16 +113,6 @@ export async function run(_args: string[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check mount allowlist
|
||||
let mountAllowlist = 'missing';
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(homeDir, '.config', 'nanoclaw', 'mount-allowlist.json'),
|
||||
)
|
||||
) {
|
||||
mountAllowlist = 'configured';
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
const status =
|
||||
service === 'running' &&
|
||||
@@ -168,7 +130,6 @@ export async function run(_args: string[]): Promise<void> {
|
||||
CONFIGURED_CHANNELS: configuredChannels.join(','),
|
||||
CHANNEL_AUTH: JSON.stringify(channelAuth),
|
||||
REGISTERED_GROUPS: registeredGroups,
|
||||
MOUNT_ALLOWLIST: mountAllowlist,
|
||||
STATUS: status,
|
||||
LOG: 'logs/setup.log',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user