Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<a href="https://render.com/deploy?repo=https://github.com/chrysb/openclaw-render-template"><img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" /></a>
</p>

> **Platform:** AlphaClaw currently targets Docker/Linux deployments. macOS local development is not yet supported.
> **Platform:** AlphaClaw supports Docker/Linux deployments and local macOS development. Linux installs the hourly git sync via `/etc/cron.d`; macOS runs the sync schedule from AlphaClaw's managed in-process scheduler.

## Features

Expand Down
174 changes: 125 additions & 49 deletions bin/alphaclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ const {
normalizeGitSyncFilePath,
validateGitSyncFilePath,
} = require("../lib/cli/git-sync");
const { buildSecretReplacements } = require("../lib/server/helpers");
const {
migrateManagedInternalFiles,
} = require("../lib/server/internal-files-migration");
const {
applySystemCronConfig,
normalizeCronPlatform,
readSystemCronConfig,
startManagedScheduler,
} = require("../lib/server/system-cron");

const kUsageTrackerPluginPath = path.resolve(
__dirname,
Expand All @@ -21,6 +26,76 @@ const kUsageTrackerPluginPath = path.resolve(
"plugin",
"usage-tracker",
);
const kSystemBinDir = "/usr/local/bin";

const prependPathEntry = (entryPath) => {
const currentPath = String(process.env.PATH || "");
const entries = currentPath
.split(path.delimiter)
.map((value) => value.trim())
.filter(Boolean);
if (entries.includes(entryPath)) return;
process.env.PATH = [entryPath, ...entries].join(path.delimiter);
};

const isWritableDirectory = (dirPath) => {
try {
fs.accessSync(dirPath, fs.constants.W_OK);
return true;
} catch {
return false;
}
};

const findFileRecursive = (rootPath, fileName) => {
const pending = [rootPath];
while (pending.length) {
const currentPath = pending.pop();
let entries = [];
try {
entries = fs.readdirSync(currentPath, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const entryPath = path.join(currentPath, entry.name);
if (entry.isFile() && entry.name === fileName) return entryPath;
if (entry.isDirectory()) pending.push(entryPath);
}
}
return "";
};

const installTarballBinary = ({
url,
binaryName,
installDir,
logLabel,
}) => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "alphaclaw-bin-"));
const tarballPath = path.join(tempDir, `${binaryName}.tar.gz`);
try {
execSync(`curl -fsSL "${url}" -o ${quoteArg(tarballPath)}`, {
stdio: "inherit",
});
execSync(`tar -xzf ${quoteArg(tarballPath)} -C ${quoteArg(tempDir)}`, {
stdio: "inherit",
});
const extractedBinaryPath = findFileRecursive(tempDir, binaryName);
if (!extractedBinaryPath) {
throw new Error(`Could not find ${binaryName} in downloaded archive`);
}
const targetPath = path.join(installDir, binaryName);
fs.copyFileSync(extractedBinaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
console.log(`[alphaclaw] ${logLabel} installed at ${targetPath}`);
return targetPath;
} finally {
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
};

// ---------------------------------------------------------------------------
// Parse CLI flags
Expand Down Expand Up @@ -146,10 +221,16 @@ if (portFlag) {

const openclawDir = path.join(rootDir, ".openclaw");
fs.mkdirSync(openclawDir, { recursive: true });
const { hourlyGitSyncPath } = migrateManagedInternalFiles({
const { hourlyGitSyncPath, internalDir } = migrateManagedInternalFiles({
fs,
openclawDir,
});
const managedBinDir = path.join(internalDir, "bin");
fs.mkdirSync(managedBinDir, { recursive: true });
prependPathEntry(managedBinDir);
const installBinDir = isWritableDirectory(kSystemBinDir)
? kSystemBinDir
: managedBinDir;
console.log(`[alphaclaw] Root directory: ${rootDir}`);

// Check for pending update marker (written by the update endpoint before restart).
Expand Down Expand Up @@ -233,6 +314,8 @@ if (fs.existsSync(envFilePath)) {
console.log("[alphaclaw] Loaded .env");
}

const { buildSecretReplacements } = require("../lib/server/helpers");

const runGitSync = () => {
const githubToken = String(process.env.GITHUB_TOKEN || "").trim();
const githubRepo = resolveGithubRepoPath(
Expand Down Expand Up @@ -507,11 +590,12 @@ if (!gogInstalled) {
const arch = os.arch() === "arm64" ? "arm64" : "amd64";
const tarball = `gogcli_${gogVersion}_${platform}_${arch}.tar.gz`;
const url = `https://github.com/steipete/gogcli/releases/download/v${gogVersion}/${tarball}`;
execSync(
`curl -fsSL "${url}" -o /tmp/gog.tar.gz && tar -xzf /tmp/gog.tar.gz -C /tmp/ && mv /tmp/gog /usr/local/bin/gog && chmod +x /usr/local/bin/gog && rm -f /tmp/gog.tar.gz`,
{ stdio: "inherit" },
);
console.log("[alphaclaw] gog CLI installed");
installTarballBinary({
url,
binaryName: "gog",
installDir: installBinDir,
logLabel: "gog CLI",
});
} catch (e) {
console.log(`[alphaclaw] gog install skipped: ${e.message}`);
}
Expand Down Expand Up @@ -549,35 +633,25 @@ try {

if (fs.existsSync(hourlyGitSyncPath)) {
try {
const syncCronConfig = path.join(openclawDir, "cron", "system-sync.json");
let cronEnabled = true;
let cronSchedule = "0 * * * *";

if (fs.existsSync(syncCronConfig)) {
try {
const cfg = JSON.parse(fs.readFileSync(syncCronConfig, "utf8"));
cronEnabled = cfg.enabled !== false;
const schedule = String(cfg.schedule || "").trim();
if (/^(\S+\s+){4}\S+$/.test(schedule)) cronSchedule = schedule;
} catch {}
}

const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
if (cronEnabled) {
const cronContent = [
"SHELL=/bin/bash",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
`${cronSchedule} root bash "${hourlyGitSyncPath}" >> /var/log/openclaw-hourly-sync.log 2>&1`,
"",
].join("\n");
fs.writeFileSync(cronFilePath, cronContent, { mode: 0o644 });
console.log("[alphaclaw] System cron entry installed");
} else {
try {
fs.unlinkSync(cronFilePath);
} catch {}
console.log("[alphaclaw] System cron entry disabled");
}
startManagedScheduler({
fs,
openclawDir,
platform: process.platform,
});
const cronConfig = readSystemCronConfig({
fs,
openclawDir,
platform: process.platform,
});
const cronStatus = applySystemCronConfig({
fs,
openclawDir,
nextConfig: cronConfig,
platform: process.platform,
});
console.log(
`[alphaclaw] System cron ${cronStatus.enabled ? "configured" : "disabled"} (${cronStatus.installMethod})`,
);
} catch (e) {
console.log(`[alphaclaw] Cron setup skipped: ${e.message}`);
}
Expand All @@ -587,15 +661,17 @@ if (fs.existsSync(hourlyGitSyncPath)) {
// 9. Start cron daemon if available
// ---------------------------------------------------------------------------

try {
execSync("command -v cron", { stdio: "ignore" });
if (normalizeCronPlatform(process.platform) !== "darwin") {
try {
execSync("pgrep -x cron", { stdio: "ignore" });
} catch {
execSync("cron", { stdio: "ignore" });
}
console.log("[alphaclaw] Cron daemon running");
} catch {}
execSync("command -v cron", { stdio: "ignore" });
try {
execSync("pgrep -x cron", { stdio: "ignore" });
} catch {
execSync("cron", { stdio: "ignore" });
}
console.log("[alphaclaw] Cron daemon running");
} catch {}
}

// ---------------------------------------------------------------------------
// 10. Reconcile channels if already onboarded
Expand Down Expand Up @@ -821,11 +897,11 @@ try {
execSync("command -v systemctl", { stdio: "ignore" });
} catch {
const shimSrc = path.join(__dirname, "..", "lib", "scripts", "systemctl");
const shimDest = "/usr/local/bin/systemctl";
const shimDest = path.join(installBinDir, "systemctl");
try {
fs.copyFileSync(shimSrc, shimDest);
fs.chmodSync(shimDest, 0o755);
console.log("[alphaclaw] systemctl shim installed");
console.log(`[alphaclaw] systemctl shim installed at ${shimDest}`);
} catch (e) {
console.log(`[alphaclaw] systemctl shim skipped: ${e.message}`);
}
Expand All @@ -837,9 +913,9 @@ try {

try {
const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
const gitAskPassDest = path.join(managedBinDir, "alphaclaw-git-askpass.sh");
const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
const gitShimDest = "/usr/local/bin/git";
const gitShimDest = path.join(installBinDir, "git");

if (fs.existsSync(gitAskPassSrc)) {
fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
Expand Down Expand Up @@ -870,7 +946,7 @@ try {
.replace("@@REAL_GIT@@", realGitPath)
.replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
console.log("[alphaclaw] git auth shim installed");
console.log(`[alphaclaw] git auth shim installed at ${gitShimDest}`);
}
} catch (e) {
console.log(`[alphaclaw] git auth shim skipped: ${e.message}`);
Expand Down
52 changes: 26 additions & 26 deletions lib/server/onboarding/cron.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
const os = require("os");
const path = require("path");
const { kSetupDir } = require("../constants");
const { buildManagedPaths } = require("../internal-files-migration");
const {
applySystemCronConfig,
getSystemCronPaths,
kDefaultSystemCronSchedule,
} = require("../system-cron");

const kHourlyGitSyncTemplatePath = path.join(kSetupDir, "hourly-git-sync.sh");
const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
const kSystemCronConfigDir = "cron";
const kSystemCronConfigFile = "system-sync.json";
const kDefaultSystemCronSchedule = "0 * * * *";

const buildSystemCronFile = ({ schedule, scriptPath }) =>
[
"SHELL=/bin/bash",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
`${schedule} root bash "${scriptPath}" >> /var/log/openclaw-hourly-sync.log 2>&1`,
"",
].join("\n");

const installHourlyGitSyncScript = ({ fs, openclawDir }) => {
try {
Expand All @@ -28,22 +22,28 @@ const installHourlyGitSyncScript = ({ fs, openclawDir }) => {
}
};

const installHourlyGitSyncCron = async ({ fs, openclawDir }) => {
const installHourlyGitSyncCron = async ({
fs,
openclawDir,
platform = os.platform(),
execFileSyncImpl,
}) => {
try {
const { hourlyGitSyncPath } = buildManagedPaths({ openclawDir });
const configDir = `${openclawDir}/${kSystemCronConfigDir}`;
const configPath = `${configDir}/${kSystemCronConfigFile}`;
const config = { enabled: true, schedule: kDefaultSystemCronSchedule };
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));

const cronContent = buildSystemCronFile({
schedule: config.schedule,
scriptPath: hourlyGitSyncPath,
const paths = getSystemCronPaths({ openclawDir, platform });
const status = applySystemCronConfig({
fs,
openclawDir,
nextConfig: {
enabled: true,
schedule: kDefaultSystemCronSchedule,
},
platform,
execFileSyncImpl,
});
fs.writeFileSync(kSystemCronPath, cronContent, { mode: 0o644 });
console.log(`[onboard] Installed system cron job at ${kSystemCronPath} (${configPath})`);
return true;
console.log(
`[onboard] Installed system cron job at ${paths.installPath} (${paths.configPath})`,
);
return status.installed;
} catch (e) {
console.error("[onboard] System cron install error:", e.message);
return false;
Expand Down
9 changes: 8 additions & 1 deletion lib/server/onboarding/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ const createOnboardingService = ({
ensureGatewayProxyConfig,
getBaseUrl,
startGateway,
platform = process.platform,
execFileSyncImpl,
}) => {
const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;

Expand Down Expand Up @@ -506,7 +508,12 @@ const createOnboardingService = ({
installGogCliSkill({ fs, openclawDir: OPENCLAW_DIR });

installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
await installHourlyGitSyncCron({
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS, the managed scheduler only starts during process startup in bin/alphaclaw.js when the sync script already exists. In a fresh onboarding flow, we create the script/config here, but never start the scheduler in-process afterward, so hourly sync will not run until the service restarts. Can we start the managed scheduler after successful cron config install on darwin (or have installHourlyGitSyncCron do that) so onboarding is immediately effective?

fs,
openclawDir: OPENCLAW_DIR,
platform,
execFileSyncImpl,
});
fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
fs.writeFileSync(
kOnboardingMarkerPath,
Expand Down
5 changes: 5 additions & 0 deletions lib/server/routes/onboarding.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { execFileSync } = require("child_process");
const {
createOnboardingService,
getImportedPlaceholderReview,
Expand Down Expand Up @@ -94,6 +95,8 @@ const registerOnboardingRoutes = ({
ensureGatewayProxyConfig,
getBaseUrl,
startGateway,
platform = process.platform,
execFileSyncImpl = execFileSync,
}) => {
// Keep mutating onboarding routes marker-gated so in-progress imports
// can promote files before the final completion marker is written.
Expand All @@ -115,6 +118,8 @@ const registerOnboardingRoutes = ({
ensureGatewayProxyConfig,
getBaseUrl,
startGateway,
platform,
execFileSyncImpl,
});

const kEnvVarNamePattern = /^[A-Z_][A-Z0-9_]*$/;
Expand Down
Loading