mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-12 18:11:51 +08:00
Merge remote-tracking branch 'origin/main' into setup-feedback-fixes
# Conflicts: # setup/auto.ts # setup/channels/whatsapp.ts
This commit is contained in:
@@ -110,13 +110,15 @@ mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the credentials…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
launchctl kickstart -k "gui/$(id -u)/com.nanoclaw" >&2 2>/dev/null || true
|
||||
launchctl kickstart -k "gui/$(id -u)/$(launchd_label)" >&2 2>/dev/null || true
|
||||
;;
|
||||
Linux)
|
||||
systemctl --user restart nanoclaw >&2 2>/dev/null \
|
||||
|| sudo systemctl restart nanoclaw >&2 2>/dev/null \
|
||||
systemctl --user restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| sudo systemctl restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| true
|
||||
;;
|
||||
esac
|
||||
|
||||
+5
-3
@@ -119,13 +119,15 @@ mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the credentials…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
launchctl kickstart -k "gui/$(id -u)/com.nanoclaw" >&2 2>/dev/null || true
|
||||
launchctl kickstart -k "gui/$(id -u)/$(launchd_label)" >&2 2>/dev/null || true
|
||||
;;
|
||||
Linux)
|
||||
systemctl --user restart nanoclaw >&2 2>/dev/null \
|
||||
|| sudo systemctl restart nanoclaw >&2 2>/dev/null \
|
||||
systemctl --user restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| sudo systemctl restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| true
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -144,13 +144,15 @@ cp .env data/env/env
|
||||
# non-interactive install.
|
||||
|
||||
log "Restarting service so the new adapter picks up the token…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
launchctl kickstart -k "gui/$(id -u)/com.nanoclaw" >&2 2>/dev/null || true
|
||||
launchctl kickstart -k "gui/$(id -u)/$(launchd_label)" >&2 2>/dev/null || true
|
||||
;;
|
||||
Linux)
|
||||
systemctl --user restart nanoclaw >&2 2>/dev/null \
|
||||
|| sudo systemctl restart nanoclaw >&2 2>/dev/null \
|
||||
systemctl --user restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| sudo systemctl restart "$(systemd_unit)" >&2 2>/dev/null \
|
||||
|| true
|
||||
;;
|
||||
esac
|
||||
|
||||
+6
-4
@@ -34,6 +34,7 @@ import { pingCliAgent, type PingResult } from './lib/agent-ping.js';
|
||||
import { brightSelect } from './lib/bright-select.js';
|
||||
import { offerClaudeAssist } from './lib/claude-assist.js';
|
||||
import { runWindowedStep } from './lib/windowed-runner.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js';
|
||||
import {
|
||||
claudeCliAvailable,
|
||||
resolveTimezoneViaClaude,
|
||||
@@ -308,13 +309,14 @@ async function main(): Promise<void> {
|
||||
}
|
||||
const service = res.terminal?.fields.SERVICE;
|
||||
if (service === 'running_other_checkout') {
|
||||
const label = getLaunchdLabel();
|
||||
notes.push(
|
||||
wrapForGutter(
|
||||
[
|
||||
'• Your NanoClaw service is running from a different folder on this machine.',
|
||||
' Point it at this checkout with:',
|
||||
' launchctl bootout gui/$(id -u)/com.nanoclaw',
|
||||
' launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.nanoclaw.plist',
|
||||
` launchctl bootout gui/$(id -u)/${label}`,
|
||||
` launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/${label}.plist`,
|
||||
].join('\n'),
|
||||
6,
|
||||
),
|
||||
@@ -460,8 +462,8 @@ function renderPingFailureNote(result: PingResult): void {
|
||||
6,
|
||||
),
|
||||
'',
|
||||
k.dim(' macOS: launchctl kickstart -k gui/$(id -u)/com.nanoclaw'),
|
||||
k.dim(' Linux: systemctl --user restart nanoclaw'),
|
||||
k.dim(` macOS: launchctl kickstart -k gui/$(id -u)/${getLaunchdLabel()}`),
|
||||
k.dim(` Linux: systemctl --user restart ${getSystemdUnit()}`),
|
||||
].join('\n')
|
||||
: wrapForGutter(
|
||||
'No reply from your assistant within 30 seconds. Check `logs/nanoclaw.log` for clues, then try `pnpm run chat hi`.',
|
||||
|
||||
@@ -34,6 +34,7 @@ import k from 'kleur';
|
||||
|
||||
import * as setupLog from '../logs.js';
|
||||
import { brightSelect } from '../lib/bright-select.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../../src/install-slug.js';
|
||||
import {
|
||||
type Block,
|
||||
type StepResult,
|
||||
@@ -359,17 +360,18 @@ async function restartService(): Promise<void> {
|
||||
if (platform === 'darwin') {
|
||||
spawnSync(
|
||||
'launchctl',
|
||||
['kickstart', '-k', `gui/${process.getuid?.() ?? 501}/com.nanoclaw`],
|
||||
['kickstart', '-k', `gui/${process.getuid?.() ?? 501}/${getLaunchdLabel()}`],
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
} else if (platform === 'linux') {
|
||||
const unit = getSystemdUnit();
|
||||
const user = spawnSync(
|
||||
'systemctl',
|
||||
['--user', 'restart', 'nanoclaw'],
|
||||
['--user', 'restart', unit],
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
if (user.status !== 0) {
|
||||
spawnSync('sudo', ['systemctl', 'restart', 'nanoclaw'], {
|
||||
spawnSync('sudo', ['systemctl', 'restart', unit], {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import path from 'path';
|
||||
import { setTimeout as sleep } from 'timers/promises';
|
||||
|
||||
import { log } from '../src/log.js';
|
||||
import { getDefaultContainerImage } from '../src/install-slug.js';
|
||||
import { commandExists, getPlatform } from './platform.js';
|
||||
import { emitStatus } from './status.js';
|
||||
|
||||
@@ -81,7 +82,7 @@ function parseArgs(args: string[]): { runtime: string } {
|
||||
export async function run(args: string[]): Promise<void> {
|
||||
const projectRoot = process.cwd();
|
||||
const { runtime } = parseArgs(args);
|
||||
const image = 'nanoclaw-agent:latest';
|
||||
const image = getDefaultContainerImage(projectRoot);
|
||||
const logFile = path.join(projectRoot, 'logs', 'setup.log');
|
||||
|
||||
if (runtime !== 'docker') {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# install-slug.sh — shell mirror of setup/lib/install-slug.ts.
|
||||
#
|
||||
# Source this file after $PROJECT_ROOT is set:
|
||||
#
|
||||
# source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
# label=$(launchd_label) # com.nanoclaw-v2-<slug>
|
||||
# unit=$(systemd_unit) # nanoclaw-v2-<slug>
|
||||
# image=$(container_image_base) # nanoclaw-agent-v2-<slug>
|
||||
#
|
||||
# Slug is sha1(PROJECT_ROOT)[:8] — must match the TS helper exactly so both
|
||||
# halves of setup name things consistently.
|
||||
|
||||
_nanoclaw_install_slug() {
|
||||
local root="${NANOCLAW_PROJECT_ROOT:-${PROJECT_ROOT:-$PWD}}"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
printf '%s' "$root" | shasum | cut -c 1-8
|
||||
elif command -v sha1sum >/dev/null 2>&1; then
|
||||
printf '%s' "$root" | sha1sum | cut -c 1-8
|
||||
else
|
||||
# Fallback: hash the path with something deterministic-ish. Not ideal —
|
||||
# but shasum is present on every modern macOS/Linux, so this is just
|
||||
# belt-and-braces against a truly minimal system.
|
||||
printf '%s' "$root" | od -An -tx1 | tr -d ' \n' | cut -c 1-8
|
||||
fi
|
||||
}
|
||||
|
||||
launchd_label() {
|
||||
printf 'com.nanoclaw-v2-%s' "$(_nanoclaw_install_slug)"
|
||||
}
|
||||
|
||||
systemd_unit() {
|
||||
printf 'nanoclaw-v2-%s' "$(_nanoclaw_install_slug)"
|
||||
}
|
||||
|
||||
container_image_base() {
|
||||
printf 'nanoclaw-agent-v2-%s' "$(_nanoclaw_install_slug)"
|
||||
}
|
||||
+9
-3
@@ -19,7 +19,13 @@ START_S=$(date +%s)
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOCAL_BIN="$HOME/.local/bin"
|
||||
AGENT_IMAGE="nanoclaw-agent:latest"
|
||||
|
||||
# Per-checkout install names (match setup/lib/install-slug.ts).
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
LAUNCHD_LABEL=$(launchd_label)
|
||||
SYSTEMD_UNIT=$(systemd_unit)
|
||||
AGENT_IMAGE="$(container_image_base):latest"
|
||||
|
||||
export PATH="$LOCAL_BIN:$PATH"
|
||||
|
||||
@@ -144,7 +150,7 @@ probe_service_status() {
|
||||
macos)
|
||||
command_exists launchctl || { echo "not_configured"; return; }
|
||||
local line
|
||||
line=$(with_timeout launchctl list 2>/dev/null | grep "com.nanoclaw") || {
|
||||
line=$(with_timeout launchctl list 2>/dev/null | grep "$LAUNCHD_LABEL") || {
|
||||
echo "not_configured"; return; }
|
||||
local pid
|
||||
pid=$(echo "$line" | awk '{print $1}')
|
||||
@@ -156,7 +162,7 @@ probe_service_status() {
|
||||
;;
|
||||
linux|wsl)
|
||||
command_exists systemctl || { echo "not_configured"; return; }
|
||||
if with_timeout systemctl --user is-active nanoclaw >/dev/null 2>&1; then
|
||||
if with_timeout systemctl --user is-active "$SYSTEMD_UNIT" >/dev/null 2>&1; then
|
||||
echo "running"
|
||||
elif with_timeout systemctl --user cat nanoclaw >/dev/null 2>&1; then
|
||||
echo "stopped"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import path from 'path';
|
||||
|
||||
import { getLaunchdLabel } from '../src/install-slug.js';
|
||||
|
||||
/**
|
||||
* Tests for service configuration generation.
|
||||
*
|
||||
@@ -14,12 +16,13 @@ function generatePlist(
|
||||
projectRoot: string,
|
||||
homeDir: string,
|
||||
): string {
|
||||
const label = getLaunchdLabel(projectRoot);
|
||||
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>
|
||||
<string>${label}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>${nodePath}</string>
|
||||
@@ -73,13 +76,11 @@ 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('contains the slug-scoped label', () => {
|
||||
const projectRoot = '/home/user/nanoclaw';
|
||||
const plist = generatePlist('/usr/local/bin/node', projectRoot, '/home/user');
|
||||
expect(plist).toContain(`<string>${getLaunchdLabel(projectRoot)}</string>`);
|
||||
expect(plist).toMatch(/<string>com\.nanoclaw-v2-[0-9a-f]{8}<\/string>/);
|
||||
});
|
||||
|
||||
it('uses the correct node path', () => {
|
||||
|
||||
+16
-8
@@ -10,6 +10,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { log } from '../src/log.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js';
|
||||
import {
|
||||
commandExists,
|
||||
getPlatform,
|
||||
@@ -74,11 +75,14 @@ function setupLaunchd(
|
||||
nodePath: string,
|
||||
homeDir: string,
|
||||
): void {
|
||||
// Per-checkout service label so multiple NanoClaw installs can coexist
|
||||
// without clobbering each other's plist.
|
||||
const label = getLaunchdLabel(projectRoot);
|
||||
const plistPath = path.join(
|
||||
homeDir,
|
||||
'Library',
|
||||
'LaunchAgents',
|
||||
'com.nanoclaw.plist',
|
||||
`${label}.plist`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
||||
|
||||
@@ -87,7 +91,7 @@ function setupLaunchd(
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.nanoclaw</string>
|
||||
<string>${label}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>${nodePath}</string>
|
||||
@@ -146,13 +150,14 @@ function setupLaunchd(
|
||||
let serviceLoaded = false;
|
||||
try {
|
||||
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
||||
serviceLoaded = output.includes('com.nanoclaw');
|
||||
serviceLoaded = output.includes(label);
|
||||
} catch {
|
||||
// launchctl list failed
|
||||
}
|
||||
|
||||
emitStatus('SETUP_SERVICE', {
|
||||
SERVICE_TYPE: 'launchd',
|
||||
SERVICE_LABEL: label,
|
||||
NODE_PATH: nodePath,
|
||||
PROJECT_PATH: projectRoot,
|
||||
PLIST_PATH: plistPath,
|
||||
@@ -225,13 +230,15 @@ function setupSystemd(
|
||||
homeDir: string,
|
||||
): void {
|
||||
const runningAsRoot = isRoot();
|
||||
const unitName = getSystemdUnit(projectRoot);
|
||||
const unitFileName = `${unitName}.service`;
|
||||
|
||||
// Root uses system-level service, non-root uses user-level
|
||||
let unitPath: string;
|
||||
let systemctlPrefix: string;
|
||||
|
||||
if (runningAsRoot) {
|
||||
unitPath = '/etc/systemd/system/nanoclaw.service';
|
||||
unitPath = `/etc/systemd/system/${unitFileName}`;
|
||||
systemctlPrefix = 'systemctl';
|
||||
log.info('Running as root — installing system-level systemd unit');
|
||||
} else {
|
||||
@@ -247,7 +254,7 @@ function setupSystemd(
|
||||
}
|
||||
const unitDir = path.join(homeDir, '.config', 'systemd', 'user');
|
||||
fs.mkdirSync(unitDir, { recursive: true });
|
||||
unitPath = path.join(unitDir, 'nanoclaw.service');
|
||||
unitPath = path.join(unitDir, unitFileName);
|
||||
systemctlPrefix = 'systemctl --user';
|
||||
}
|
||||
|
||||
@@ -328,7 +335,7 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`${systemctlPrefix} enable nanoclaw`, { stdio: 'ignore' });
|
||||
execSync(`${systemctlPrefix} enable ${unitName}`, { stdio: 'ignore' });
|
||||
} catch (err) {
|
||||
log.error('systemctl enable failed', { err });
|
||||
}
|
||||
@@ -339,7 +346,7 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
|
||||
// `restart` on a stopped unit is equivalent to `start`, so this is safe
|
||||
// as a first-install path too.
|
||||
try {
|
||||
execSync(`${systemctlPrefix} restart nanoclaw`, { stdio: 'ignore' });
|
||||
execSync(`${systemctlPrefix} restart ${unitName}`, { stdio: 'ignore' });
|
||||
} catch (err) {
|
||||
log.error('systemctl restart failed', { err });
|
||||
}
|
||||
@@ -347,7 +354,7 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
|
||||
// Verify
|
||||
let serviceLoaded = false;
|
||||
try {
|
||||
execSync(`${systemctlPrefix} is-active nanoclaw`, { stdio: 'ignore' });
|
||||
execSync(`${systemctlPrefix} is-active ${unitName}`, { stdio: 'ignore' });
|
||||
serviceLoaded = true;
|
||||
} catch {
|
||||
// Not active
|
||||
@@ -355,6 +362,7 @@ WantedBy=${runningAsRoot ? 'multi-user.target' : 'default.target'}`;
|
||||
|
||||
emitStatus('SETUP_SERVICE', {
|
||||
SERVICE_TYPE: runningAsRoot ? 'systemd-system' : 'systemd-user',
|
||||
SERVICE_UNIT: unitName,
|
||||
NODE_PATH: nodePath,
|
||||
PROJECT_PATH: projectRoot,
|
||||
UNIT_PATH: unitPath,
|
||||
|
||||
+8
-4
@@ -15,6 +15,7 @@ import { DATA_DIR } from '../src/config.js';
|
||||
import { readEnvFile } from '../src/env.js';
|
||||
import { log } from '../src/log.js';
|
||||
import { pingCliAgent } from './lib/agent-ping.js';
|
||||
import { getLaunchdLabel, getSystemdUnit } from '../src/install-slug.js';
|
||||
import {
|
||||
getPlatform,
|
||||
getServiceManager,
|
||||
@@ -45,10 +46,13 @@ export async function run(_args: string[]): Promise<void> {
|
||||
let runningFromPath: string | null = null;
|
||||
const mgr = getServiceManager();
|
||||
|
||||
const launchdLabel = getLaunchdLabel(projectRoot);
|
||||
const systemdUnit = getSystemdUnit(projectRoot);
|
||||
|
||||
if (mgr === 'launchd') {
|
||||
try {
|
||||
const output = execSync('launchctl list', { encoding: 'utf-8' });
|
||||
const line = output.split('\n').find((l) => l.includes('com.nanoclaw'));
|
||||
const line = output.split('\n').find((l) => l.includes(launchdLabel));
|
||||
if (line) {
|
||||
const pidField = line.trim().split(/\s+/)[0];
|
||||
if (pidField !== '-' && pidField) {
|
||||
@@ -67,11 +71,11 @@ export async function run(_args: string[]): Promise<void> {
|
||||
} else if (mgr === 'systemd') {
|
||||
const prefix = isRoot() ? 'systemctl' : 'systemctl --user';
|
||||
try {
|
||||
execSync(`${prefix} is-active nanoclaw`, { stdio: 'ignore' });
|
||||
execSync(`${prefix} is-active ${systemdUnit}`, { stdio: 'ignore' });
|
||||
service = 'running';
|
||||
try {
|
||||
const pidStr = execSync(
|
||||
`${prefix} show nanoclaw -p MainPID --value`,
|
||||
`${prefix} show ${systemdUnit} -p MainPID --value`,
|
||||
{ encoding: 'utf-8' },
|
||||
).trim();
|
||||
const pid = Number(pidStr);
|
||||
@@ -86,7 +90,7 @@ export async function run(_args: string[]): Promise<void> {
|
||||
const output = execSync(`${prefix} list-unit-files`, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (output.includes('nanoclaw')) {
|
||||
if (output.includes(systemdUnit)) {
|
||||
service = 'stopped';
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user