openclaw-ani-installer 2026.3.292

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @wzfukui/openclaw-ani
2
+
3
+ Installer and updater for the ANI plugin on OpenClaw.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx -y @wzfukui/openclaw-ani install
9
+ npx -y @wzfukui/openclaw-ani update
10
+ ```
11
+
12
+ ## Options
13
+
14
+ ```bash
15
+ npx -y @wzfukui/openclaw-ani install --version 2026.3.282
16
+ npx -y @wzfukui/openclaw-ani update --openclaw-home ~/.openclaw
17
+ npx -y @wzfukui/openclaw-ani install --dry-run
18
+ ```
19
+
20
+ ## Notes
21
+
22
+ - The installer always resolves the ANI plugin from npm as a tarball first.
23
+ - It then hydrates runtime dependencies and performs an atomic replacement into `~/.openclaw/extensions/ani`.
24
+ - This avoids known compatibility issues in older OpenClaw releases when installing third-party scoped npm plugins directly.
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ const INSTALLER_VERSION = "2026.3.29";
9
+ const PLUGIN_ID = "ani";
10
+ const PLUGIN_NPM_NAME = "@wzfukui/ani";
11
+
12
+ function fail(message, code = 1) {
13
+ console.error(message);
14
+ process.exit(code);
15
+ }
16
+
17
+ function run(command, args, options = {}) {
18
+ const result = spawnSync(command, args, {
19
+ stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
20
+ encoding: "utf8",
21
+ env: options.env ?? process.env,
22
+ cwd: options.cwd,
23
+ });
24
+ if (options.capture) {
25
+ return result;
26
+ }
27
+ if (result.status !== 0) {
28
+ fail(`${command} ${args.join(" ")} failed with exit code ${result.status ?? 1}`);
29
+ }
30
+ return result;
31
+ }
32
+
33
+ function parseArgs(argv) {
34
+ const args = {
35
+ command: "install",
36
+ version: "latest",
37
+ dryRun: false,
38
+ openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"),
39
+ };
40
+ const positional = [];
41
+ for (let i = 0; i < argv.length; i += 1) {
42
+ const token = argv[i];
43
+ if (token === "--version") {
44
+ args.version = argv[i + 1] || fail("missing value for --version");
45
+ i += 1;
46
+ continue;
47
+ }
48
+ if (token === "--openclaw-home") {
49
+ args.openclawHome = argv[i + 1] || fail("missing value for --openclaw-home");
50
+ i += 1;
51
+ continue;
52
+ }
53
+ if (token === "--dry-run") {
54
+ args.dryRun = true;
55
+ continue;
56
+ }
57
+ if (token === "-h" || token === "--help") {
58
+ printHelp();
59
+ process.exit(0);
60
+ }
61
+ positional.push(token);
62
+ }
63
+ if (positional.length > 0) {
64
+ args.command = positional[0];
65
+ }
66
+ return args;
67
+ }
68
+
69
+ function printHelp() {
70
+ console.log(`openclaw-ani ${INSTALLER_VERSION}
71
+
72
+ Usage:
73
+ openclaw-ani install [--version <version>] [--openclaw-home <dir>] [--dry-run]
74
+ openclaw-ani update [--version <version>] [--openclaw-home <dir>] [--dry-run]
75
+ openclaw-ani doctor [--openclaw-home <dir>]
76
+ `);
77
+ }
78
+
79
+ function ensureDir(dir) {
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ }
82
+
83
+ function readJson(file) {
84
+ return JSON.parse(fs.readFileSync(file, "utf8"));
85
+ }
86
+
87
+ function writeJson(file, value) {
88
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
89
+ }
90
+
91
+ function resolveOpenClawBinary(homeDir) {
92
+ const candidate = path.join(homeDir, "bin", "openclaw");
93
+ if (fs.existsSync(candidate)) {
94
+ return candidate;
95
+ }
96
+ const which = run("bash", ["-lc", "command -v openclaw"], { capture: true });
97
+ if (which.status === 0) {
98
+ return which.stdout.trim();
99
+ }
100
+ fail(`openclaw binary not found under ${candidate} or PATH`);
101
+ }
102
+
103
+ function resolveState(homeDir) {
104
+ const configPath = path.join(homeDir, "openclaw.json");
105
+ if (!fs.existsSync(configPath)) {
106
+ fail(`OpenClaw config not found: ${configPath}`);
107
+ }
108
+ return {
109
+ configPath,
110
+ pluginsDir: path.join(homeDir, "extensions"),
111
+ pluginDir: path.join(homeDir, "extensions", PLUGIN_ID),
112
+ tmpDir: fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ani-installer-")),
113
+ };
114
+ }
115
+
116
+ function cleanupTemp(tmpDir) {
117
+ fs.rmSync(tmpDir, { recursive: true, force: true });
118
+ }
119
+
120
+ function npmPack(spec, cwd, env) {
121
+ const result = run("npm", ["pack", spec], { cwd, env, capture: true });
122
+ if (result.status !== 0) {
123
+ fail(result.stderr || result.stdout || `npm pack failed for ${spec}`);
124
+ }
125
+ const filename = result.stdout.trim().split(/\n/).filter(Boolean).at(-1);
126
+ if (!filename) {
127
+ fail(`npm pack produced no filename for ${spec}`);
128
+ }
129
+ return path.join(cwd, filename);
130
+ }
131
+
132
+ function hydratePackage(extractDir, env) {
133
+ const packageJsonPath = path.join(extractDir, "package.json");
134
+ const pkg = readJson(packageJsonPath);
135
+ delete pkg.devDependencies;
136
+ delete pkg.peerDependencies;
137
+ writeJson(packageJsonPath, pkg);
138
+ const install = run("npm", ["install", "--omit=dev", "--package-lock=false"], {
139
+ cwd: extractDir,
140
+ env,
141
+ capture: true,
142
+ });
143
+ if (install.status !== 0) {
144
+ fail(install.stderr || install.stdout || "npm install failed while hydrating ANI");
145
+ }
146
+ }
147
+
148
+ function extractTarball(tgzPath, destDir) {
149
+ ensureDir(destDir);
150
+ const result = run("tar", ["-xzf", tgzPath, "-C", destDir, "--strip-components=1"], {
151
+ capture: true,
152
+ });
153
+ if (result.status !== 0) {
154
+ fail(result.stderr || result.stdout || `failed to extract ${tgzPath}`);
155
+ }
156
+ }
157
+
158
+ function updateConfigInstallRecord(configPath, tgzPath, pluginDir, version) {
159
+ const config = readJson(configPath);
160
+ const plugins = (config.plugins ??= {});
161
+ const installs = (plugins.installs ??= {});
162
+ installs[PLUGIN_ID] = {
163
+ source: "archive",
164
+ sourcePath: tgzPath,
165
+ installPath: pluginDir,
166
+ version,
167
+ installedAt: new Date().toISOString(),
168
+ };
169
+ const allow = new Set(Array.isArray(plugins.allow) ? plugins.allow : []);
170
+ allow.add(PLUGIN_ID);
171
+ plugins.allow = [...allow];
172
+ const entries = (plugins.entries ??= {});
173
+ entries[PLUGIN_ID] = { ...(entries[PLUGIN_ID] ?? {}), enabled: true };
174
+ writeJson(configPath, config);
175
+ }
176
+
177
+ function atomicReplaceDir(sourceDir, targetDir) {
178
+ const backupDir = `${targetDir}.bak-${Date.now()}`;
179
+ if (fs.existsSync(targetDir)) {
180
+ fs.renameSync(targetDir, backupDir);
181
+ }
182
+ fs.renameSync(sourceDir, targetDir);
183
+ if (fs.existsSync(backupDir)) {
184
+ fs.rmSync(backupDir, { recursive: true, force: true });
185
+ }
186
+ }
187
+
188
+ function getInstalledVersion(pluginDir) {
189
+ const packageJsonPath = path.join(pluginDir, "package.json");
190
+ if (!fs.existsSync(packageJsonPath)) {
191
+ return null;
192
+ }
193
+ try {
194
+ return readJson(packageJsonPath).version ?? null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ function runOpenClawCheck(openclawBin, args) {
201
+ const result = run(openclawBin, args, { capture: true });
202
+ return {
203
+ ok: result.status === 0,
204
+ stdout: result.stdout?.trim() ?? "",
205
+ stderr: result.stderr?.trim() ?? "",
206
+ };
207
+ }
208
+
209
+ function installOrUpdate(params) {
210
+ const openclawBin = resolveOpenClawBinary(params.openclawHome);
211
+ const state = resolveState(params.openclawHome);
212
+ const env = { ...process.env };
213
+ try {
214
+ const spec =
215
+ params.version === "latest" ? PLUGIN_NPM_NAME : `${PLUGIN_NPM_NAME}@${params.version}`;
216
+ const tgzPath = npmPack(spec, state.tmpDir, env);
217
+ const extractDir = path.join(state.tmpDir, "plugin");
218
+ extractTarball(tgzPath, extractDir);
219
+ hydratePackage(extractDir, env);
220
+ const version = getInstalledVersion(extractDir);
221
+ if (!version) {
222
+ fail("Unable to determine packaged ANI version after extraction");
223
+ }
224
+ if (params.dryRun) {
225
+ console.log(
226
+ JSON.stringify(
227
+ {
228
+ action: params.command,
229
+ spec,
230
+ openclawHome: params.openclawHome,
231
+ installPath: state.pluginDir,
232
+ version,
233
+ },
234
+ null,
235
+ 2,
236
+ ),
237
+ );
238
+ return;
239
+ }
240
+ ensureDir(state.pluginsDir);
241
+ atomicReplaceDir(extractDir, state.pluginDir);
242
+ const persistedTgz = path.join(os.tmpdir(), path.basename(tgzPath));
243
+ fs.copyFileSync(tgzPath, persistedTgz);
244
+ updateConfigInstallRecord(state.configPath, persistedTgz, state.pluginDir, version);
245
+ const inspect = runOpenClawCheck(openclawBin, ["plugins", "inspect", PLUGIN_ID]);
246
+ if (!inspect.ok) {
247
+ fail(inspect.stderr || inspect.stdout || "OpenClaw failed to inspect ANI after install");
248
+ }
249
+ console.log(inspect.stdout);
250
+ const doctor = runOpenClawCheck(openclawBin, ["plugins", "doctor"]);
251
+ if (!doctor.ok) {
252
+ fail(doctor.stderr || doctor.stdout || "OpenClaw plugin doctor reported errors");
253
+ }
254
+ console.log(doctor.stdout);
255
+ } finally {
256
+ cleanupTemp(state.tmpDir);
257
+ }
258
+ }
259
+
260
+ function doctor(params) {
261
+ const openclawBin = resolveOpenClawBinary(params.openclawHome);
262
+ const inspect = runOpenClawCheck(openclawBin, ["plugins", "inspect", PLUGIN_ID]);
263
+ if (inspect.stdout) {
264
+ console.log(inspect.stdout);
265
+ }
266
+ if (inspect.stderr) {
267
+ console.error(inspect.stderr);
268
+ }
269
+ const doctorResult = runOpenClawCheck(openclawBin, ["plugins", "doctor"]);
270
+ if (doctorResult.stdout) {
271
+ console.log(doctorResult.stdout);
272
+ }
273
+ if (doctorResult.stderr) {
274
+ console.error(doctorResult.stderr);
275
+ }
276
+ process.exit(doctorResult.ok ? 0 : 1);
277
+ }
278
+
279
+ const args = parseArgs(process.argv.slice(2));
280
+ if (!["install", "update", "doctor"].includes(args.command)) {
281
+ printHelp();
282
+ fail(`unknown command: ${args.command}`);
283
+ }
284
+
285
+ if (args.command === "doctor") {
286
+ doctor(args);
287
+ } else {
288
+ installOrUpdate(args);
289
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "openclaw-ani-installer",
3
+ "version": "2026.3.292",
4
+ "description": "Installer and updater for the ANI OpenClaw plugin",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "openclaw-ani": "./bin/openclaw-ani.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "openclaw",
16
+ "ani",
17
+ "installer"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/wzfukui/openclaw-ani-installer.git"
22
+ },
23
+ "homepage": "https://github.com/wzfukui/openclaw-ani-installer#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/wzfukui/openclaw-ani-installer/issues"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }