build: PaddedCell-style install.mjs + SDK-aligned packaging
install.mjs (--install/--build-only/--uninstall/--openclaw-profile-path), tsconfig outDir dist/fabric, package.json openclaw file dep + main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
212
install.mjs
Normal file
212
install.mjs
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fabric.OpenclawPlugin installer (modeled on PaddedCell's install.mjs).
|
||||
*
|
||||
* node install.mjs --install build + install + configure
|
||||
* node install.mjs --build-only build only
|
||||
* node install.mjs --uninstall remove plugin + config
|
||||
* flags: --skip-check --verbose -v --openclaw-profile-path <dir>
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync } from 'fs';
|
||||
import { dirname, join, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const __dirname = resolve(dirname(fileURLToPath(import.meta.url)));
|
||||
const PLUGIN_ID = 'fabric';
|
||||
const DIST_DIR = join(__dirname, 'dist', 'fabric');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = {
|
||||
buildOnly: args.includes('--build-only'),
|
||||
skipCheck: args.includes('--skip-check'),
|
||||
verbose: args.includes('--verbose') || args.includes('-v'),
|
||||
uninstall: args.includes('--uninstall'),
|
||||
};
|
||||
const pIdx = args.indexOf('--openclaw-profile-path');
|
||||
const profileOverride = pIdx !== -1 && args[pIdx + 1] ? resolve(args[pIdx + 1]) : null;
|
||||
|
||||
function openclawPath() {
|
||||
if (profileOverride) return profileOverride;
|
||||
if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH);
|
||||
return join(homedir(), '.openclaw');
|
||||
}
|
||||
|
||||
const c = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' };
|
||||
const log = (m, k = 'reset') => console.log(`${c[k]}${m}${c.reset}`);
|
||||
const step = (n, t, m) => log(`[${n}/${t}] ${m}`, 'cyan');
|
||||
const ok = (m) => log(` ✓ ${m}`, 'green');
|
||||
const warn = (m) => log(` ⚠ ${m}`, 'yellow');
|
||||
const err = (m) => log(` ✗ ${m}`, 'red');
|
||||
|
||||
function exec(cmd, o = {}) {
|
||||
return execSync(cmd, { cwd: __dirname, stdio: o.silent ? 'pipe' : 'inherit', encoding: 'utf8', ...o });
|
||||
}
|
||||
function cfgGet(key, def) {
|
||||
try {
|
||||
const out = exec(`openclaw config get ${key} --json 2>/dev/null || echo undefined`, { silent: true }).trim();
|
||||
return out === 'undefined' || out === '' ? def : JSON.parse(out);
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
function cfgSet(key, val) {
|
||||
exec(`openclaw config set ${key} '${JSON.stringify(val)}' --json`, { silent: true });
|
||||
}
|
||||
function cfgUnset(key) {
|
||||
try {
|
||||
exec(`openclaw config unset ${key}`, { silent: true });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
function copyDir(src, dest) {
|
||||
mkdirSync(dest, { recursive: true });
|
||||
for (const e of readdirSync(src, { withFileTypes: true })) {
|
||||
if (e.name === 'node_modules') continue;
|
||||
const s = join(src, e.name);
|
||||
const d = join(dest, e.name);
|
||||
e.isDirectory() ? copyDir(s, d) : copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
|
||||
function detect() {
|
||||
step(1, 5, 'Detecting environment...');
|
||||
let node = null;
|
||||
try {
|
||||
node = exec('node --version', { silent: true }).trim();
|
||||
ok(`Node ${node}`);
|
||||
} catch {
|
||||
err('Node not found');
|
||||
}
|
||||
try {
|
||||
ok(`openclaw at ${exec('which openclaw', { silent: true }).trim()}`);
|
||||
} catch {
|
||||
warn('openclaw CLI not in PATH');
|
||||
}
|
||||
return { node };
|
||||
}
|
||||
|
||||
function checkDeps(env) {
|
||||
if (opt.skipCheck) return;
|
||||
step(2, 5, 'Checking dependencies...');
|
||||
if (!env.node || parseInt(env.node.slice(1), 10) < 18) {
|
||||
err('Node 18+ required');
|
||||
process.exit(1);
|
||||
}
|
||||
ok('deps OK');
|
||||
}
|
||||
|
||||
function build() {
|
||||
step(3, 5, 'Building plugin...');
|
||||
rmSync(join(__dirname, 'dist'), { recursive: true, force: true });
|
||||
exec('npm install', { silent: !opt.verbose });
|
||||
exec('npm run build', { silent: !opt.verbose });
|
||||
if (!existsSync(join(DIST_DIR, 'index.js'))) throw new Error('build produced no dist/fabric/index.js');
|
||||
ok('compiled -> dist/fabric');
|
||||
}
|
||||
|
||||
function clearInstall(base) {
|
||||
const dest = join(base, 'plugins', PLUGIN_ID);
|
||||
if (existsSync(dest)) {
|
||||
rmSync(dest, { recursive: true, force: true });
|
||||
ok(`removed ${dest}`);
|
||||
}
|
||||
}
|
||||
function cleanupConfig(base) {
|
||||
const dest = join(base, 'plugins', PLUGIN_ID);
|
||||
const allow = cfgGet('plugins.allow', []);
|
||||
if (Array.isArray(allow) && allow.includes(PLUGIN_ID)) {
|
||||
cfgSet('plugins.allow', allow.filter((x) => x !== PLUGIN_ID));
|
||||
ok('removed from plugins.allow');
|
||||
}
|
||||
const paths = cfgGet('plugins.load.paths', []);
|
||||
if (Array.isArray(paths) && paths.includes(dest)) {
|
||||
cfgSet('plugins.load.paths', paths.filter((x) => x !== dest));
|
||||
ok('removed from plugins.load.paths');
|
||||
}
|
||||
cfgUnset(`plugins.entries.${PLUGIN_ID}`);
|
||||
ok('removed plugin entry');
|
||||
}
|
||||
|
||||
function install() {
|
||||
step(4, 5, 'Installing...');
|
||||
const base = openclawPath();
|
||||
const dest = join(base, 'plugins', PLUGIN_ID);
|
||||
log(` OpenClaw path: ${base}`, 'blue');
|
||||
|
||||
if (existsSync(dest)) {
|
||||
warn('existing install -> replacing');
|
||||
clearInstall(base);
|
||||
}
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
copyDir(DIST_DIR, dest);
|
||||
copyFileSync(join(__dirname, 'openclaw.plugin.json'), join(dest, 'openclaw.plugin.json'));
|
||||
copyFileSync(join(__dirname, 'package.json'), join(dest, 'package.json'));
|
||||
ok(`plugin files -> ${dest}`);
|
||||
exec('npm install --omit=dev', { cwd: dest, silent: !opt.verbose });
|
||||
ok('runtime deps installed');
|
||||
return { base, dest };
|
||||
}
|
||||
|
||||
function configure(base, dest) {
|
||||
step(5, 5, 'Configuring OpenClaw...');
|
||||
const paths = cfgGet('plugins.load.paths', []);
|
||||
if (Array.isArray(paths) && !paths.includes(dest)) {
|
||||
cfgSet('plugins.load.paths', [...paths, dest]);
|
||||
}
|
||||
ok(`plugins.load.paths includes ${dest}`);
|
||||
|
||||
const allow = cfgGet('plugins.allow', []);
|
||||
if (Array.isArray(allow) && !allow.includes(PLUGIN_ID)) {
|
||||
cfgSet('plugins.allow', [...allow, PLUGIN_ID]);
|
||||
}
|
||||
ok(`plugins.allow includes ${PLUGIN_ID}`);
|
||||
|
||||
if (cfgGet(`plugins.entries.${PLUGIN_ID}.enabled`, undefined) === undefined) {
|
||||
cfgSet(`plugins.entries.${PLUGIN_ID}.enabled`, true);
|
||||
}
|
||||
if (cfgGet('channels.fabric.centerApiBase', undefined) === undefined) {
|
||||
cfgSet('channels.fabric.centerApiBase', 'http://localhost:7001/api');
|
||||
ok('channels.fabric.centerApiBase = http://localhost:7001/api (default)');
|
||||
}
|
||||
ok('plugin entry configured (missing defaults only)');
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('');
|
||||
log('Fabric.OpenclawPlugin installer', 'cyan');
|
||||
console.log('');
|
||||
try {
|
||||
const env = detect();
|
||||
if (opt.uninstall) {
|
||||
const base = openclawPath();
|
||||
clearInstall(base);
|
||||
cleanupConfig(base);
|
||||
log('\nRun: openclaw gateway restart', 'yellow');
|
||||
return;
|
||||
}
|
||||
checkDeps(env);
|
||||
build();
|
||||
if (opt.buildOnly) {
|
||||
log('\nbuild-only — not installed.', 'yellow');
|
||||
return;
|
||||
}
|
||||
const { base, dest } = install();
|
||||
configure(base, dest);
|
||||
console.log('');
|
||||
log('Install complete. Next:', 'blue');
|
||||
log(' 1. Mint an agent key: (in Center) node dist/cli.js user apikey --email <agent-email>', 'cyan');
|
||||
log(' 2. openclaw gateway restart', 'cyan');
|
||||
log(' 3. As an agent, call the fabric-register tool with that key', 'cyan');
|
||||
console.log('');
|
||||
} catch (e) {
|
||||
log(`\nInstall failed: ${e.message}`, 'red');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user