feat(setup): per-checkout service name and docker image tag
Two NanoClaw installs on the same host used to fight over the shared `com.nanoclaw` launchd label / `nanoclaw.service` systemd unit and the `nanoclaw-agent:latest` docker tag — the second install silently rewrote the service pointer and rebuilt the image out from under the first. Introduces a deterministic per-checkout slug (sha1(projectRoot)[:8]) and namespaces everything off it: - Service: `com.nanoclaw-v2-<slug>` / `nanoclaw-v2-<slug>.service` - Image: `nanoclaw-agent-v2-<slug>:latest` (base), `nanoclaw-agent-v2-<slug>:<agentGroupId>` (per-group) New shared helpers: src/install-slug.ts (host) + setup/lib/install-slug.sh (bash). Both compute the same slug so verify/probe/add-*.sh/build.sh/container-runner all agree. Any v1 `com.nanoclaw` service left on the host stays untouched and can coexist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user