* refactor: migrate setup from bash scripts to cross-platform Node.js modules Replace 9 bash scripts + qr-auth.html with a two-phase setup system: a bash bootstrap (setup.sh) for Node.js/npm verification, and TypeScript modules (src/setup/) for everything else. Resolves cross-platform issues: sed -i replaced with fs operations, sqlite3 CLI replaced with better-sqlite3, browser opening made cross-platform, service management supports launchd/ systemd/WSL nohup fallback, SQL injection prevented with parameterized queries. Add Linux systemctl equivalents alongside macOS launchctl commands in 8 skill files and CLAUDE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: setup migration issues — pairing code, systemd fallback, nohup escaping - Emit WhatsApp pairing code immediately when received, before polling for auth completion. Previously the code was only shown in the final status block after auth succeeded — a catch-22 since the user needs the code to authenticate. (whatsapp-auth.ts) - Add systemd user session pre-check before attempting to write the user-level service unit. Falls back to nohup wrapper when user-level systemd is unavailable (e.g. su session without login/D-Bus). (service.ts) - Rewrite nohup wrapper template using array join instead of template literal to fix shell variable escaping (\\$ → $). (service.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: detect stale docker group and kill orphaned processes on Linux systemd * fix: remove redundant shell option from execSync to fix TS2769 execSync already runs in a shell by default; the explicit `shell: true` caused a type error with @types/node which expects string, not boolean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: hide QR browser auth option on headless Linux Emit IS_HEADLESS from environment step and condition SKILL.md to only show pairing code + QR terminal when no display server is available (headless Linux without WSL). WSL is excluded from the headless gate because browser opening works via Windows interop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import path from 'path';
|
|
|
|
/**
|
|
* Tests for service configuration generation.
|
|
*
|
|
* These tests verify the generated content of plist/systemd/nohup configs
|
|
* without actually loading services.
|
|
*/
|
|
|
|
// Helper: generate a plist string the same way service.ts does
|
|
function generatePlist(nodePath: string, projectRoot: string, homeDir: string): string {
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.nanoclaw</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${nodePath}</string>
|
|
<string>${projectRoot}/dist/index.js</string>
|
|
</array>
|
|
<key>WorkingDirectory</key>
|
|
<string>${projectRoot}</string>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>PATH</key>
|
|
<string>/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin</string>
|
|
<key>HOME</key>
|
|
<string>${homeDir}</string>
|
|
</dict>
|
|
<key>StandardOutPath</key>
|
|
<string>${projectRoot}/logs/nanoclaw.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${projectRoot}/logs/nanoclaw.error.log</string>
|
|
</dict>
|
|
</plist>`;
|
|
}
|
|
|
|
function generateSystemdUnit(
|
|
nodePath: string,
|
|
projectRoot: string,
|
|
homeDir: string,
|
|
isSystem: boolean,
|
|
): string {
|
|
return `[Unit]
|
|
Description=NanoClaw Personal Assistant
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${nodePath} ${projectRoot}/dist/index.js
|
|
WorkingDirectory=${projectRoot}
|
|
Restart=always
|
|
RestartSec=5
|
|
Environment=HOME=${homeDir}
|
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
|
|
StandardOutput=append:${projectRoot}/logs/nanoclaw.log
|
|
StandardError=append:${projectRoot}/logs/nanoclaw.error.log
|
|
|
|
[Install]
|
|
WantedBy=${isSystem ? 'multi-user.target' : 'default.target'}`;
|
|
}
|
|
|
|
describe('plist generation', () => {
|
|
it('contains the correct label', () => {
|
|
const plist = generatePlist('/usr/local/bin/node', '/home/user/nanoclaw', '/home/user');
|
|
expect(plist).toContain('<string>com.nanoclaw</string>');
|
|
});
|
|
|
|
it('uses the correct node path', () => {
|
|
const plist = generatePlist('/opt/node/bin/node', '/home/user/nanoclaw', '/home/user');
|
|
expect(plist).toContain('<string>/opt/node/bin/node</string>');
|
|
});
|
|
|
|
it('points to dist/index.js', () => {
|
|
const plist = generatePlist('/usr/local/bin/node', '/home/user/nanoclaw', '/home/user');
|
|
expect(plist).toContain('/home/user/nanoclaw/dist/index.js');
|
|
});
|
|
|
|
it('sets log paths', () => {
|
|
const plist = generatePlist('/usr/local/bin/node', '/home/user/nanoclaw', '/home/user');
|
|
expect(plist).toContain('nanoclaw.log');
|
|
expect(plist).toContain('nanoclaw.error.log');
|
|
});
|
|
});
|
|
|
|
describe('systemd unit generation', () => {
|
|
it('user unit uses default.target', () => {
|
|
const unit = generateSystemdUnit('/usr/bin/node', '/home/user/nanoclaw', '/home/user', false);
|
|
expect(unit).toContain('WantedBy=default.target');
|
|
});
|
|
|
|
it('system unit uses multi-user.target', () => {
|
|
const unit = generateSystemdUnit('/usr/bin/node', '/home/user/nanoclaw', '/home/user', true);
|
|
expect(unit).toContain('WantedBy=multi-user.target');
|
|
});
|
|
|
|
it('contains restart policy', () => {
|
|
const unit = generateSystemdUnit('/usr/bin/node', '/home/user/nanoclaw', '/home/user', false);
|
|
expect(unit).toContain('Restart=always');
|
|
expect(unit).toContain('RestartSec=5');
|
|
});
|
|
|
|
it('sets correct ExecStart', () => {
|
|
const unit = generateSystemdUnit('/usr/bin/node', '/srv/nanoclaw', '/home/user', false);
|
|
expect(unit).toContain('ExecStart=/usr/bin/node /srv/nanoclaw/dist/index.js');
|
|
});
|
|
});
|
|
|
|
describe('WSL nohup fallback', () => {
|
|
it('generates a valid wrapper script', () => {
|
|
const projectRoot = '/home/user/nanoclaw';
|
|
const nodePath = '/usr/bin/node';
|
|
const pidFile = path.join(projectRoot, 'nanoclaw.pid');
|
|
|
|
// Simulate what service.ts generates
|
|
const wrapper = `#!/bin/bash
|
|
set -euo pipefail
|
|
cd ${JSON.stringify(projectRoot)}
|
|
nohup ${JSON.stringify(nodePath)} ${JSON.stringify(projectRoot)}/dist/index.js >> ${JSON.stringify(projectRoot)}/logs/nanoclaw.log 2>> ${JSON.stringify(projectRoot)}/logs/nanoclaw.error.log &
|
|
echo $! > ${JSON.stringify(pidFile)}`;
|
|
|
|
expect(wrapper).toContain('#!/bin/bash');
|
|
expect(wrapper).toContain('nohup');
|
|
expect(wrapper).toContain(nodePath);
|
|
expect(wrapper).toContain('nanoclaw.pid');
|
|
});
|
|
});
|