@totalreclaw/totalreclaw 3.3.2 → 3.3.4-rc.1

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/postinstall.mjs DELETED
@@ -1,260 +0,0 @@
1
- // scanner-sim: allow — postinstall scripts run during `npm install`, NOT inside the OpenClaw runtime sandbox. Per check-scanner.mjs guidance ("Moving the subprocess call into a separate post-install helper that OpenClaw sandboxes (NOT covered by this scanner)"), this file is the intended home for child_process usage. The plugin's runtime code (index.ts, etc.) stays scanner-clean; this file only runs once at install-time.
2
- /**
3
- * postinstall.mjs — TotalReclaw plugin post-install lifecycle script.
4
- *
5
- * Runs after `npm install` finishes inside the plugin extension dir
6
- * (`~/.openclaw/extensions/totalreclaw/`). Three jobs, in order:
7
- *
8
- * 1. Clean the partial-install marker (`.tr-partial-install`) that
9
- * `preinstall` dropped. Mirrors the inline shim that shipped in
10
- * pre-3.3.2 releases.
11
- * 2. (3.3.2-rc.1 / issue #188) Smoke-check critical deps. After `npm
12
- * install` claims success we require() the modules whose absence
13
- * bricked rc.22 first-attempt installs (`@scure/bip39`,
14
- * `@scure/bip39/wordlists/english.js`, `@totalreclaw/core`,
15
- * `@totalreclaw/client`, `qrcode`, `ws`). If any throws, the
16
- * post-install fails LOUDLY — better than the rc.21 silent
17
- * half-install where `enabled: true` shipped with a missing dep.
18
- * 3. (3.3.2-rc.1 / issue #190) Sweep `<extensions>/.openclaw-install-stage-*`
19
- * siblings. The runtime register() helper handles this on plugin
20
- * load too, but doing it here means a re-install starts from a
21
- * clean parent dir — no "duplicate plugin id detected; global
22
- * plugin will be overridden by global plugin" warning during the
23
- * install itself.
24
- *
25
- * Constraints:
26
- * - Must be idempotent: re-running on a clean tree is a no-op.
27
- * - Must not import any production module that itself runs `register()`
28
- * or makes outbound calls. We use only Node stdlib + dynamic require()
29
- * of the smoke-check deps.
30
- * - Must run in CommonJS-compatible Node ESM (the plugin's package.json
31
- * declares `"type": "module"`, so this file uses `.mjs` and
32
- * `createRequire` to call require() against the plugin's node_modules).
33
- *
34
- * Phrase-safety note: this file does NOT touch credentials.json, mnemonics,
35
- * keys, or any phrase code path. It only validates module loading and
36
- * cleans staging directories.
37
- */
38
-
39
- import fs from 'node:fs';
40
- import path from 'node:path';
41
- import { fileURLToPath } from 'node:url';
42
- import { createRequire } from 'node:module';
43
- import { execSync } from 'node:child_process';
44
-
45
- const here = path.dirname(fileURLToPath(import.meta.url));
46
- const require = createRequire(import.meta.url);
47
-
48
- const PARTIAL_INSTALL_MARKER = '.tr-partial-install';
49
-
50
- // Order matters: light, fast modules first so a failure surfaces quickly.
51
- // `@scure/bip39/wordlists/english.js` is the EXACT path that bricked rc.21
52
- // (issue #188 — `Cannot find module '@scure/bip39/wordlists/english.js'`).
53
- const CRITICAL_DEPS = [
54
- '@scure/bip39',
55
- '@scure/bip39/wordlists/english.js',
56
- '@totalreclaw/core',
57
- '@totalreclaw/client',
58
- 'qrcode',
59
- 'ws',
60
- ];
61
-
62
- function log(msg) {
63
- process.stdout.write(`[totalreclaw postinstall] ${msg}\n`);
64
- }
65
-
66
- function warn(msg) {
67
- process.stderr.write(`[totalreclaw postinstall] WARN: ${msg}\n`);
68
- }
69
-
70
- // ---------------------------------------------------------------------------
71
- // Step 1 — clear .tr-partial-install marker
72
- // ---------------------------------------------------------------------------
73
-
74
- function clearPartialInstallMarker() {
75
- try {
76
- const markerPath = path.join(here, PARTIAL_INSTALL_MARKER);
77
- if (fs.existsSync(markerPath)) {
78
- fs.unlinkSync(markerPath);
79
- log('cleared .tr-partial-install marker');
80
- }
81
- } catch (err) {
82
- // Best-effort. The runtime register() also clears this defensively.
83
- warn(`could not clear .tr-partial-install marker: ${err.message}`);
84
- }
85
- }
86
-
87
- // ---------------------------------------------------------------------------
88
- // Step 2 — atomic critical-dep validation (issue #188)
89
- // ---------------------------------------------------------------------------
90
-
91
- /**
92
- * Try to require() each critical dep. Returns the list of names that
93
- * failed; an empty array means everything resolved.
94
- */
95
- function smokeCheckDeps() {
96
- const missing = [];
97
- for (const dep of CRITICAL_DEPS) {
98
- try {
99
- require(dep);
100
- } catch (err) {
101
- missing.push({ dep, message: err.message });
102
- }
103
- }
104
- return missing;
105
- }
106
-
107
- /**
108
- * Recovery path: if smoke-check fails, blow away the local node_modules
109
- * tree the parent install populated and re-run `npm install --no-audit
110
- * --no-fund --no-save --offline=false` once. This is meant to recover
111
- * from race-condition partial-fetches (issue #188), NOT from a missing
112
- * dep in package.json.
113
- *
114
- * If the second attempt also fails, exit non-zero so `openclaw plugins
115
- * install` surfaces the failure to the agent rather than writing
116
- * `enabled: true` over a broken install.
117
- *
118
- * Skipped if `TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1` (CI / sandboxes that
119
- * cannot reach the registry from inside the postinstall hook).
120
- */
121
- function retryNpmInstall() {
122
- if (process.env.TOTALRECLAW_SKIP_POSTINSTALL_RETRY === '1') {
123
- warn('TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1 — skipping retry');
124
- return false;
125
- }
126
- try {
127
- log('first-attempt smoke check failed — clearing node_modules and retrying npm install once...');
128
- const nm = path.join(here, 'node_modules');
129
- if (fs.existsSync(nm)) {
130
- fs.rmSync(nm, { recursive: true, force: true });
131
- }
132
- // Note: we deliberately re-invoke npm install here. The `--ignore-scripts`
133
- // flag is critical — without it we'd re-trigger this same postinstall
134
- // and recurse forever.
135
- execSync('npm install --no-audit --no-fund --ignore-scripts', {
136
- cwd: here,
137
- stdio: 'inherit',
138
- });
139
- log('retry npm install completed; re-validating deps');
140
- return true;
141
- } catch (err) {
142
- warn(`retry npm install failed: ${err.message}`);
143
- return false;
144
- }
145
- }
146
-
147
- function validateDepsOrFail() {
148
- const firstMiss = smokeCheckDeps();
149
- if (firstMiss.length === 0) {
150
- log(`smoke check OK (${CRITICAL_DEPS.length} critical deps resolved)`);
151
- return;
152
- }
153
-
154
- warn(`smoke check failed on first attempt:`);
155
- for (const m of firstMiss) {
156
- warn(` - ${m.dep}: ${m.message}`);
157
- }
158
-
159
- const retried = retryNpmInstall();
160
- if (!retried) {
161
- process.exitCode = 1;
162
- throw new Error(
163
- `TotalReclaw postinstall: critical deps missing after npm install — ` +
164
- `[${firstMiss.map((m) => m.dep).join(', ')}]. ` +
165
- `Re-run \`openclaw plugins install @totalreclaw/totalreclaw\` to retry, ` +
166
- `or set TOTALRECLAW_SKIP_POSTINSTALL_RETRY=1 to bypass and surface the ` +
167
- `original error.`,
168
- );
169
- }
170
-
171
- const secondMiss = smokeCheckDeps();
172
- if (secondMiss.length === 0) {
173
- log(`smoke check OK after retry (${CRITICAL_DEPS.length} deps resolved)`);
174
- return;
175
- }
176
-
177
- process.exitCode = 1;
178
- throw new Error(
179
- `TotalReclaw postinstall: deps still missing after retry — ` +
180
- `[${secondMiss.map((m) => m.dep).join(', ')}]. ` +
181
- `This is likely a permanent breakage (registry outage, package rename, ` +
182
- `or corrupted node_modules). The plugin will not load. Original errors:\n` +
183
- secondMiss.map((m) => ` - ${m.dep}: ${m.message}`).join('\n'),
184
- );
185
- }
186
-
187
- // ---------------------------------------------------------------------------
188
- // Step 3 — sweep `.openclaw-install-stage-*` siblings (issue #190)
189
- // ---------------------------------------------------------------------------
190
-
191
- /**
192
- * Resolve the OpenClaw extensions dir from the plugin's own location.
193
- * The plugin lives at `<extensions>/totalreclaw/` so the parent is the
194
- * extensions root. Returns null if the layout is not what we expect
195
- * (npm tarball linked outside an `<extensions>/` parent — e.g. dev
196
- * checkout) so we never delete random siblings.
197
- */
198
- function resolveExtensionsDir() {
199
- // `here` is the plugin root (this file is at the package root, NOT in dist/).
200
- // The parent should be the OpenClaw extensions directory.
201
- const parent = path.resolve(here, '..');
202
- // Heuristic check: only sweep if we look like we're inside an OpenClaw
203
- // install dir. We accept (a) the well-known `extensions` dirname, OR
204
- // (b) the presence of any sibling `.openclaw-install-stage-*` (which is
205
- // proof we're inside an extensions dir).
206
- if (path.basename(parent) === 'extensions') return parent;
207
- try {
208
- const entries = fs.readdirSync(parent);
209
- if (entries.some((n) => n.startsWith('.openclaw-install-stage-'))) {
210
- return parent;
211
- }
212
- } catch {
213
- // Parent unreadable — bail safely.
214
- }
215
- return null;
216
- }
217
-
218
- function sweepStagingSiblings() {
219
- const extensionsDir = resolveExtensionsDir();
220
- if (!extensionsDir) {
221
- log('no extensions parent detected (dev checkout?) — skipping staging sweep');
222
- return;
223
- }
224
- let removed = 0;
225
- let entries;
226
- try {
227
- entries = fs.readdirSync(extensionsDir);
228
- } catch (err) {
229
- warn(`could not list ${extensionsDir}: ${err.message}`);
230
- return;
231
- }
232
- for (const name of entries) {
233
- if (!name.startsWith('.openclaw-install-stage-')) continue;
234
- const target = path.join(extensionsDir, name);
235
- try {
236
- const st = fs.lstatSync(target);
237
- if (!st.isDirectory()) continue;
238
- fs.rmSync(target, { recursive: true, force: true });
239
- removed++;
240
- log(`removed stale staging dir: ${name}`);
241
- } catch (err) {
242
- warn(`could not remove ${name}: ${err.message}`);
243
- }
244
- }
245
- if (removed === 0) {
246
- log('no stale staging dirs to sweep');
247
- } else {
248
- log(`swept ${removed} stale staging dir(s)`);
249
- }
250
- }
251
-
252
- // ---------------------------------------------------------------------------
253
- // Main
254
- // ---------------------------------------------------------------------------
255
-
256
- clearPartialInstallMarker();
257
- sweepStagingSiblings();
258
- validateDepsOrFail();
259
-
260
- log('postinstall complete');