|
| 1 | +#!/usr/bin/env node |
| 2 | +// npm-facing installer for openclaw-smart. |
| 3 | +// |
| 4 | +// This intentionally mirrors the core non-interactive install steps from |
| 5 | +// `reflexio setup openclaw` so npm and Python installs do not drift. |
| 6 | + |
| 7 | +import { spawnSync } from "node:child_process"; |
| 8 | +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
| 9 | +import { homedir } from "node:os"; |
| 10 | +import { dirname, join, resolve } from "node:path"; |
| 11 | +import { fileURLToPath } from "node:url"; |
| 12 | + |
| 13 | +const PLUGIN_ID = "reflexio-openclaw-smart"; |
| 14 | +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); |
| 15 | +const PLUGIN_ROOT = resolve(SCRIPT_DIR, ".."); |
| 16 | +const REFLEXIO_DIR = join(homedir(), ".reflexio"); |
| 17 | +const REFLEXIO_ENV = join(REFLEXIO_DIR, ".env"); |
| 18 | +const STALE_EXTENSION_DIR = join(homedir(), ".openclaw", "extensions", PLUGIN_ID); |
| 19 | + |
| 20 | +function usage() { |
| 21 | + console.log(`openclaw-smart |
| 22 | +
|
| 23 | +Usage: |
| 24 | + openclaw-smart install |
| 25 | + openclaw-smart uninstall [--purge] |
| 26 | + openclaw-smart repair |
| 27 | +
|
| 28 | +Install registers the bundled OpenClaw plugin, enables typed hook access, |
| 29 | +writes OPENCLAW_BIN to ~/.reflexio/.env, warms dependencies, and verifies |
| 30 | +that OpenClaw loaded the plugin.`); |
| 31 | +} |
| 32 | + |
| 33 | +function fail(message, code = 1) { |
| 34 | + console.error(`openclaw-smart: ${message}`); |
| 35 | + process.exit(code); |
| 36 | +} |
| 37 | + |
| 38 | +function run(argv, opts = {}) { |
| 39 | + const result = spawnSync(argv[0], argv.slice(1), { |
| 40 | + encoding: "utf8", |
| 41 | + stdio: opts.stdio ?? "pipe", |
| 42 | + ...opts, |
| 43 | + }); |
| 44 | + if (result.error) { |
| 45 | + return { |
| 46 | + status: 1, |
| 47 | + stdout: result.stdout ?? "", |
| 48 | + stderr: result.error.message, |
| 49 | + }; |
| 50 | + } |
| 51 | + return { |
| 52 | + status: result.status ?? 1, |
| 53 | + stdout: result.stdout ?? "", |
| 54 | + stderr: result.stderr ?? "", |
| 55 | + }; |
| 56 | +} |
| 57 | + |
| 58 | +function findExecutable(name) { |
| 59 | + const candidates = []; |
| 60 | + if (process.env.PATH) { |
| 61 | + for (const dir of process.env.PATH.split(process.platform === "win32" ? ";" : ":")) { |
| 62 | + if (!dir) continue; |
| 63 | + candidates.push(join(dir, name)); |
| 64 | + if (process.platform === "win32") candidates.push(join(dir, `${name}.cmd`)); |
| 65 | + if (process.platform === "win32") candidates.push(join(dir, `${name}.exe`)); |
| 66 | + } |
| 67 | + } |
| 68 | + for (const candidate of candidates) { |
| 69 | + if (existsSync(candidate)) return candidate; |
| 70 | + } |
| 71 | + return null; |
| 72 | +} |
| 73 | + |
| 74 | +function resolveOpenClawBin() { |
| 75 | + const configured = process.env.OPENCLAW_BIN; |
| 76 | + if (configured && existsSync(configured)) return configured; |
| 77 | + return findExecutable("openclaw"); |
| 78 | +} |
| 79 | + |
| 80 | +function shellQuote(value) { |
| 81 | + return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; |
| 82 | +} |
| 83 | + |
| 84 | +function upsertEnv(envPath, updates) { |
| 85 | + mkdirSync(dirname(envPath), { recursive: true }); |
| 86 | + const existing = existsSync(envPath) ? readFileSync(envPath, "utf8").split(/\r?\n/) : []; |
| 87 | + const remaining = existing.filter((line) => { |
| 88 | + const trimmed = line.trim(); |
| 89 | + if (!trimmed || trimmed.startsWith("#")) return true; |
| 90 | + return !Object.keys(updates).some((key) => trimmed.startsWith(`${key}=`)); |
| 91 | + }); |
| 92 | + for (const [key, value] of Object.entries(updates)) { |
| 93 | + remaining.push(`${key}=${shellQuote(value)}`); |
| 94 | + } |
| 95 | + writeFileSync(envPath, `${remaining.filter((line) => line.length > 0).join("\n")}\n`); |
| 96 | +} |
| 97 | + |
| 98 | +function removeEnvKeys(envPath, keys) { |
| 99 | + if (!existsSync(envPath)) return; |
| 100 | + const kept = readFileSync(envPath, "utf8") |
| 101 | + .split(/\r?\n/) |
| 102 | + .filter((line) => !keys.some((key) => line.trim().startsWith(`${key}=`))) |
| 103 | + .filter((line) => line.length > 0); |
| 104 | + writeFileSync(envPath, kept.length ? `${kept.join("\n")}\n` : ""); |
| 105 | +} |
| 106 | + |
| 107 | +function ensurePluginRoot() { |
| 108 | + if (!existsSync(join(PLUGIN_ROOT, "openclaw.plugin.json"))) { |
| 109 | + fail(`plugin root is incomplete: ${PLUGIN_ROOT}`); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +function runOpenClaw(cli, args, opts = {}) { |
| 114 | + return run([cli, ...args], opts); |
| 115 | +} |
| 116 | + |
| 117 | +function printCommandFailure(label, result) { |
| 118 | + const detail = (result.stderr || result.stdout || "").trim(); |
| 119 | + console.error(`openclaw-smart: ${label} failed${detail ? `: ${detail}` : ""}`); |
| 120 | +} |
| 121 | + |
| 122 | +function runSmartInstall() { |
| 123 | + const script = join(PLUGIN_ROOT, "scripts", "smart-install.sh"); |
| 124 | + if (!existsSync(script)) { |
| 125 | + console.warn(`openclaw-smart: ${script} missing; skipping dependency warmup`); |
| 126 | + return; |
| 127 | + } |
| 128 | + const result = run(["bash", script], { stdio: "inherit" }); |
| 129 | + if (result.status !== 0) { |
| 130 | + console.warn( |
| 131 | + `openclaw-smart: smart-install.sh exited ${result.status}; first session may bootstrap dependencies`, |
| 132 | + ); |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +function inspectLoaded(cli) { |
| 137 | + const result = runOpenClaw(cli, ["plugins", "inspect", PLUGIN_ID]); |
| 138 | + return result.status === 0 && /Status:\s*loaded\b/.test(result.stdout); |
| 139 | +} |
| 140 | + |
| 141 | +function install() { |
| 142 | + ensurePluginRoot(); |
| 143 | + const cli = resolveOpenClawBin(); |
| 144 | + if (!cli) fail("openclaw CLI not found. Install OpenClaw first or set OPENCLAW_BIN."); |
| 145 | + |
| 146 | + upsertEnv(REFLEXIO_ENV, { |
| 147 | + OPENCLAW_BIN: cli, |
| 148 | + OPENCLAW_SMART_USE_LOCAL_CLI: "1", |
| 149 | + }); |
| 150 | + |
| 151 | + runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]); |
| 152 | + rmSync(STALE_EXTENSION_DIR, { recursive: true, force: true }); |
| 153 | + |
| 154 | + const installResult = runOpenClaw(cli, ["plugins", "install", PLUGIN_ROOT]); |
| 155 | + if (installResult.status !== 0) { |
| 156 | + printCommandFailure("plugins install", installResult); |
| 157 | + process.exit(1); |
| 158 | + } |
| 159 | + |
| 160 | + const enableResult = runOpenClaw(cli, ["plugins", "enable", PLUGIN_ID]); |
| 161 | + if (enableResult.status !== 0) { |
| 162 | + printCommandFailure("plugins enable", enableResult); |
| 163 | + process.exit(1); |
| 164 | + } |
| 165 | + |
| 166 | + const accessResult = runOpenClaw(cli, [ |
| 167 | + "config", |
| 168 | + "set", |
| 169 | + `plugins.entries.${PLUGIN_ID}.hooks.allowConversationAccess`, |
| 170 | + "true", |
| 171 | + ]); |
| 172 | + if (accessResult.status !== 0) { |
| 173 | + printCommandFailure("config set allowConversationAccess", accessResult); |
| 174 | + process.exit(1); |
| 175 | + } |
| 176 | + |
| 177 | + runSmartInstall(); |
| 178 | + runOpenClaw(cli, ["gateway", "restart"]); |
| 179 | + |
| 180 | + if (!inspectLoaded(cli)) { |
| 181 | + fail(`plugin not loaded; check 'openclaw plugins inspect ${PLUGIN_ID}'`); |
| 182 | + } |
| 183 | + console.log("openclaw-smart installed and registered."); |
| 184 | +} |
| 185 | + |
| 186 | +function uninstall({ purge = false } = {}) { |
| 187 | + const cli = resolveOpenClawBin(); |
| 188 | + if (cli) { |
| 189 | + runOpenClaw(cli, ["plugins", "disable", PLUGIN_ID]); |
| 190 | + runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]); |
| 191 | + } else { |
| 192 | + console.warn("openclaw-smart: openclaw CLI not found; skipping plugin removal"); |
| 193 | + } |
| 194 | + removeEnvKeys(REFLEXIO_ENV, ["OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI"]); |
| 195 | + if (purge) rmSync(join(homedir(), ".openclaw-smart"), { recursive: true, force: true }); |
| 196 | + console.log("openclaw-smart uninstalled."); |
| 197 | +} |
| 198 | + |
| 199 | +function repair() { |
| 200 | + runSmartInstall(); |
| 201 | + console.log("openclaw-smart repair complete."); |
| 202 | +} |
| 203 | + |
| 204 | +const [command, ...args] = process.argv.slice(2); |
| 205 | +if (!command || command === "-h" || command === "--help") { |
| 206 | + usage(); |
| 207 | + process.exit(0); |
| 208 | +} |
| 209 | +if (command === "install") install(); |
| 210 | +else if (command === "uninstall") uninstall({ purge: args.includes("--purge") }); |
| 211 | +else if (command === "repair") repair(); |
| 212 | +else { |
| 213 | + usage(); |
| 214 | + fail(`unknown command: ${command}`); |
| 215 | +} |
0 commit comments