@openagents-org/agent-launcher 0.2.142 → 0.2.145
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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +108 -7
- package/src/adapters/claude.js +50 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +10 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +665 -79
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +29 -7
- package/src/config.js +8 -2
- package/src/daemon.js +108 -6
- package/src/installer.js +509 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install / verify the native NanoClaw `openagents` channel.
|
|
3
|
+
*
|
|
4
|
+
* The channel source ships with this package (nanoclaw-channel/openagents.ts).
|
|
5
|
+
* Installing it means: copy it to `<home>/src/channels/openagents.ts` and add a
|
|
6
|
+
* marker-delimited `import './openagents.js';` block to `src/channels/index.ts`
|
|
7
|
+
* so it self-registers on host startup — the same thing NanoClaw's own
|
|
8
|
+
* `/add-<channel>` skills do. We never touch the database.
|
|
9
|
+
*
|
|
10
|
+
* This is an EXPLICIT, opt-in action (not run automatically on every adapter
|
|
11
|
+
* start) because it modifies the user's NanoClaw checkout and needs a host
|
|
12
|
+
* restart. It is COMPATIBILITY-GATED and ROLLBACK-SAFE:
|
|
13
|
+
* - refuses unknown / incompatible NanoClaw versions (no modification);
|
|
14
|
+
* - verifies the expected dirs + the `ChannelAdapter` interface + the barrel
|
|
15
|
+
* before touching anything;
|
|
16
|
+
* - writes atomically and restores the barrel on any failure;
|
|
17
|
+
* - never overwrites a user's own `openagents.ts` (only files we own);
|
|
18
|
+
* - edits the barrel via a precise marker block, never fuzzy text replace;
|
|
19
|
+
* - validates the barrel after editing.
|
|
20
|
+
*
|
|
21
|
+
* If NanoClaw later exposes a formal channel-install entry point, prefer it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { execFileSync } = require('child_process');
|
|
30
|
+
|
|
31
|
+
const { looksLikeNanoclaw } = require('./nanoclaw-control');
|
|
32
|
+
|
|
33
|
+
// The EXACT NanoClaw revision this channel was verified against. Auto-install is
|
|
34
|
+
// gated on this precise commit (NOT a semver range): a different commit — even
|
|
35
|
+
// with the same version number — is treated as unverified.
|
|
36
|
+
const VERIFIED = {
|
|
37
|
+
remote: 'https://github.com/nanocoai/nanoclaw',
|
|
38
|
+
version: '2.1.19',
|
|
39
|
+
commit: '625264ba4b9de0a466d10debb267ca9ad688f4c0',
|
|
40
|
+
// The verified commit is the cloned `main` HEAD reporting version 2.1.19.
|
|
41
|
+
// It is NOT confirmed to be a tagged release.
|
|
42
|
+
tag: null,
|
|
43
|
+
// Fingerprint of the ChannelAdapter method signatures in adapter.ts at the
|
|
44
|
+
// verified commit. Extra protection / basis for a future tag+fingerprint
|
|
45
|
+
// allowlist; the auto-install gate remains the exact commit.
|
|
46
|
+
interfaceFingerprint: '6bde65465812d49d',
|
|
47
|
+
};
|
|
48
|
+
// Commits we will auto-install into without an explicit force. Currently just
|
|
49
|
+
// the verified commit; a future release can add {commit, fingerprint} entries.
|
|
50
|
+
const AUTO_INSTALL_COMMITS = new Set([VERIFIED.commit]);
|
|
51
|
+
|
|
52
|
+
// A line present in the shipped channel header — used to recognise a file we own.
|
|
53
|
+
const OURS_MARKER = 'OPENAGENTS-CHANNEL v1';
|
|
54
|
+
const BARREL_BEGIN = '// >>> openagents channel (OpenAgents Workspace bridge) — managed, do not edit';
|
|
55
|
+
const BARREL_IMPORT = "import './openagents.js';";
|
|
56
|
+
const BARREL_END = '// <<< openagents channel';
|
|
57
|
+
const BARREL_BLOCK = `${BARREL_BEGIN}\n${BARREL_IMPORT}\n${BARREL_END}\n`;
|
|
58
|
+
|
|
59
|
+
function channelSourcePath() {
|
|
60
|
+
return path.join(__dirname, '..', '..', 'nanoclaw-channel', 'openagents.ts');
|
|
61
|
+
}
|
|
62
|
+
function channelDestPath(home) {
|
|
63
|
+
return path.join(home, 'src', 'channels', 'openagents.ts');
|
|
64
|
+
}
|
|
65
|
+
function barrelPath(home) {
|
|
66
|
+
return path.join(home, 'src', 'channels', 'index.ts');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readFileSafe(p) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.readFileSync(p, 'utf-8');
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getNanoclawVersion(home) {
|
|
78
|
+
const raw = readFileSafe(path.join(home, 'package.json'));
|
|
79
|
+
if (!raw) return null;
|
|
80
|
+
try {
|
|
81
|
+
const v = JSON.parse(raw).version;
|
|
82
|
+
return typeof v === 'string' ? v : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Resolve the checkout's git commit SHA (null if not a git repo / unknown). */
|
|
89
|
+
function getNanoclawCommit(home, opts = {}) {
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'commitOverride')) {
|
|
91
|
+
const c = opts.commitOverride;
|
|
92
|
+
return c && /^[0-9a-f]{40}$/.test(c) ? c : null;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const out = execFileSync('git', ['-C', home, 'rev-parse', 'HEAD'], {
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
99
|
+
});
|
|
100
|
+
const sha = String(out || '').trim();
|
|
101
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Fingerprint the ChannelAdapter method signatures in adapter.ts. */
|
|
108
|
+
function computeInterfaceFingerprint(adapterTs) {
|
|
109
|
+
if (adapterTs == null) return null;
|
|
110
|
+
const sig = (
|
|
111
|
+
adapterTs.match(/(onInbound|onMetadata|onInboundEvent|onAction|deliver|setup|teardown|isConnected|setTyping)\s*\(/g) ||
|
|
112
|
+
[]
|
|
113
|
+
)
|
|
114
|
+
.sort()
|
|
115
|
+
.join('|');
|
|
116
|
+
return crypto.createHash('sha256').update(sig).digest('hex').slice(0, 16);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Structural / interface pre-flight — a HARD gate that even `force` cannot
|
|
121
|
+
* bypass (installing into a checkout with a moved channel API would produce a
|
|
122
|
+
* broken host). Returns {ok, reason, fingerprint}.
|
|
123
|
+
*/
|
|
124
|
+
function checkStructure(home) {
|
|
125
|
+
const adapterTs = readFileSafe(path.join(home, 'src', 'channels', 'adapter.ts'));
|
|
126
|
+
const registryTs = readFileSafe(path.join(home, 'src', 'channels', 'channel-registry.ts'));
|
|
127
|
+
const configTs = readFileSafe(path.join(home, 'src', 'config.ts'));
|
|
128
|
+
const barrel = readFileSafe(barrelPath(home));
|
|
129
|
+
const missing = [];
|
|
130
|
+
if (adapterTs == null) missing.push('src/channels/adapter.ts');
|
|
131
|
+
if (registryTs == null) missing.push('src/channels/channel-registry.ts');
|
|
132
|
+
if (configTs == null) missing.push('src/config.ts');
|
|
133
|
+
if (barrel == null) missing.push('src/channels/index.ts');
|
|
134
|
+
if (missing.length) return { ok: false, reason: `missing expected files: ${missing.join(', ')}`, fingerprint: null };
|
|
135
|
+
|
|
136
|
+
const fingerprint = computeInterfaceFingerprint(adapterTs);
|
|
137
|
+
if (!(/interface\s+ChannelAdapter/.test(adapterTs) && /onInbound\s*\(/.test(adapterTs) && /deliver\s*\(/.test(adapterTs))) {
|
|
138
|
+
return { ok: false, reason: 'ChannelAdapter interface shape not recognised', fingerprint };
|
|
139
|
+
}
|
|
140
|
+
if (!/registerChannelAdapter/.test(registryTs)) return { ok: false, reason: 'registerChannelAdapter not found', fingerprint };
|
|
141
|
+
if (!/DATA_DIR/.test(configTs)) return { ok: false, reason: 'DATA_DIR export not found in config.ts', fingerprint };
|
|
142
|
+
if (!/import\s+['"]\.\//.test(barrel)) return { ok: false, reason: 'index.ts does not look like the channel barrel', fingerprint };
|
|
143
|
+
return { ok: true, reason: null, fingerprint };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pre-flight compatibility. Auto-install (`ok:true`, code `verified`) requires
|
|
148
|
+
* BOTH a sound structure AND the EXACT verified commit. A different commit —
|
|
149
|
+
* even with the same version number — is `commit-mismatch`; an undeterminable
|
|
150
|
+
* commit is `unknown`; a moved interface is `incompatible`. None of these
|
|
151
|
+
* auto-install; the checkout is left untouched.
|
|
152
|
+
* @param {string} home
|
|
153
|
+
* @param {{versionOverride?:string, commitOverride?:string|null}} [opts]
|
|
154
|
+
*/
|
|
155
|
+
function checkCompatibility(home, opts = {}) {
|
|
156
|
+
if (!home || !looksLikeNanoclaw(home)) {
|
|
157
|
+
return { ok: false, code: 'unknown', version: null, commit: null, structureOk: false, reason: `not a NanoClaw checkout: ${home}` };
|
|
158
|
+
}
|
|
159
|
+
const version = opts.versionOverride || getNanoclawVersion(home);
|
|
160
|
+
const structure = checkStructure(home);
|
|
161
|
+
const commit = getNanoclawCommit(home, opts);
|
|
162
|
+
const fingerprintMatch = structure.fingerprint === VERIFIED.interfaceFingerprint;
|
|
163
|
+
const base = { version, commit, structureOk: structure.ok, fingerprint: structure.fingerprint, fingerprintMatch };
|
|
164
|
+
|
|
165
|
+
if (!structure.ok) return { ok: false, code: 'incompatible', reason: structure.reason, ...base };
|
|
166
|
+
if (commit && AUTO_INSTALL_COMMITS.has(commit)) {
|
|
167
|
+
return { ok: true, code: 'verified', reason: null, ...base };
|
|
168
|
+
}
|
|
169
|
+
if (!commit) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
code: 'unknown',
|
|
173
|
+
reason: `could not determine the NanoClaw commit (not a git checkout?); auto-install requires the verified commit ${VERIFIED.commit}`,
|
|
174
|
+
...base,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
code: 'commit-mismatch',
|
|
180
|
+
reason: `NanoClaw commit ${commit} is not the verified commit ${VERIFIED.commit}${fingerprintMatch ? ' (interface fingerprint matches, but auto-install still requires the exact commit)' : ''}`,
|
|
181
|
+
...base,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Is the channel both present (a file we own) and wired into the barrel? */
|
|
186
|
+
function isChannelInstalled(home) {
|
|
187
|
+
const dest = readFileSafe(channelDestPath(home));
|
|
188
|
+
if (dest == null) return false;
|
|
189
|
+
const barrel = readFileSafe(barrelPath(home));
|
|
190
|
+
return !!barrel && barrel.includes(BARREL_IMPORT);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Does a dest file exist that we did NOT write (a user's own channel)? */
|
|
194
|
+
function _destIsForeign(home) {
|
|
195
|
+
const dest = readFileSafe(channelDestPath(home));
|
|
196
|
+
if (dest == null) return false; // no file → not foreign
|
|
197
|
+
return !dest.includes(OURS_MARKER);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A NanoClaw channels barrel is a list of `import './x.js';` lines (+ comments). */
|
|
201
|
+
function _validateBarrel(content) {
|
|
202
|
+
for (const rawLine of content.split('\n')) {
|
|
203
|
+
const line = rawLine.trim();
|
|
204
|
+
if (!line || line.startsWith('//')) continue;
|
|
205
|
+
if (!/^import\s+['"][^'"]+['"];?$/.test(line)) {
|
|
206
|
+
return { ok: false, reason: `unexpected barrel line: ${line.slice(0, 60)}` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { ok: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Copy the channel and add the barrel import. Compatibility-gated, idempotent,
|
|
214
|
+
* rollback-safe.
|
|
215
|
+
* @param {string} home
|
|
216
|
+
* @param {{force?:boolean, versionOverride?:string}} [opts]
|
|
217
|
+
* @returns {{ok:boolean, code?:string, changed:boolean, needsRestart:boolean, error?:string, dest?:string, version?:string}}
|
|
218
|
+
*/
|
|
219
|
+
function installChannel(home, opts = {}) {
|
|
220
|
+
const compat = checkCompatibility(home, opts);
|
|
221
|
+
const forced = opts.force === true; // OFF by default; admin/CLI only, never a Workspace user
|
|
222
|
+
|
|
223
|
+
// Structure is a HARD gate — never bypassable, even with force (installing
|
|
224
|
+
// into a checkout with a moved channel API would just break the host).
|
|
225
|
+
if (!compat.structureOk) {
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
code: compat.code || 'incompatible',
|
|
229
|
+
changed: false,
|
|
230
|
+
needsRestart: false,
|
|
231
|
+
error: compat.reason,
|
|
232
|
+
version: compat.version,
|
|
233
|
+
commit: compat.commit,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// Commit gate: auto-install only into the verified commit, unless forced.
|
|
237
|
+
if (!compat.ok && !forced) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
code: compat.code,
|
|
241
|
+
changed: false,
|
|
242
|
+
needsRestart: false,
|
|
243
|
+
error: compat.reason,
|
|
244
|
+
version: compat.version,
|
|
245
|
+
commit: compat.commit,
|
|
246
|
+
hint: 'unverified NanoClaw commit — pass force:true (admin/CLI only) to override',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!compat.ok && forced) {
|
|
250
|
+
// Redacted diagnostic — no secrets, no full paths, commit truncated.
|
|
251
|
+
const diag = {
|
|
252
|
+
code: compat.code,
|
|
253
|
+
version: compat.version,
|
|
254
|
+
commit: compat.commit ? compat.commit.slice(0, 12) : null,
|
|
255
|
+
fingerprintMatch: compat.fingerprintMatch,
|
|
256
|
+
};
|
|
257
|
+
(typeof opts.logger === 'function' ? opts.logger : (m, d) => console.warn(m, d))(
|
|
258
|
+
'NanoClaw channel: FORCED install into an unverified commit',
|
|
259
|
+
diag,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const src = channelSourcePath();
|
|
264
|
+
if (!fs.existsSync(src)) {
|
|
265
|
+
return { ok: false, code: 'unknown', changed: false, needsRestart: false, error: `channel source missing: ${src}` };
|
|
266
|
+
}
|
|
267
|
+
if (!opts.force && _destIsForeign(home)) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
code: 'foreign-file',
|
|
271
|
+
changed: false,
|
|
272
|
+
needsRestart: false,
|
|
273
|
+
error: 'refusing to overwrite an existing src/channels/openagents.ts not written by OpenAgents (use force to override)',
|
|
274
|
+
dest: channelDestPath(home),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const dest = channelDestPath(home);
|
|
279
|
+
let changed = false;
|
|
280
|
+
|
|
281
|
+
// 1) Copy the channel file atomically (temp + rename); skip if identical.
|
|
282
|
+
let needWrite = opts.force || !fs.existsSync(dest);
|
|
283
|
+
if (!needWrite) needWrite = readFileSafe(dest) !== readFileSafe(src);
|
|
284
|
+
if (needWrite) {
|
|
285
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
286
|
+
try {
|
|
287
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
288
|
+
fs.copyFileSync(src, tmp);
|
|
289
|
+
fs.renameSync(tmp, dest);
|
|
290
|
+
changed = true;
|
|
291
|
+
} catch (e) {
|
|
292
|
+
try {
|
|
293
|
+
fs.unlinkSync(tmp);
|
|
294
|
+
} catch {
|
|
295
|
+
/* best-effort */
|
|
296
|
+
}
|
|
297
|
+
return { ok: false, code: compat.code, changed, needsRestart: false, error: `copy failed: ${e.message}`, dest };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 2) Add the barrel import block (marker-delimited; backup + restore on error).
|
|
302
|
+
const barrelFile = barrelPath(home);
|
|
303
|
+
const original = readFileSafe(barrelFile);
|
|
304
|
+
if (original == null) {
|
|
305
|
+
return { ok: false, code: compat.code, changed, needsRestart: changed, error: 'barrel disappeared', dest };
|
|
306
|
+
}
|
|
307
|
+
if (!original.includes(BARREL_IMPORT)) {
|
|
308
|
+
const sep = original.length && !original.endsWith('\n') ? '\n' : '';
|
|
309
|
+
const updated = `${original}${sep}${BARREL_BLOCK}`;
|
|
310
|
+
const check = _validateBarrel(updated);
|
|
311
|
+
if (!check.ok) {
|
|
312
|
+
return { ok: false, code: compat.code, changed, needsRestart: changed, error: `barrel validation failed: ${check.reason}`, dest };
|
|
313
|
+
}
|
|
314
|
+
const tmp = `${barrelFile}.tmp-${process.pid}`;
|
|
315
|
+
try {
|
|
316
|
+
fs.writeFileSync(tmp, updated);
|
|
317
|
+
fs.renameSync(tmp, barrelFile);
|
|
318
|
+
// Post-edit re-read validation: our import present + original preserved.
|
|
319
|
+
const after = readFileSafe(barrelFile);
|
|
320
|
+
if (!after || !after.includes(BARREL_IMPORT) || !after.includes(original.trim().split('\n')[0])) {
|
|
321
|
+
fs.writeFileSync(barrelFile, original); // restore
|
|
322
|
+
return { ok: false, code: compat.code, changed, needsRestart: changed, error: 'post-edit barrel validation failed; restored', dest };
|
|
323
|
+
}
|
|
324
|
+
changed = true;
|
|
325
|
+
} catch (e) {
|
|
326
|
+
try {
|
|
327
|
+
fs.unlinkSync(tmp);
|
|
328
|
+
} catch {
|
|
329
|
+
/* best-effort */
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
fs.writeFileSync(barrelFile, original); // restore
|
|
333
|
+
} catch {
|
|
334
|
+
/* best-effort */
|
|
335
|
+
}
|
|
336
|
+
return { ok: false, code: compat.code, changed, needsRestart: changed, error: `barrel patch failed (restored): ${e.message}`, dest };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
code: forced && !compat.ok ? 'forced' : compat.code,
|
|
343
|
+
changed,
|
|
344
|
+
needsRestart: changed,
|
|
345
|
+
dest,
|
|
346
|
+
version: compat.version,
|
|
347
|
+
commit: compat.commit,
|
|
348
|
+
forced: forced && !compat.ok,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Remove ONLY what we injected: our channel file (if ours) + our barrel block. */
|
|
353
|
+
function uninstallChannel(home) {
|
|
354
|
+
let changed = false;
|
|
355
|
+
const dest = channelDestPath(home);
|
|
356
|
+
const destContent = readFileSafe(dest);
|
|
357
|
+
if (destContent != null && destContent.includes(OURS_MARKER)) {
|
|
358
|
+
try {
|
|
359
|
+
fs.unlinkSync(dest);
|
|
360
|
+
changed = true;
|
|
361
|
+
} catch {
|
|
362
|
+
/* best-effort */
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const barrelFile = barrelPath(home);
|
|
366
|
+
const barrel = readFileSafe(barrelFile);
|
|
367
|
+
if (barrel != null && barrel.includes(BARREL_IMPORT)) {
|
|
368
|
+
// Remove our marker block precisely; fall back to removing just our import line.
|
|
369
|
+
let updated = barrel.replace(
|
|
370
|
+
new RegExp(`${_escapeRe(BARREL_BEGIN)}\\n${_escapeRe(BARREL_IMPORT)}\\n${_escapeRe(BARREL_END)}\\n?`, 'g'),
|
|
371
|
+
'',
|
|
372
|
+
);
|
|
373
|
+
if (updated === barrel) {
|
|
374
|
+
updated = barrel
|
|
375
|
+
.split('\n')
|
|
376
|
+
.filter((l) => l.trim() !== BARREL_IMPORT && l.trim() !== BARREL_BEGIN && l.trim() !== BARREL_END)
|
|
377
|
+
.join('\n');
|
|
378
|
+
}
|
|
379
|
+
if (updated !== barrel) {
|
|
380
|
+
try {
|
|
381
|
+
fs.writeFileSync(barrelFile, updated);
|
|
382
|
+
changed = true;
|
|
383
|
+
} catch {
|
|
384
|
+
/* best-effort */
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { ok: true, changed };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function _escapeRe(s) {
|
|
392
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
VERIFIED,
|
|
397
|
+
AUTO_INSTALL_COMMITS,
|
|
398
|
+
OURS_MARKER,
|
|
399
|
+
channelSourcePath,
|
|
400
|
+
channelDestPath,
|
|
401
|
+
barrelPath,
|
|
402
|
+
getNanoclawVersion,
|
|
403
|
+
getNanoclawCommit,
|
|
404
|
+
computeInterfaceFingerprint,
|
|
405
|
+
checkStructure,
|
|
406
|
+
checkCompatibility,
|
|
407
|
+
isChannelInstalled,
|
|
408
|
+
installChannel,
|
|
409
|
+
uninstallChannel,
|
|
410
|
+
BARREL_IMPORT,
|
|
411
|
+
};
|