@karmaniverous/jeeves-watcher-openclaw 0.8.1 → 0.9.0
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/dist/cli.js +1518 -370
- package/dist/index.js +1664 -233
- package/dist/src/cli.d.ts +3 -2
- package/dist/src/helpers.d.ts +10 -49
- package/dist/src/index.d.ts +1 -1
- package/dist/src/watcherTools.d.ts +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/dist/src/cli.test.d.ts +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,1223 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync, mkdirSync, cpSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import 'vm';
|
|
6
|
+
import require$$0$3 from 'path';
|
|
7
|
+
import require$$0$2 from 'fs';
|
|
6
8
|
import require$$0 from 'constants';
|
|
7
9
|
import require$$0$1 from 'stream';
|
|
8
10
|
import require$$4 from 'util';
|
|
9
11
|
import require$$5 from 'assert';
|
|
10
12
|
import require$$2 from 'events';
|
|
11
|
-
import 'node:url';
|
|
12
13
|
import 'node:child_process';
|
|
13
|
-
import { homedir } from 'os';
|
|
14
|
-
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @implements {IHooks}
|
|
18
|
+
*/
|
|
19
|
+
class Hooks {
|
|
20
|
+
/**
|
|
21
|
+
* @callback HookCallback
|
|
22
|
+
* @this {*|Jsep} this
|
|
23
|
+
* @param {Jsep} env
|
|
24
|
+
* @returns: void
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Adds the given callback to the list of callbacks for the given hook.
|
|
28
|
+
*
|
|
29
|
+
* The callback will be invoked when the hook it is registered for is run.
|
|
30
|
+
*
|
|
31
|
+
* One callback function can be registered to multiple hooks and the same hook multiple times.
|
|
32
|
+
*
|
|
33
|
+
* @param {string|object} name The name of the hook, or an object of callbacks keyed by name
|
|
34
|
+
* @param {HookCallback|boolean} callback The callback function which is given environment variables.
|
|
35
|
+
* @param {?boolean} [first=false] Will add the hook to the top of the list (defaults to the bottom)
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
add(name, callback, first) {
|
|
39
|
+
if (typeof arguments[0] != 'string') {
|
|
40
|
+
// Multiple hook callbacks, keyed by name
|
|
41
|
+
for (let name in arguments[0]) {
|
|
42
|
+
this.add(name, arguments[0][name], arguments[1]);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
(Array.isArray(name) ? name : [name]).forEach(function (name) {
|
|
46
|
+
this[name] = this[name] || [];
|
|
47
|
+
if (callback) {
|
|
48
|
+
this[name][first ? 'unshift' : 'push'](callback);
|
|
49
|
+
}
|
|
50
|
+
}, this);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Runs a hook invoking all registered callbacks with the given environment variables.
|
|
56
|
+
*
|
|
57
|
+
* Callbacks will be invoked synchronously and in the order in which they were registered.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} name The name of the hook.
|
|
60
|
+
* @param {Object<string, any>} env The environment variables of the hook passed to all callbacks registered.
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
run(name, env) {
|
|
64
|
+
this[name] = this[name] || [];
|
|
65
|
+
this[name].forEach(function (callback) {
|
|
66
|
+
callback.call(env && env.context ? env.context : env, env);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @implements {IPlugins}
|
|
73
|
+
*/
|
|
74
|
+
class Plugins {
|
|
75
|
+
constructor(jsep) {
|
|
76
|
+
this.jsep = jsep;
|
|
77
|
+
this.registered = {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @callback PluginSetup
|
|
82
|
+
* @this {Jsep} jsep
|
|
83
|
+
* @returns: void
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* Adds the given plugin(s) to the registry
|
|
87
|
+
*
|
|
88
|
+
* @param {object} plugins
|
|
89
|
+
* @param {string} plugins.name The name of the plugin
|
|
90
|
+
* @param {PluginSetup} plugins.init The init function
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
register(...plugins) {
|
|
94
|
+
plugins.forEach(plugin => {
|
|
95
|
+
if (typeof plugin !== 'object' || !plugin.name || !plugin.init) {
|
|
96
|
+
throw new Error('Invalid JSEP plugin format');
|
|
97
|
+
}
|
|
98
|
+
if (this.registered[plugin.name]) {
|
|
99
|
+
// already registered. Ignore.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
plugin.init(this.jsep);
|
|
103
|
+
this.registered[plugin.name] = plugin;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// JavaScript Expression Parser (JSEP) 1.4.0
|
|
109
|
+
|
|
110
|
+
class Jsep {
|
|
111
|
+
/**
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
static get version() {
|
|
115
|
+
// To be filled in by the template
|
|
116
|
+
return '1.4.0';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
static toString() {
|
|
123
|
+
return 'JavaScript Expression Parser (JSEP) v' + Jsep.version;
|
|
124
|
+
}
|
|
125
|
+
// ==================== CONFIG ================================
|
|
126
|
+
/**
|
|
127
|
+
* @method addUnaryOp
|
|
128
|
+
* @param {string} op_name The name of the unary op to add
|
|
129
|
+
* @returns {Jsep}
|
|
130
|
+
*/
|
|
131
|
+
static addUnaryOp(op_name) {
|
|
132
|
+
Jsep.max_unop_len = Math.max(op_name.length, Jsep.max_unop_len);
|
|
133
|
+
Jsep.unary_ops[op_name] = 1;
|
|
134
|
+
return Jsep;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @method jsep.addBinaryOp
|
|
139
|
+
* @param {string} op_name The name of the binary op to add
|
|
140
|
+
* @param {number} precedence The precedence of the binary op (can be a float). Higher number = higher precedence
|
|
141
|
+
* @param {boolean} [isRightAssociative=false] whether operator is right-associative
|
|
142
|
+
* @returns {Jsep}
|
|
143
|
+
*/
|
|
144
|
+
static addBinaryOp(op_name, precedence, isRightAssociative) {
|
|
145
|
+
Jsep.max_binop_len = Math.max(op_name.length, Jsep.max_binop_len);
|
|
146
|
+
Jsep.binary_ops[op_name] = precedence;
|
|
147
|
+
if (isRightAssociative) {
|
|
148
|
+
Jsep.right_associative.add(op_name);
|
|
149
|
+
} else {
|
|
150
|
+
Jsep.right_associative.delete(op_name);
|
|
151
|
+
}
|
|
152
|
+
return Jsep;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @method addIdentifierChar
|
|
157
|
+
* @param {string} char The additional character to treat as a valid part of an identifier
|
|
158
|
+
* @returns {Jsep}
|
|
159
|
+
*/
|
|
160
|
+
static addIdentifierChar(char) {
|
|
161
|
+
Jsep.additional_identifier_chars.add(char);
|
|
162
|
+
return Jsep;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @method addLiteral
|
|
167
|
+
* @param {string} literal_name The name of the literal to add
|
|
168
|
+
* @param {*} literal_value The value of the literal
|
|
169
|
+
* @returns {Jsep}
|
|
170
|
+
*/
|
|
171
|
+
static addLiteral(literal_name, literal_value) {
|
|
172
|
+
Jsep.literals[literal_name] = literal_value;
|
|
173
|
+
return Jsep;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @method removeUnaryOp
|
|
178
|
+
* @param {string} op_name The name of the unary op to remove
|
|
179
|
+
* @returns {Jsep}
|
|
180
|
+
*/
|
|
181
|
+
static removeUnaryOp(op_name) {
|
|
182
|
+
delete Jsep.unary_ops[op_name];
|
|
183
|
+
if (op_name.length === Jsep.max_unop_len) {
|
|
184
|
+
Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
|
|
185
|
+
}
|
|
186
|
+
return Jsep;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @method removeAllUnaryOps
|
|
191
|
+
* @returns {Jsep}
|
|
192
|
+
*/
|
|
193
|
+
static removeAllUnaryOps() {
|
|
194
|
+
Jsep.unary_ops = {};
|
|
195
|
+
Jsep.max_unop_len = 0;
|
|
196
|
+
return Jsep;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @method removeIdentifierChar
|
|
201
|
+
* @param {string} char The additional character to stop treating as a valid part of an identifier
|
|
202
|
+
* @returns {Jsep}
|
|
203
|
+
*/
|
|
204
|
+
static removeIdentifierChar(char) {
|
|
205
|
+
Jsep.additional_identifier_chars.delete(char);
|
|
206
|
+
return Jsep;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @method removeBinaryOp
|
|
211
|
+
* @param {string} op_name The name of the binary op to remove
|
|
212
|
+
* @returns {Jsep}
|
|
213
|
+
*/
|
|
214
|
+
static removeBinaryOp(op_name) {
|
|
215
|
+
delete Jsep.binary_ops[op_name];
|
|
216
|
+
if (op_name.length === Jsep.max_binop_len) {
|
|
217
|
+
Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
|
|
218
|
+
}
|
|
219
|
+
Jsep.right_associative.delete(op_name);
|
|
220
|
+
return Jsep;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @method removeAllBinaryOps
|
|
225
|
+
* @returns {Jsep}
|
|
226
|
+
*/
|
|
227
|
+
static removeAllBinaryOps() {
|
|
228
|
+
Jsep.binary_ops = {};
|
|
229
|
+
Jsep.max_binop_len = 0;
|
|
230
|
+
return Jsep;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @method removeLiteral
|
|
235
|
+
* @param {string} literal_name The name of the literal to remove
|
|
236
|
+
* @returns {Jsep}
|
|
237
|
+
*/
|
|
238
|
+
static removeLiteral(literal_name) {
|
|
239
|
+
delete Jsep.literals[literal_name];
|
|
240
|
+
return Jsep;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @method removeAllLiterals
|
|
245
|
+
* @returns {Jsep}
|
|
246
|
+
*/
|
|
247
|
+
static removeAllLiterals() {
|
|
248
|
+
Jsep.literals = {};
|
|
249
|
+
return Jsep;
|
|
250
|
+
}
|
|
251
|
+
// ==================== END CONFIG ============================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
get char() {
|
|
257
|
+
return this.expr.charAt(this.index);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @returns {number}
|
|
262
|
+
*/
|
|
263
|
+
get code() {
|
|
264
|
+
return this.expr.charCodeAt(this.index);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @param {string} expr a string with the passed in express
|
|
268
|
+
* @returns Jsep
|
|
269
|
+
*/
|
|
270
|
+
constructor(expr) {
|
|
271
|
+
// `index` stores the character number we are currently at
|
|
272
|
+
// All of the gobbles below will modify `index` as we move along
|
|
273
|
+
this.expr = expr;
|
|
274
|
+
this.index = 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* static top-level parser
|
|
279
|
+
* @returns {jsep.Expression}
|
|
280
|
+
*/
|
|
281
|
+
static parse(expr) {
|
|
282
|
+
return new Jsep(expr).parse();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get the longest key length of any object
|
|
287
|
+
* @param {object} obj
|
|
288
|
+
* @returns {number}
|
|
289
|
+
*/
|
|
290
|
+
static getMaxKeyLen(obj) {
|
|
291
|
+
return Math.max(0, ...Object.keys(obj).map(k => k.length));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* `ch` is a character code in the next three functions
|
|
296
|
+
* @param {number} ch
|
|
297
|
+
* @returns {boolean}
|
|
298
|
+
*/
|
|
299
|
+
static isDecimalDigit(ch) {
|
|
300
|
+
return ch >= 48 && ch <= 57; // 0...9
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Returns the precedence of a binary operator or `0` if it isn't a binary operator. Can be float.
|
|
305
|
+
* @param {string} op_val
|
|
306
|
+
* @returns {number}
|
|
307
|
+
*/
|
|
308
|
+
static binaryPrecedence(op_val) {
|
|
309
|
+
return Jsep.binary_ops[op_val] || 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Looks for start of identifier
|
|
314
|
+
* @param {number} ch
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
static isIdentifierStart(ch) {
|
|
318
|
+
return ch >= 65 && ch <= 90 ||
|
|
319
|
+
// A...Z
|
|
320
|
+
ch >= 97 && ch <= 122 ||
|
|
321
|
+
// a...z
|
|
322
|
+
ch >= 128 && !Jsep.binary_ops[String.fromCharCode(ch)] ||
|
|
323
|
+
// any non-ASCII that is not an operator
|
|
324
|
+
Jsep.additional_identifier_chars.has(String.fromCharCode(ch)); // additional characters
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {number} ch
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
static isIdentifierPart(ch) {
|
|
332
|
+
return Jsep.isIdentifierStart(ch) || Jsep.isDecimalDigit(ch);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* throw error at index of the expression
|
|
337
|
+
* @param {string} message
|
|
338
|
+
* @throws
|
|
339
|
+
*/
|
|
340
|
+
throwError(message) {
|
|
341
|
+
const error = new Error(message + ' at character ' + this.index);
|
|
342
|
+
error.index = this.index;
|
|
343
|
+
error.description = message;
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Run a given hook
|
|
349
|
+
* @param {string} name
|
|
350
|
+
* @param {jsep.Expression|false} [node]
|
|
351
|
+
* @returns {?jsep.Expression}
|
|
352
|
+
*/
|
|
353
|
+
runHook(name, node) {
|
|
354
|
+
if (Jsep.hooks[name]) {
|
|
355
|
+
const env = {
|
|
356
|
+
context: this,
|
|
357
|
+
node
|
|
358
|
+
};
|
|
359
|
+
Jsep.hooks.run(name, env);
|
|
360
|
+
return env.node;
|
|
361
|
+
}
|
|
362
|
+
return node;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Runs a given hook until one returns a node
|
|
367
|
+
* @param {string} name
|
|
368
|
+
* @returns {?jsep.Expression}
|
|
369
|
+
*/
|
|
370
|
+
searchHook(name) {
|
|
371
|
+
if (Jsep.hooks[name]) {
|
|
372
|
+
const env = {
|
|
373
|
+
context: this
|
|
374
|
+
};
|
|
375
|
+
Jsep.hooks[name].find(function (callback) {
|
|
376
|
+
callback.call(env.context, env);
|
|
377
|
+
return env.node;
|
|
378
|
+
});
|
|
379
|
+
return env.node;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Push `index` up to the next non-space character
|
|
385
|
+
*/
|
|
386
|
+
gobbleSpaces() {
|
|
387
|
+
let ch = this.code;
|
|
388
|
+
// Whitespace
|
|
389
|
+
while (ch === Jsep.SPACE_CODE || ch === Jsep.TAB_CODE || ch === Jsep.LF_CODE || ch === Jsep.CR_CODE) {
|
|
390
|
+
ch = this.expr.charCodeAt(++this.index);
|
|
391
|
+
}
|
|
392
|
+
this.runHook('gobble-spaces');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Top-level method to parse all expressions and returns compound or single node
|
|
397
|
+
* @returns {jsep.Expression}
|
|
398
|
+
*/
|
|
399
|
+
parse() {
|
|
400
|
+
this.runHook('before-all');
|
|
401
|
+
const nodes = this.gobbleExpressions();
|
|
402
|
+
|
|
403
|
+
// If there's only one expression just try returning the expression
|
|
404
|
+
const node = nodes.length === 1 ? nodes[0] : {
|
|
405
|
+
type: Jsep.COMPOUND,
|
|
406
|
+
body: nodes
|
|
407
|
+
};
|
|
408
|
+
return this.runHook('after-all', node);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* top-level parser (but can be reused within as well)
|
|
413
|
+
* @param {number} [untilICode]
|
|
414
|
+
* @returns {jsep.Expression[]}
|
|
415
|
+
*/
|
|
416
|
+
gobbleExpressions(untilICode) {
|
|
417
|
+
let nodes = [],
|
|
418
|
+
ch_i,
|
|
419
|
+
node;
|
|
420
|
+
while (this.index < this.expr.length) {
|
|
421
|
+
ch_i = this.code;
|
|
422
|
+
|
|
423
|
+
// Expressions can be separated by semicolons, commas, or just inferred without any
|
|
424
|
+
// separators
|
|
425
|
+
if (ch_i === Jsep.SEMCOL_CODE || ch_i === Jsep.COMMA_CODE) {
|
|
426
|
+
this.index++; // ignore separators
|
|
427
|
+
} else {
|
|
428
|
+
// Try to gobble each expression individually
|
|
429
|
+
if (node = this.gobbleExpression()) {
|
|
430
|
+
nodes.push(node);
|
|
431
|
+
// If we weren't able to find a binary expression and are out of room, then
|
|
432
|
+
// the expression passed in probably has too much
|
|
433
|
+
} else if (this.index < this.expr.length) {
|
|
434
|
+
if (ch_i === untilICode) {
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
this.throwError('Unexpected "' + this.char + '"');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return nodes;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The main parsing function.
|
|
446
|
+
* @returns {?jsep.Expression}
|
|
447
|
+
*/
|
|
448
|
+
gobbleExpression() {
|
|
449
|
+
const node = this.searchHook('gobble-expression') || this.gobbleBinaryExpression();
|
|
450
|
+
this.gobbleSpaces();
|
|
451
|
+
return this.runHook('after-expression', node);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Search for the operation portion of the string (e.g. `+`, `===`)
|
|
456
|
+
* Start by taking the longest possible binary operations (3 characters: `===`, `!==`, `>>>`)
|
|
457
|
+
* and move down from 3 to 2 to 1 character until a matching binary operation is found
|
|
458
|
+
* then, return that binary operation
|
|
459
|
+
* @returns {string|boolean}
|
|
460
|
+
*/
|
|
461
|
+
gobbleBinaryOp() {
|
|
462
|
+
this.gobbleSpaces();
|
|
463
|
+
let to_check = this.expr.substr(this.index, Jsep.max_binop_len);
|
|
464
|
+
let tc_len = to_check.length;
|
|
465
|
+
while (tc_len > 0) {
|
|
466
|
+
// Don't accept a binary op when it is an identifier.
|
|
467
|
+
// Binary ops that start with a identifier-valid character must be followed
|
|
468
|
+
// by a non identifier-part valid character
|
|
469
|
+
if (Jsep.binary_ops.hasOwnProperty(to_check) && (!Jsep.isIdentifierStart(this.code) || this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))) {
|
|
470
|
+
this.index += tc_len;
|
|
471
|
+
return to_check;
|
|
472
|
+
}
|
|
473
|
+
to_check = to_check.substr(0, --tc_len);
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* This function is responsible for gobbling an individual expression,
|
|
480
|
+
* e.g. `1`, `1+2`, `a+(b*2)-Math.sqrt(2)`
|
|
481
|
+
* @returns {?jsep.BinaryExpression}
|
|
482
|
+
*/
|
|
483
|
+
gobbleBinaryExpression() {
|
|
484
|
+
let node, biop, prec, stack, biop_info, left, right, i, cur_biop;
|
|
485
|
+
|
|
486
|
+
// First, try to get the leftmost thing
|
|
487
|
+
// Then, check to see if there's a binary operator operating on that leftmost thing
|
|
488
|
+
// Don't gobbleBinaryOp without a left-hand-side
|
|
489
|
+
left = this.gobbleToken();
|
|
490
|
+
if (!left) {
|
|
491
|
+
return left;
|
|
492
|
+
}
|
|
493
|
+
biop = this.gobbleBinaryOp();
|
|
494
|
+
|
|
495
|
+
// If there wasn't a binary operator, just return the leftmost node
|
|
496
|
+
if (!biop) {
|
|
497
|
+
return left;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Otherwise, we need to start a stack to properly place the binary operations in their
|
|
501
|
+
// precedence structure
|
|
502
|
+
biop_info = {
|
|
503
|
+
value: biop,
|
|
504
|
+
prec: Jsep.binaryPrecedence(biop),
|
|
505
|
+
right_a: Jsep.right_associative.has(biop)
|
|
506
|
+
};
|
|
507
|
+
right = this.gobbleToken();
|
|
508
|
+
if (!right) {
|
|
509
|
+
this.throwError("Expected expression after " + biop);
|
|
510
|
+
}
|
|
511
|
+
stack = [left, biop_info, right];
|
|
512
|
+
|
|
513
|
+
// Properly deal with precedence using [recursive descent](http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm)
|
|
514
|
+
while (biop = this.gobbleBinaryOp()) {
|
|
515
|
+
prec = Jsep.binaryPrecedence(biop);
|
|
516
|
+
if (prec === 0) {
|
|
517
|
+
this.index -= biop.length;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
biop_info = {
|
|
521
|
+
value: biop,
|
|
522
|
+
prec,
|
|
523
|
+
right_a: Jsep.right_associative.has(biop)
|
|
524
|
+
};
|
|
525
|
+
cur_biop = biop;
|
|
526
|
+
|
|
527
|
+
// Reduce: make a binary expression from the three topmost entries.
|
|
528
|
+
const comparePrev = prev => biop_info.right_a && prev.right_a ? prec > prev.prec : prec <= prev.prec;
|
|
529
|
+
while (stack.length > 2 && comparePrev(stack[stack.length - 2])) {
|
|
530
|
+
right = stack.pop();
|
|
531
|
+
biop = stack.pop().value;
|
|
532
|
+
left = stack.pop();
|
|
533
|
+
node = {
|
|
534
|
+
type: Jsep.BINARY_EXP,
|
|
535
|
+
operator: biop,
|
|
536
|
+
left,
|
|
537
|
+
right
|
|
538
|
+
};
|
|
539
|
+
stack.push(node);
|
|
540
|
+
}
|
|
541
|
+
node = this.gobbleToken();
|
|
542
|
+
if (!node) {
|
|
543
|
+
this.throwError("Expected expression after " + cur_biop);
|
|
544
|
+
}
|
|
545
|
+
stack.push(biop_info, node);
|
|
546
|
+
}
|
|
547
|
+
i = stack.length - 1;
|
|
548
|
+
node = stack[i];
|
|
549
|
+
while (i > 1) {
|
|
550
|
+
node = {
|
|
551
|
+
type: Jsep.BINARY_EXP,
|
|
552
|
+
operator: stack[i - 1].value,
|
|
553
|
+
left: stack[i - 2],
|
|
554
|
+
right: node
|
|
555
|
+
};
|
|
556
|
+
i -= 2;
|
|
557
|
+
}
|
|
558
|
+
return node;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* An individual part of a binary expression:
|
|
563
|
+
* e.g. `foo.bar(baz)`, `1`, `"abc"`, `(a % 2)` (because it's in parenthesis)
|
|
564
|
+
* @returns {boolean|jsep.Expression}
|
|
565
|
+
*/
|
|
566
|
+
gobbleToken() {
|
|
567
|
+
let ch, to_check, tc_len, node;
|
|
568
|
+
this.gobbleSpaces();
|
|
569
|
+
node = this.searchHook('gobble-token');
|
|
570
|
+
if (node) {
|
|
571
|
+
return this.runHook('after-token', node);
|
|
572
|
+
}
|
|
573
|
+
ch = this.code;
|
|
574
|
+
if (Jsep.isDecimalDigit(ch) || ch === Jsep.PERIOD_CODE) {
|
|
575
|
+
// Char code 46 is a dot `.` which can start off a numeric literal
|
|
576
|
+
return this.gobbleNumericLiteral();
|
|
577
|
+
}
|
|
578
|
+
if (ch === Jsep.SQUOTE_CODE || ch === Jsep.DQUOTE_CODE) {
|
|
579
|
+
// Single or double quotes
|
|
580
|
+
node = this.gobbleStringLiteral();
|
|
581
|
+
} else if (ch === Jsep.OBRACK_CODE) {
|
|
582
|
+
node = this.gobbleArray();
|
|
583
|
+
} else {
|
|
584
|
+
to_check = this.expr.substr(this.index, Jsep.max_unop_len);
|
|
585
|
+
tc_len = to_check.length;
|
|
586
|
+
while (tc_len > 0) {
|
|
587
|
+
// Don't accept an unary op when it is an identifier.
|
|
588
|
+
// Unary ops that start with a identifier-valid character must be followed
|
|
589
|
+
// by a non identifier-part valid character
|
|
590
|
+
if (Jsep.unary_ops.hasOwnProperty(to_check) && (!Jsep.isIdentifierStart(this.code) || this.index + to_check.length < this.expr.length && !Jsep.isIdentifierPart(this.expr.charCodeAt(this.index + to_check.length)))) {
|
|
591
|
+
this.index += tc_len;
|
|
592
|
+
const argument = this.gobbleToken();
|
|
593
|
+
if (!argument) {
|
|
594
|
+
this.throwError('missing unaryOp argument');
|
|
595
|
+
}
|
|
596
|
+
return this.runHook('after-token', {
|
|
597
|
+
type: Jsep.UNARY_EXP,
|
|
598
|
+
operator: to_check,
|
|
599
|
+
argument,
|
|
600
|
+
prefix: true
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
to_check = to_check.substr(0, --tc_len);
|
|
604
|
+
}
|
|
605
|
+
if (Jsep.isIdentifierStart(ch)) {
|
|
606
|
+
node = this.gobbleIdentifier();
|
|
607
|
+
if (Jsep.literals.hasOwnProperty(node.name)) {
|
|
608
|
+
node = {
|
|
609
|
+
type: Jsep.LITERAL,
|
|
610
|
+
value: Jsep.literals[node.name],
|
|
611
|
+
raw: node.name
|
|
612
|
+
};
|
|
613
|
+
} else if (node.name === Jsep.this_str) {
|
|
614
|
+
node = {
|
|
615
|
+
type: Jsep.THIS_EXP
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
} else if (ch === Jsep.OPAREN_CODE) {
|
|
619
|
+
// open parenthesis
|
|
620
|
+
node = this.gobbleGroup();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!node) {
|
|
624
|
+
return this.runHook('after-token', false);
|
|
625
|
+
}
|
|
626
|
+
node = this.gobbleTokenProperty(node);
|
|
627
|
+
return this.runHook('after-token', node);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Gobble properties of of identifiers/strings/arrays/groups.
|
|
632
|
+
* e.g. `foo`, `bar.baz`, `foo['bar'].baz`
|
|
633
|
+
* It also gobbles function calls:
|
|
634
|
+
* e.g. `Math.acos(obj.angle)`
|
|
635
|
+
* @param {jsep.Expression} node
|
|
636
|
+
* @returns {jsep.Expression}
|
|
637
|
+
*/
|
|
638
|
+
gobbleTokenProperty(node) {
|
|
639
|
+
this.gobbleSpaces();
|
|
640
|
+
let ch = this.code;
|
|
641
|
+
while (ch === Jsep.PERIOD_CODE || ch === Jsep.OBRACK_CODE || ch === Jsep.OPAREN_CODE || ch === Jsep.QUMARK_CODE) {
|
|
642
|
+
let optional;
|
|
643
|
+
if (ch === Jsep.QUMARK_CODE) {
|
|
644
|
+
if (this.expr.charCodeAt(this.index + 1) !== Jsep.PERIOD_CODE) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
optional = true;
|
|
648
|
+
this.index += 2;
|
|
649
|
+
this.gobbleSpaces();
|
|
650
|
+
ch = this.code;
|
|
651
|
+
}
|
|
652
|
+
this.index++;
|
|
653
|
+
if (ch === Jsep.OBRACK_CODE) {
|
|
654
|
+
node = {
|
|
655
|
+
type: Jsep.MEMBER_EXP,
|
|
656
|
+
computed: true,
|
|
657
|
+
object: node,
|
|
658
|
+
property: this.gobbleExpression()
|
|
659
|
+
};
|
|
660
|
+
if (!node.property) {
|
|
661
|
+
this.throwError('Unexpected "' + this.char + '"');
|
|
662
|
+
}
|
|
663
|
+
this.gobbleSpaces();
|
|
664
|
+
ch = this.code;
|
|
665
|
+
if (ch !== Jsep.CBRACK_CODE) {
|
|
666
|
+
this.throwError('Unclosed [');
|
|
667
|
+
}
|
|
668
|
+
this.index++;
|
|
669
|
+
} else if (ch === Jsep.OPAREN_CODE) {
|
|
670
|
+
// A function call is being made; gobble all the arguments
|
|
671
|
+
node = {
|
|
672
|
+
type: Jsep.CALL_EXP,
|
|
673
|
+
'arguments': this.gobbleArguments(Jsep.CPAREN_CODE),
|
|
674
|
+
callee: node
|
|
675
|
+
};
|
|
676
|
+
} else if (ch === Jsep.PERIOD_CODE || optional) {
|
|
677
|
+
if (optional) {
|
|
678
|
+
this.index--;
|
|
679
|
+
}
|
|
680
|
+
this.gobbleSpaces();
|
|
681
|
+
node = {
|
|
682
|
+
type: Jsep.MEMBER_EXP,
|
|
683
|
+
computed: false,
|
|
684
|
+
object: node,
|
|
685
|
+
property: this.gobbleIdentifier()
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (optional) {
|
|
689
|
+
node.optional = true;
|
|
690
|
+
} // else leave undefined for compatibility with esprima
|
|
691
|
+
|
|
692
|
+
this.gobbleSpaces();
|
|
693
|
+
ch = this.code;
|
|
694
|
+
}
|
|
695
|
+
return node;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Parse simple numeric literals: `12`, `3.4`, `.5`. Do this by using a string to
|
|
700
|
+
* keep track of everything in the numeric literal and then calling `parseFloat` on that string
|
|
701
|
+
* @returns {jsep.Literal}
|
|
702
|
+
*/
|
|
703
|
+
gobbleNumericLiteral() {
|
|
704
|
+
let number = '',
|
|
705
|
+
ch,
|
|
706
|
+
chCode;
|
|
707
|
+
while (Jsep.isDecimalDigit(this.code)) {
|
|
708
|
+
number += this.expr.charAt(this.index++);
|
|
709
|
+
}
|
|
710
|
+
if (this.code === Jsep.PERIOD_CODE) {
|
|
711
|
+
// can start with a decimal marker
|
|
712
|
+
number += this.expr.charAt(this.index++);
|
|
713
|
+
while (Jsep.isDecimalDigit(this.code)) {
|
|
714
|
+
number += this.expr.charAt(this.index++);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
ch = this.char;
|
|
718
|
+
if (ch === 'e' || ch === 'E') {
|
|
719
|
+
// exponent marker
|
|
720
|
+
number += this.expr.charAt(this.index++);
|
|
721
|
+
ch = this.char;
|
|
722
|
+
if (ch === '+' || ch === '-') {
|
|
723
|
+
// exponent sign
|
|
724
|
+
number += this.expr.charAt(this.index++);
|
|
725
|
+
}
|
|
726
|
+
while (Jsep.isDecimalDigit(this.code)) {
|
|
727
|
+
// exponent itself
|
|
728
|
+
number += this.expr.charAt(this.index++);
|
|
729
|
+
}
|
|
730
|
+
if (!Jsep.isDecimalDigit(this.expr.charCodeAt(this.index - 1))) {
|
|
731
|
+
this.throwError('Expected exponent (' + number + this.char + ')');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
chCode = this.code;
|
|
735
|
+
|
|
736
|
+
// Check to make sure this isn't a variable name that start with a number (123abc)
|
|
737
|
+
if (Jsep.isIdentifierStart(chCode)) {
|
|
738
|
+
this.throwError('Variable names cannot start with a number (' + number + this.char + ')');
|
|
739
|
+
} else if (chCode === Jsep.PERIOD_CODE || number.length === 1 && number.charCodeAt(0) === Jsep.PERIOD_CODE) {
|
|
740
|
+
this.throwError('Unexpected period');
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
type: Jsep.LITERAL,
|
|
744
|
+
value: parseFloat(number),
|
|
745
|
+
raw: number
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Parses a string literal, staring with single or double quotes with basic support for escape codes
|
|
751
|
+
* e.g. `"hello world"`, `'this is\nJSEP'`
|
|
752
|
+
* @returns {jsep.Literal}
|
|
753
|
+
*/
|
|
754
|
+
gobbleStringLiteral() {
|
|
755
|
+
let str = '';
|
|
756
|
+
const startIndex = this.index;
|
|
757
|
+
const quote = this.expr.charAt(this.index++);
|
|
758
|
+
let closed = false;
|
|
759
|
+
while (this.index < this.expr.length) {
|
|
760
|
+
let ch = this.expr.charAt(this.index++);
|
|
761
|
+
if (ch === quote) {
|
|
762
|
+
closed = true;
|
|
763
|
+
break;
|
|
764
|
+
} else if (ch === '\\') {
|
|
765
|
+
// Check for all of the common escape codes
|
|
766
|
+
ch = this.expr.charAt(this.index++);
|
|
767
|
+
switch (ch) {
|
|
768
|
+
case 'n':
|
|
769
|
+
str += '\n';
|
|
770
|
+
break;
|
|
771
|
+
case 'r':
|
|
772
|
+
str += '\r';
|
|
773
|
+
break;
|
|
774
|
+
case 't':
|
|
775
|
+
str += '\t';
|
|
776
|
+
break;
|
|
777
|
+
case 'b':
|
|
778
|
+
str += '\b';
|
|
779
|
+
break;
|
|
780
|
+
case 'f':
|
|
781
|
+
str += '\f';
|
|
782
|
+
break;
|
|
783
|
+
case 'v':
|
|
784
|
+
str += '\x0B';
|
|
785
|
+
break;
|
|
786
|
+
default:
|
|
787
|
+
str += ch;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
str += ch;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!closed) {
|
|
794
|
+
this.throwError('Unclosed quote after "' + str + '"');
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
type: Jsep.LITERAL,
|
|
798
|
+
value: str,
|
|
799
|
+
raw: this.expr.substring(startIndex, this.index)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Gobbles only identifiers
|
|
805
|
+
* e.g.: `foo`, `_value`, `$x1`
|
|
806
|
+
* Also, this function checks if that identifier is a literal:
|
|
807
|
+
* (e.g. `true`, `false`, `null`) or `this`
|
|
808
|
+
* @returns {jsep.Identifier}
|
|
809
|
+
*/
|
|
810
|
+
gobbleIdentifier() {
|
|
811
|
+
let ch = this.code,
|
|
812
|
+
start = this.index;
|
|
813
|
+
if (Jsep.isIdentifierStart(ch)) {
|
|
814
|
+
this.index++;
|
|
815
|
+
} else {
|
|
816
|
+
this.throwError('Unexpected ' + this.char);
|
|
817
|
+
}
|
|
818
|
+
while (this.index < this.expr.length) {
|
|
819
|
+
ch = this.code;
|
|
820
|
+
if (Jsep.isIdentifierPart(ch)) {
|
|
821
|
+
this.index++;
|
|
822
|
+
} else {
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
type: Jsep.IDENTIFIER,
|
|
828
|
+
name: this.expr.slice(start, this.index)
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Gobbles a list of arguments within the context of a function call
|
|
834
|
+
* or array literal. This function also assumes that the opening character
|
|
835
|
+
* `(` or `[` has already been gobbled, and gobbles expressions and commas
|
|
836
|
+
* until the terminator character `)` or `]` is encountered.
|
|
837
|
+
* e.g. `foo(bar, baz)`, `my_func()`, or `[bar, baz]`
|
|
838
|
+
* @param {number} termination
|
|
839
|
+
* @returns {jsep.Expression[]}
|
|
840
|
+
*/
|
|
841
|
+
gobbleArguments(termination) {
|
|
842
|
+
const args = [];
|
|
843
|
+
let closed = false;
|
|
844
|
+
let separator_count = 0;
|
|
845
|
+
while (this.index < this.expr.length) {
|
|
846
|
+
this.gobbleSpaces();
|
|
847
|
+
let ch_i = this.code;
|
|
848
|
+
if (ch_i === termination) {
|
|
849
|
+
// done parsing
|
|
850
|
+
closed = true;
|
|
851
|
+
this.index++;
|
|
852
|
+
if (termination === Jsep.CPAREN_CODE && separator_count && separator_count >= args.length) {
|
|
853
|
+
this.throwError('Unexpected token ' + String.fromCharCode(termination));
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
} else if (ch_i === Jsep.COMMA_CODE) {
|
|
857
|
+
// between expressions
|
|
858
|
+
this.index++;
|
|
859
|
+
separator_count++;
|
|
860
|
+
if (separator_count !== args.length) {
|
|
861
|
+
// missing argument
|
|
862
|
+
if (termination === Jsep.CPAREN_CODE) {
|
|
863
|
+
this.throwError('Unexpected token ,');
|
|
864
|
+
} else if (termination === Jsep.CBRACK_CODE) {
|
|
865
|
+
for (let arg = args.length; arg < separator_count; arg++) {
|
|
866
|
+
args.push(null);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} else if (args.length !== separator_count && separator_count !== 0) {
|
|
871
|
+
// NOTE: `&& separator_count !== 0` allows for either all commas, or all spaces as arguments
|
|
872
|
+
this.throwError('Expected comma');
|
|
873
|
+
} else {
|
|
874
|
+
const node = this.gobbleExpression();
|
|
875
|
+
if (!node || node.type === Jsep.COMPOUND) {
|
|
876
|
+
this.throwError('Expected comma');
|
|
877
|
+
}
|
|
878
|
+
args.push(node);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (!closed) {
|
|
882
|
+
this.throwError('Expected ' + String.fromCharCode(termination));
|
|
883
|
+
}
|
|
884
|
+
return args;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Responsible for parsing a group of things within parentheses `()`
|
|
889
|
+
* that have no identifier in front (so not a function call)
|
|
890
|
+
* This function assumes that it needs to gobble the opening parenthesis
|
|
891
|
+
* and then tries to gobble everything within that parenthesis, assuming
|
|
892
|
+
* that the next thing it should see is the close parenthesis. If not,
|
|
893
|
+
* then the expression probably doesn't have a `)`
|
|
894
|
+
* @returns {boolean|jsep.Expression}
|
|
895
|
+
*/
|
|
896
|
+
gobbleGroup() {
|
|
897
|
+
this.index++;
|
|
898
|
+
let nodes = this.gobbleExpressions(Jsep.CPAREN_CODE);
|
|
899
|
+
if (this.code === Jsep.CPAREN_CODE) {
|
|
900
|
+
this.index++;
|
|
901
|
+
if (nodes.length === 1) {
|
|
902
|
+
return nodes[0];
|
|
903
|
+
} else if (!nodes.length) {
|
|
904
|
+
return false;
|
|
905
|
+
} else {
|
|
906
|
+
return {
|
|
907
|
+
type: Jsep.SEQUENCE_EXP,
|
|
908
|
+
expressions: nodes
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
this.throwError('Unclosed (');
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Responsible for parsing Array literals `[1, 2, 3]`
|
|
918
|
+
* This function assumes that it needs to gobble the opening bracket
|
|
919
|
+
* and then tries to gobble the expressions as arguments.
|
|
920
|
+
* @returns {jsep.ArrayExpression}
|
|
921
|
+
*/
|
|
922
|
+
gobbleArray() {
|
|
923
|
+
this.index++;
|
|
924
|
+
return {
|
|
925
|
+
type: Jsep.ARRAY_EXP,
|
|
926
|
+
elements: this.gobbleArguments(Jsep.CBRACK_CODE)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Static fields:
|
|
932
|
+
const hooks = new Hooks();
|
|
933
|
+
Object.assign(Jsep, {
|
|
934
|
+
hooks,
|
|
935
|
+
plugins: new Plugins(Jsep),
|
|
936
|
+
// Node Types
|
|
937
|
+
// ----------
|
|
938
|
+
// This is the full set of types that any JSEP node can be.
|
|
939
|
+
// Store them here to save space when minified
|
|
940
|
+
COMPOUND: 'Compound',
|
|
941
|
+
SEQUENCE_EXP: 'SequenceExpression',
|
|
942
|
+
IDENTIFIER: 'Identifier',
|
|
943
|
+
MEMBER_EXP: 'MemberExpression',
|
|
944
|
+
LITERAL: 'Literal',
|
|
945
|
+
THIS_EXP: 'ThisExpression',
|
|
946
|
+
CALL_EXP: 'CallExpression',
|
|
947
|
+
UNARY_EXP: 'UnaryExpression',
|
|
948
|
+
BINARY_EXP: 'BinaryExpression',
|
|
949
|
+
ARRAY_EXP: 'ArrayExpression',
|
|
950
|
+
TAB_CODE: 9,
|
|
951
|
+
LF_CODE: 10,
|
|
952
|
+
CR_CODE: 13,
|
|
953
|
+
SPACE_CODE: 32,
|
|
954
|
+
PERIOD_CODE: 46,
|
|
955
|
+
// '.'
|
|
956
|
+
COMMA_CODE: 44,
|
|
957
|
+
// ','
|
|
958
|
+
SQUOTE_CODE: 39,
|
|
959
|
+
// single quote
|
|
960
|
+
DQUOTE_CODE: 34,
|
|
961
|
+
// double quotes
|
|
962
|
+
OPAREN_CODE: 40,
|
|
963
|
+
// (
|
|
964
|
+
CPAREN_CODE: 41,
|
|
965
|
+
// )
|
|
966
|
+
OBRACK_CODE: 91,
|
|
967
|
+
// [
|
|
968
|
+
CBRACK_CODE: 93,
|
|
969
|
+
// ]
|
|
970
|
+
QUMARK_CODE: 63,
|
|
971
|
+
// ?
|
|
972
|
+
SEMCOL_CODE: 59,
|
|
973
|
+
// ;
|
|
974
|
+
COLON_CODE: 58,
|
|
975
|
+
// :
|
|
976
|
+
|
|
977
|
+
// Operations
|
|
978
|
+
// ----------
|
|
979
|
+
// Use a quickly-accessible map to store all of the unary operators
|
|
980
|
+
// Values are set to `1` (it really doesn't matter)
|
|
981
|
+
unary_ops: {
|
|
982
|
+
'-': 1,
|
|
983
|
+
'!': 1,
|
|
984
|
+
'~': 1,
|
|
985
|
+
'+': 1
|
|
986
|
+
},
|
|
987
|
+
// Also use a map for the binary operations but set their values to their
|
|
988
|
+
// binary precedence for quick reference (higher number = higher precedence)
|
|
989
|
+
// see [Order of operations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)
|
|
990
|
+
binary_ops: {
|
|
991
|
+
'||': 1,
|
|
992
|
+
'??': 1,
|
|
993
|
+
'&&': 2,
|
|
994
|
+
'|': 3,
|
|
995
|
+
'^': 4,
|
|
996
|
+
'&': 5,
|
|
997
|
+
'==': 6,
|
|
998
|
+
'!=': 6,
|
|
999
|
+
'===': 6,
|
|
1000
|
+
'!==': 6,
|
|
1001
|
+
'<': 7,
|
|
1002
|
+
'>': 7,
|
|
1003
|
+
'<=': 7,
|
|
1004
|
+
'>=': 7,
|
|
1005
|
+
'<<': 8,
|
|
1006
|
+
'>>': 8,
|
|
1007
|
+
'>>>': 8,
|
|
1008
|
+
'+': 9,
|
|
1009
|
+
'-': 9,
|
|
1010
|
+
'*': 10,
|
|
1011
|
+
'/': 10,
|
|
1012
|
+
'%': 10,
|
|
1013
|
+
'**': 11
|
|
1014
|
+
},
|
|
1015
|
+
// sets specific binary_ops as right-associative
|
|
1016
|
+
right_associative: new Set(['**']),
|
|
1017
|
+
// Additional valid identifier chars, apart from a-z, A-Z and 0-9 (except on the starting char)
|
|
1018
|
+
additional_identifier_chars: new Set(['$', '_']),
|
|
1019
|
+
// Literals
|
|
1020
|
+
// ----------
|
|
1021
|
+
// Store the values to return for the various literals we may encounter
|
|
1022
|
+
literals: {
|
|
1023
|
+
'true': true,
|
|
1024
|
+
'false': false,
|
|
1025
|
+
'null': null
|
|
1026
|
+
},
|
|
1027
|
+
// Except for `this`, which is special. This could be changed to something like `'self'` as well
|
|
1028
|
+
this_str: 'this'
|
|
1029
|
+
});
|
|
1030
|
+
Jsep.max_unop_len = Jsep.getMaxKeyLen(Jsep.unary_ops);
|
|
1031
|
+
Jsep.max_binop_len = Jsep.getMaxKeyLen(Jsep.binary_ops);
|
|
1032
|
+
|
|
1033
|
+
// Backward Compatibility:
|
|
1034
|
+
const jsep = expr => new Jsep(expr).parse();
|
|
1035
|
+
const stdClassProps = Object.getOwnPropertyNames(class Test {});
|
|
1036
|
+
Object.getOwnPropertyNames(Jsep).filter(prop => !stdClassProps.includes(prop) && jsep[prop] === undefined).forEach(m => {
|
|
1037
|
+
jsep[m] = Jsep[m];
|
|
1038
|
+
});
|
|
1039
|
+
jsep.Jsep = Jsep; // allows for const { Jsep } = require('jsep');
|
|
1040
|
+
|
|
1041
|
+
const CONDITIONAL_EXP = 'ConditionalExpression';
|
|
1042
|
+
var ternary = {
|
|
1043
|
+
name: 'ternary',
|
|
1044
|
+
init(jsep) {
|
|
1045
|
+
// Ternary expression: test ? consequent : alternate
|
|
1046
|
+
jsep.hooks.add('after-expression', function gobbleTernary(env) {
|
|
1047
|
+
if (env.node && this.code === jsep.QUMARK_CODE) {
|
|
1048
|
+
this.index++;
|
|
1049
|
+
const test = env.node;
|
|
1050
|
+
const consequent = this.gobbleExpression();
|
|
1051
|
+
if (!consequent) {
|
|
1052
|
+
this.throwError('Expected expression');
|
|
1053
|
+
}
|
|
1054
|
+
this.gobbleSpaces();
|
|
1055
|
+
if (this.code === jsep.COLON_CODE) {
|
|
1056
|
+
this.index++;
|
|
1057
|
+
const alternate = this.gobbleExpression();
|
|
1058
|
+
if (!alternate) {
|
|
1059
|
+
this.throwError('Expected expression');
|
|
1060
|
+
}
|
|
1061
|
+
env.node = {
|
|
1062
|
+
type: CONDITIONAL_EXP,
|
|
1063
|
+
test,
|
|
1064
|
+
consequent,
|
|
1065
|
+
alternate
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
// check for operators of higher priority than ternary (i.e. assignment)
|
|
1069
|
+
// jsep sets || at 1, and assignment at 0.9, and conditional should be between them
|
|
1070
|
+
if (test.operator && jsep.binary_ops[test.operator] <= 0.9) {
|
|
1071
|
+
let newTest = test;
|
|
1072
|
+
while (newTest.right.operator && jsep.binary_ops[newTest.right.operator] <= 0.9) {
|
|
1073
|
+
newTest = newTest.right;
|
|
1074
|
+
}
|
|
1075
|
+
env.node.test = newTest.right;
|
|
1076
|
+
newTest.right = env.node;
|
|
1077
|
+
env.node = test;
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
this.throwError('Expected :');
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// Add default plugins:
|
|
1088
|
+
|
|
1089
|
+
jsep.plugins.register(ternary);
|
|
1090
|
+
|
|
1091
|
+
const FSLASH_CODE = 47; // '/'
|
|
1092
|
+
const BSLASH_CODE = 92; // '\\'
|
|
1093
|
+
|
|
1094
|
+
var index = {
|
|
1095
|
+
name: 'regex',
|
|
1096
|
+
init(jsep) {
|
|
1097
|
+
// Regex literal: /abc123/ig
|
|
1098
|
+
jsep.hooks.add('gobble-token', function gobbleRegexLiteral(env) {
|
|
1099
|
+
if (this.code === FSLASH_CODE) {
|
|
1100
|
+
const patternIndex = ++this.index;
|
|
1101
|
+
let inCharSet = false;
|
|
1102
|
+
while (this.index < this.expr.length) {
|
|
1103
|
+
if (this.code === FSLASH_CODE && !inCharSet) {
|
|
1104
|
+
const pattern = this.expr.slice(patternIndex, this.index);
|
|
1105
|
+
let flags = '';
|
|
1106
|
+
while (++this.index < this.expr.length) {
|
|
1107
|
+
const code = this.code;
|
|
1108
|
+
if (code >= 97 && code <= 122 // a...z
|
|
1109
|
+
|| code >= 65 && code <= 90 // A...Z
|
|
1110
|
+
|| code >= 48 && code <= 57) {
|
|
1111
|
+
// 0-9
|
|
1112
|
+
flags += this.char;
|
|
1113
|
+
} else {
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
let value;
|
|
1118
|
+
try {
|
|
1119
|
+
value = new RegExp(pattern, flags);
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
this.throwError(e.message);
|
|
1122
|
+
}
|
|
1123
|
+
env.node = {
|
|
1124
|
+
type: jsep.LITERAL,
|
|
1125
|
+
value,
|
|
1126
|
+
raw: this.expr.slice(patternIndex - 1, this.index)
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// allow . [] and () after regex: /regex/.test(a)
|
|
1130
|
+
env.node = this.gobbleTokenProperty(env.node);
|
|
1131
|
+
return env.node;
|
|
1132
|
+
}
|
|
1133
|
+
if (this.code === jsep.OBRACK_CODE) {
|
|
1134
|
+
inCharSet = true;
|
|
1135
|
+
} else if (inCharSet && this.code === jsep.CBRACK_CODE) {
|
|
1136
|
+
inCharSet = false;
|
|
1137
|
+
}
|
|
1138
|
+
this.index += this.code === BSLASH_CODE ? 2 : 1;
|
|
1139
|
+
}
|
|
1140
|
+
this.throwError('Unclosed Regex');
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const PLUS_CODE = 43; // +
|
|
1147
|
+
const MINUS_CODE = 45; // -
|
|
1148
|
+
|
|
1149
|
+
const plugin = {
|
|
1150
|
+
name: 'assignment',
|
|
1151
|
+
assignmentOperators: new Set(['=', '*=', '**=', '/=', '%=', '+=', '-=', '<<=', '>>=', '>>>=', '&=', '^=', '|=', '||=', '&&=', '??=']),
|
|
1152
|
+
updateOperators: [PLUS_CODE, MINUS_CODE],
|
|
1153
|
+
assignmentPrecedence: 0.9,
|
|
1154
|
+
init(jsep) {
|
|
1155
|
+
const updateNodeTypes = [jsep.IDENTIFIER, jsep.MEMBER_EXP];
|
|
1156
|
+
plugin.assignmentOperators.forEach(op => jsep.addBinaryOp(op, plugin.assignmentPrecedence, true));
|
|
1157
|
+
jsep.hooks.add('gobble-token', function gobbleUpdatePrefix(env) {
|
|
1158
|
+
const code = this.code;
|
|
1159
|
+
if (plugin.updateOperators.some(c => c === code && c === this.expr.charCodeAt(this.index + 1))) {
|
|
1160
|
+
this.index += 2;
|
|
1161
|
+
env.node = {
|
|
1162
|
+
type: 'UpdateExpression',
|
|
1163
|
+
operator: code === PLUS_CODE ? '++' : '--',
|
|
1164
|
+
argument: this.gobbleTokenProperty(this.gobbleIdentifier()),
|
|
1165
|
+
prefix: true
|
|
1166
|
+
};
|
|
1167
|
+
if (!env.node.argument || !updateNodeTypes.includes(env.node.argument.type)) {
|
|
1168
|
+
this.throwError(`Unexpected ${env.node.operator}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
jsep.hooks.add('after-token', function gobbleUpdatePostfix(env) {
|
|
1173
|
+
if (env.node) {
|
|
1174
|
+
const code = this.code;
|
|
1175
|
+
if (plugin.updateOperators.some(c => c === code && c === this.expr.charCodeAt(this.index + 1))) {
|
|
1176
|
+
if (!updateNodeTypes.includes(env.node.type)) {
|
|
1177
|
+
this.throwError(`Unexpected ${env.node.operator}`);
|
|
1178
|
+
}
|
|
1179
|
+
this.index += 2;
|
|
1180
|
+
env.node = {
|
|
1181
|
+
type: 'UpdateExpression',
|
|
1182
|
+
operator: code === PLUS_CODE ? '++' : '--',
|
|
1183
|
+
argument: env.node,
|
|
1184
|
+
prefix: false
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
jsep.hooks.add('after-expression', function gobbleAssignment(env) {
|
|
1190
|
+
if (env.node) {
|
|
1191
|
+
// Note: Binaries can be chained in a single expression to respect
|
|
1192
|
+
// operator precedence (i.e. a = b = 1 + 2 + 3)
|
|
1193
|
+
// Update all binary assignment nodes in the tree
|
|
1194
|
+
updateBinariesToAssignments(env.node);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
function updateBinariesToAssignments(node) {
|
|
1198
|
+
if (plugin.assignmentOperators.has(node.operator)) {
|
|
1199
|
+
node.type = 'AssignmentExpression';
|
|
1200
|
+
updateBinariesToAssignments(node.left);
|
|
1201
|
+
updateBinariesToAssignments(node.right);
|
|
1202
|
+
} else if (!node.operator) {
|
|
1203
|
+
Object.values(node).forEach(val => {
|
|
1204
|
+
if (val && typeof val === 'object') {
|
|
1205
|
+
updateBinariesToAssignments(val);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
/* eslint-disable no-bitwise -- Convenient */
|
|
1214
|
+
|
|
1215
|
+
// register plugins
|
|
1216
|
+
jsep.plugins.register(index, plugin);
|
|
1217
|
+
jsep.addUnaryOp('typeof');
|
|
1218
|
+
jsep.addUnaryOp('void');
|
|
1219
|
+
jsep.addLiteral('null', null);
|
|
1220
|
+
jsep.addLiteral('undefined', undefined);
|
|
15
1221
|
|
|
16
1222
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
17
1223
|
|
|
@@ -4837,7 +6043,7 @@ function requireSemver () {
|
|
|
4837
6043
|
return semver;
|
|
4838
6044
|
}
|
|
4839
6045
|
|
|
4840
|
-
|
|
6046
|
+
requireSemver();
|
|
4841
6047
|
|
|
4842
6048
|
function commonjsRequire(path) {
|
|
4843
6049
|
throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
|
|
@@ -17122,6 +18328,64 @@ ZodPromise.create;
|
|
|
17122
18328
|
ZodOptional.create;
|
|
17123
18329
|
ZodNullable.create;
|
|
17124
18330
|
|
|
18331
|
+
/**
|
|
18332
|
+
* Shared file I/O helpers for managed section operations.
|
|
18333
|
+
*
|
|
18334
|
+
* @remarks
|
|
18335
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
18336
|
+
* reusable utilities, eliminating duplication between
|
|
18337
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
18338
|
+
*/
|
|
18339
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
18340
|
+
const STALE_LOCK_MS = 120_000;
|
|
18341
|
+
/** Default core version when none provided. */
|
|
18342
|
+
const DEFAULT_CORE_VERSION = '0.0.0';
|
|
18343
|
+
/** Lock retry options. */
|
|
18344
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
18345
|
+
/**
|
|
18346
|
+
* Write content to a file atomically via a temp file + rename.
|
|
18347
|
+
*
|
|
18348
|
+
* @param filePath - Absolute path to the target file.
|
|
18349
|
+
* @param content - Content to write.
|
|
18350
|
+
*/
|
|
18351
|
+
function atomicWrite(filePath, content) {
|
|
18352
|
+
const dir = dirname(filePath);
|
|
18353
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
18354
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
18355
|
+
renameSync(tempPath, filePath);
|
|
18356
|
+
}
|
|
18357
|
+
/**
|
|
18358
|
+
* Execute a callback while holding a file lock.
|
|
18359
|
+
*
|
|
18360
|
+
* @remarks
|
|
18361
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
18362
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
18363
|
+
* and retries up to 5 times.
|
|
18364
|
+
*
|
|
18365
|
+
* @param filePath - Absolute path to the file to lock.
|
|
18366
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
18367
|
+
*/
|
|
18368
|
+
async function withFileLock(filePath, fn) {
|
|
18369
|
+
let release;
|
|
18370
|
+
try {
|
|
18371
|
+
release = await properLockfileExports.lock(filePath, {
|
|
18372
|
+
stale: STALE_LOCK_MS,
|
|
18373
|
+
retries: LOCK_RETRIES,
|
|
18374
|
+
});
|
|
18375
|
+
await fn();
|
|
18376
|
+
}
|
|
18377
|
+
finally {
|
|
18378
|
+
if (release) {
|
|
18379
|
+
try {
|
|
18380
|
+
await release();
|
|
18381
|
+
}
|
|
18382
|
+
catch {
|
|
18383
|
+
// Lock already released or file deleted — safe to ignore
|
|
18384
|
+
}
|
|
18385
|
+
}
|
|
18386
|
+
}
|
|
18387
|
+
}
|
|
18388
|
+
|
|
17125
18389
|
/**
|
|
17126
18390
|
* Comment markers for managed content blocks.
|
|
17127
18391
|
*
|
|
@@ -17148,10 +18412,6 @@ const TOOLS_MARKERS = {
|
|
|
17148
18412
|
* Captures: [1] marker text, [2] version, [3] timestamp
|
|
17149
18413
|
*/
|
|
17150
18414
|
const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
|
|
17151
|
-
/** Staleness threshold for version-stamp convergence in milliseconds. */
|
|
17152
|
-
const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
|
|
17153
|
-
/** Warning text prepended inside managed block when cleanup is needed. */
|
|
17154
|
-
const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
|
|
17155
18415
|
|
|
17156
18416
|
/**
|
|
17157
18417
|
* Managed section IDs and their stable ordering for TOOLS.md.
|
|
@@ -17185,61 +18445,6 @@ const SECTION_ORDER = [
|
|
|
17185
18445
|
SECTION_IDS.Meta,
|
|
17186
18446
|
];
|
|
17187
18447
|
|
|
17188
|
-
/**
|
|
17189
|
-
* Similarity-based cleanup detection for orphaned managed content.
|
|
17190
|
-
*
|
|
17191
|
-
* @remarks
|
|
17192
|
-
* Uses Jaccard similarity on 3-word shingles (Decision 22) to detect
|
|
17193
|
-
* when orphaned managed content exists in the user content zone.
|
|
17194
|
-
*/
|
|
17195
|
-
/** Default similarity threshold for cleanup detection. */
|
|
17196
|
-
const DEFAULT_THRESHOLD = 0.15;
|
|
17197
|
-
/**
|
|
17198
|
-
* Generate a set of n-word shingles from text.
|
|
17199
|
-
*
|
|
17200
|
-
* @param text - Input text.
|
|
17201
|
-
* @param n - Shingle size (default 3).
|
|
17202
|
-
* @returns Set of n-word shingles.
|
|
17203
|
-
*/
|
|
17204
|
-
function shingles(text, n = 3) {
|
|
17205
|
-
const words = text.toLowerCase().split(/\s+/).filter(Boolean);
|
|
17206
|
-
const set = new Set();
|
|
17207
|
-
for (let i = 0; i <= words.length - n; i++) {
|
|
17208
|
-
set.add(words.slice(i, i + n).join(' '));
|
|
17209
|
-
}
|
|
17210
|
-
return set;
|
|
17211
|
-
}
|
|
17212
|
-
/**
|
|
17213
|
-
* Compute Jaccard similarity between two sets.
|
|
17214
|
-
*
|
|
17215
|
-
* @param a - First set.
|
|
17216
|
-
* @param b - Second set.
|
|
17217
|
-
* @returns Jaccard similarity coefficient (0 to 1).
|
|
17218
|
-
*/
|
|
17219
|
-
function jaccard(a, b) {
|
|
17220
|
-
if (a.size === 0 && b.size === 0)
|
|
17221
|
-
return 0;
|
|
17222
|
-
let intersection = 0;
|
|
17223
|
-
for (const item of a) {
|
|
17224
|
-
if (b.has(item))
|
|
17225
|
-
intersection++;
|
|
17226
|
-
}
|
|
17227
|
-
return intersection / (a.size + b.size - intersection);
|
|
17228
|
-
}
|
|
17229
|
-
/**
|
|
17230
|
-
* Check whether user content contains orphaned managed content.
|
|
17231
|
-
*
|
|
17232
|
-
* @param managedContent - The current managed block content.
|
|
17233
|
-
* @param userContent - Content below the END marker.
|
|
17234
|
-
* @param threshold - Jaccard threshold (default 0.15).
|
|
17235
|
-
* @returns `true` if cleanup is needed.
|
|
17236
|
-
*/
|
|
17237
|
-
function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD) {
|
|
17238
|
-
if (!userContent.trim())
|
|
17239
|
-
return false;
|
|
17240
|
-
return jaccard(shingles(managedContent), shingles(userContent)) > threshold;
|
|
17241
|
-
}
|
|
17242
|
-
|
|
17243
18448
|
/**
|
|
17244
18449
|
* Stable section ordering for managed TOOLS.md blocks.
|
|
17245
18450
|
*
|
|
@@ -17408,149 +18613,6 @@ function formatBeginMarker(markerText, version) {
|
|
|
17408
18613
|
function formatEndMarker(markerText) {
|
|
17409
18614
|
return `<!-- ${markerText} -->`;
|
|
17410
18615
|
}
|
|
17411
|
-
/**
|
|
17412
|
-
* Determine whether this writer should proceed based on version-stamp
|
|
17413
|
-
* convergence rules.
|
|
17414
|
-
*
|
|
17415
|
-
* @param myVersion - The current core library version.
|
|
17416
|
-
* @param existing - The existing version stamp (if any).
|
|
17417
|
-
* @param stalenessThresholdMs - Staleness threshold in ms (default: 5 min).
|
|
17418
|
-
* @returns `true` if the writer should proceed with the write.
|
|
17419
|
-
*/
|
|
17420
|
-
function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRESHOLD_MS) {
|
|
17421
|
-
// No existing stamp — always write
|
|
17422
|
-
if (!existing)
|
|
17423
|
-
return true;
|
|
17424
|
-
// My version >= stamped version — always write (I'm current or newer)
|
|
17425
|
-
if (semverExports.gte(myVersion, existing.version))
|
|
17426
|
-
return true;
|
|
17427
|
-
// My version < stamped version — check staleness
|
|
17428
|
-
const stampAge = Date.now() - new Date(existing.timestamp).getTime();
|
|
17429
|
-
return stampAge >= stalenessThresholdMs;
|
|
17430
|
-
}
|
|
17431
|
-
|
|
17432
|
-
/**
|
|
17433
|
-
* Generic managed-section writer with block and section modes.
|
|
17434
|
-
*
|
|
17435
|
-
* @remarks
|
|
17436
|
-
* Supports two modes:
|
|
17437
|
-
* - `block`: Replaces the entire managed block (SOUL.md, AGENTS.md).
|
|
17438
|
-
* - `section`: Upserts a named H2 section within the managed block (TOOLS.md).
|
|
17439
|
-
*
|
|
17440
|
-
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
17441
|
-
*/
|
|
17442
|
-
/** Default core version when none provided. */
|
|
17443
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
17444
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
17445
|
-
const STALE_LOCK_MS = 120_000;
|
|
17446
|
-
/**
|
|
17447
|
-
* Update a managed section in a file.
|
|
17448
|
-
*
|
|
17449
|
-
* @param filePath - Absolute path to the target file.
|
|
17450
|
-
* @param content - New content to write.
|
|
17451
|
-
* @param options - Write mode and optional configuration.
|
|
17452
|
-
*/
|
|
17453
|
-
async function updateManagedSection(filePath, content, options = {}) {
|
|
17454
|
-
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_VERSION, stalenessThresholdMs, } = options;
|
|
17455
|
-
if (mode === 'section' && !sectionId) {
|
|
17456
|
-
throw new Error('sectionId is required when mode is "section"');
|
|
17457
|
-
}
|
|
17458
|
-
const dir = dirname(filePath);
|
|
17459
|
-
if (!existsSync(dir)) {
|
|
17460
|
-
mkdirSync(dir, { recursive: true });
|
|
17461
|
-
}
|
|
17462
|
-
// Create file if it doesn't exist
|
|
17463
|
-
if (!existsSync(filePath)) {
|
|
17464
|
-
writeFileSync(filePath, '', 'utf-8');
|
|
17465
|
-
}
|
|
17466
|
-
let release;
|
|
17467
|
-
try {
|
|
17468
|
-
release = await properLockfileExports.lock(filePath, {
|
|
17469
|
-
stale: STALE_LOCK_MS,
|
|
17470
|
-
retries: { retries: 5, minTimeout: 100, maxTimeout: 1000 },
|
|
17471
|
-
});
|
|
17472
|
-
const fileContent = readFileSync(filePath, 'utf-8');
|
|
17473
|
-
const parsed = parseManaged(fileContent, markers);
|
|
17474
|
-
// Version-stamp convergence check (block mode only).
|
|
17475
|
-
// In section mode, components always write their own sections — the version
|
|
17476
|
-
// stamp governs shared content convergence, not component-specific sections.
|
|
17477
|
-
if (mode === 'block' &&
|
|
17478
|
-
!shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
|
|
17479
|
-
return;
|
|
17480
|
-
}
|
|
17481
|
-
let newManagedBody;
|
|
17482
|
-
if (mode === 'block') {
|
|
17483
|
-
// Prepend H1 title if markers specify one
|
|
17484
|
-
newManagedBody = markers.title
|
|
17485
|
-
? `# ${markers.title}\n\n${content}`
|
|
17486
|
-
: content;
|
|
17487
|
-
}
|
|
17488
|
-
else {
|
|
17489
|
-
// Section mode: upsert the named section
|
|
17490
|
-
const sections = [...parsed.sections];
|
|
17491
|
-
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
17492
|
-
if (existingIdx >= 0) {
|
|
17493
|
-
sections[existingIdx] = { id: sectionId, content };
|
|
17494
|
-
}
|
|
17495
|
-
else {
|
|
17496
|
-
sections.push({ id: sectionId, content });
|
|
17497
|
-
}
|
|
17498
|
-
sortSectionsByOrder(sections);
|
|
17499
|
-
const sectionText = sections
|
|
17500
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
17501
|
-
.join('\n\n');
|
|
17502
|
-
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
17503
|
-
newManagedBody = markers.title
|
|
17504
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
17505
|
-
: sectionText;
|
|
17506
|
-
}
|
|
17507
|
-
// Cleanup detection
|
|
17508
|
-
const userContent = parsed.userContent;
|
|
17509
|
-
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
17510
|
-
// Build the full managed block
|
|
17511
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
17512
|
-
const endLine = formatEndMarker(markers.end);
|
|
17513
|
-
const parts = [];
|
|
17514
|
-
if (parsed.beforeContent) {
|
|
17515
|
-
parts.push(parsed.beforeContent);
|
|
17516
|
-
parts.push('');
|
|
17517
|
-
}
|
|
17518
|
-
parts.push(beginLine);
|
|
17519
|
-
if (cleanupNeeded) {
|
|
17520
|
-
parts.push('');
|
|
17521
|
-
parts.push(CLEANUP_FLAG);
|
|
17522
|
-
}
|
|
17523
|
-
parts.push('');
|
|
17524
|
-
parts.push(newManagedBody);
|
|
17525
|
-
parts.push('');
|
|
17526
|
-
parts.push(endLine);
|
|
17527
|
-
if (userContent) {
|
|
17528
|
-
parts.push('');
|
|
17529
|
-
parts.push(userContent);
|
|
17530
|
-
}
|
|
17531
|
-
parts.push('');
|
|
17532
|
-
const newFileContent = parts.join('\n');
|
|
17533
|
-
// Atomic write: write to temp file, then rename
|
|
17534
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
17535
|
-
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
17536
|
-
renameSync(tempPath, filePath);
|
|
17537
|
-
}
|
|
17538
|
-
catch (err) {
|
|
17539
|
-
// Log warning but don't throw — writer cycles are periodic
|
|
17540
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
17541
|
-
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
17542
|
-
}
|
|
17543
|
-
finally {
|
|
17544
|
-
if (release) {
|
|
17545
|
-
try {
|
|
17546
|
-
await release();
|
|
17547
|
-
}
|
|
17548
|
-
catch {
|
|
17549
|
-
// Lock already released or file deleted — safe to ignore
|
|
17550
|
-
}
|
|
17551
|
-
}
|
|
17552
|
-
}
|
|
17553
|
-
}
|
|
17554
18616
|
|
|
17555
18617
|
/**
|
|
17556
18618
|
* Core configuration schema and resolution.
|
|
@@ -17591,162 +18653,313 @@ objectType({
|
|
|
17591
18653
|
});
|
|
17592
18654
|
|
|
17593
18655
|
/**
|
|
17594
|
-
*
|
|
18656
|
+
* Remove a managed section or entire managed block from a file.
|
|
17595
18657
|
*
|
|
17596
|
-
*
|
|
17597
|
-
*
|
|
17598
|
-
*
|
|
18658
|
+
* @remarks
|
|
18659
|
+
* Supports two modes:
|
|
18660
|
+
* - No `sectionId`: Remove the entire managed block (markers + content),
|
|
18661
|
+
* leaving user content intact.
|
|
18662
|
+
* - With `sectionId`: Remove a specific H2 section from within the
|
|
18663
|
+
* managed block. If it was the last section, remove the entire block.
|
|
17599
18664
|
*
|
|
17600
|
-
*
|
|
17601
|
-
*
|
|
18665
|
+
* Provides file-level locking and atomic writes (temp file + rename).
|
|
18666
|
+
* Missing markers or nonexistent sections are no-ops (no error thrown).
|
|
18667
|
+
*/
|
|
18668
|
+
/**
|
|
18669
|
+
* Remove a managed section or entire managed block from a file.
|
|
17602
18670
|
*
|
|
17603
|
-
*
|
|
17604
|
-
*
|
|
17605
|
-
|
|
17606
|
-
|
|
18671
|
+
* @param filePath - Absolute path to the target file.
|
|
18672
|
+
* @param options - Optional section ID and custom markers.
|
|
18673
|
+
*/
|
|
18674
|
+
async function removeManagedSection(filePath, options = {}) {
|
|
18675
|
+
const { sectionId, markers = TOOLS_MARKERS } = options;
|
|
18676
|
+
if (!existsSync(filePath))
|
|
18677
|
+
return;
|
|
18678
|
+
await withFileLock(filePath, () => {
|
|
18679
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
18680
|
+
const parsed = parseManaged(fileContent, markers);
|
|
18681
|
+
if (!parsed.found)
|
|
18682
|
+
return;
|
|
18683
|
+
let newContent;
|
|
18684
|
+
if (!sectionId) {
|
|
18685
|
+
// Remove entire managed block
|
|
18686
|
+
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
18687
|
+
}
|
|
18688
|
+
else {
|
|
18689
|
+
// Remove specific section
|
|
18690
|
+
const remaining = parsed.sections.filter((s) => s.id !== sectionId);
|
|
18691
|
+
if (remaining.length === parsed.sections.length) {
|
|
18692
|
+
// Section not found — no-op
|
|
18693
|
+
return;
|
|
18694
|
+
}
|
|
18695
|
+
if (remaining.length === 0) {
|
|
18696
|
+
// Last section removed — remove entire block
|
|
18697
|
+
newContent = buildWithoutBlock(parsed.beforeContent, parsed.userContent);
|
|
18698
|
+
}
|
|
18699
|
+
else {
|
|
18700
|
+
// Rebuild managed block without the removed section
|
|
18701
|
+
newContent = buildWithSections(parsed.beforeContent, parsed.userContent, remaining, markers, parsed.versionStamp?.version);
|
|
18702
|
+
}
|
|
18703
|
+
}
|
|
18704
|
+
atomicWrite(filePath, newContent);
|
|
18705
|
+
});
|
|
18706
|
+
}
|
|
18707
|
+
/** Build file content without the managed block. */
|
|
18708
|
+
function buildWithoutBlock(beforeContent, userContent) {
|
|
18709
|
+
const parts = [];
|
|
18710
|
+
if (beforeContent)
|
|
18711
|
+
parts.push(beforeContent);
|
|
18712
|
+
if (userContent) {
|
|
18713
|
+
if (parts.length > 0)
|
|
18714
|
+
parts.push('');
|
|
18715
|
+
parts.push(userContent);
|
|
18716
|
+
}
|
|
18717
|
+
if (parts.length === 0)
|
|
18718
|
+
return '';
|
|
18719
|
+
return parts.join('\n') + '\n';
|
|
18720
|
+
}
|
|
18721
|
+
/** Rebuild file content with remaining sections. */
|
|
18722
|
+
function buildWithSections(beforeContent, userContent, sections, markers, coreVersion) {
|
|
18723
|
+
const sorted = sortSectionsByOrder([...sections]);
|
|
18724
|
+
const sectionText = sorted
|
|
18725
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
18726
|
+
.join('\n\n');
|
|
18727
|
+
const managedBody = markers.title
|
|
18728
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
18729
|
+
: sectionText;
|
|
18730
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion ?? DEFAULT_CORE_VERSION);
|
|
18731
|
+
const endLine = formatEndMarker(markers.end);
|
|
18732
|
+
const parts = [];
|
|
18733
|
+
if (beforeContent) {
|
|
18734
|
+
parts.push(beforeContent);
|
|
18735
|
+
parts.push('');
|
|
18736
|
+
}
|
|
18737
|
+
parts.push(beginLine);
|
|
18738
|
+
parts.push('');
|
|
18739
|
+
parts.push(managedBody);
|
|
18740
|
+
parts.push('');
|
|
18741
|
+
parts.push(endLine);
|
|
18742
|
+
if (userContent) {
|
|
18743
|
+
parts.push('');
|
|
18744
|
+
parts.push(userContent);
|
|
18745
|
+
}
|
|
18746
|
+
parts.push('');
|
|
18747
|
+
return parts.join('\n');
|
|
18748
|
+
}
|
|
18749
|
+
|
|
18750
|
+
/**
|
|
18751
|
+
* OpenClaw configuration helpers for plugin CLI installers.
|
|
18752
|
+
*
|
|
18753
|
+
* @remarks
|
|
18754
|
+
* Provides resolution of OpenClaw home directory and config file path,
|
|
18755
|
+
* plus idempotent config patching for plugin install/uninstall.
|
|
18756
|
+
*/
|
|
18757
|
+
/**
|
|
18758
|
+
* Resolve the OpenClaw home directory.
|
|
18759
|
+
*
|
|
18760
|
+
* @remarks
|
|
18761
|
+
* Resolution order:
|
|
18762
|
+
* 1. `OPENCLAW_CONFIG` env var → dirname of the config file path
|
|
18763
|
+
* 2. `OPENCLAW_HOME` env var → resolved path
|
|
18764
|
+
* 3. Default: `~/.openclaw`
|
|
18765
|
+
*
|
|
18766
|
+
* @returns Absolute path to the OpenClaw home directory.
|
|
17607
18767
|
*/
|
|
17608
|
-
const PLUGIN_ID = 'jeeves-watcher-openclaw';
|
|
17609
|
-
/** Resolve the OpenClaw home directory. */
|
|
17610
18768
|
function resolveOpenClawHome() {
|
|
17611
18769
|
if (process.env.OPENCLAW_CONFIG) {
|
|
17612
|
-
return dirname
|
|
18770
|
+
return dirname(resolve(process.env.OPENCLAW_CONFIG));
|
|
17613
18771
|
}
|
|
17614
18772
|
if (process.env.OPENCLAW_HOME) {
|
|
17615
18773
|
return resolve(process.env.OPENCLAW_HOME);
|
|
17616
18774
|
}
|
|
17617
|
-
return join
|
|
18775
|
+
return join(homedir(), '.openclaw');
|
|
17618
18776
|
}
|
|
17619
|
-
/**
|
|
18777
|
+
/**
|
|
18778
|
+
* Resolve the OpenClaw config file path.
|
|
18779
|
+
*
|
|
18780
|
+
* @remarks
|
|
18781
|
+
* If `OPENCLAW_CONFIG` is set, uses that directly.
|
|
18782
|
+
* Otherwise defaults to `{home}/openclaw.json`.
|
|
18783
|
+
*
|
|
18784
|
+
* @param home - The OpenClaw home directory.
|
|
18785
|
+
* @returns Absolute path to the config file.
|
|
18786
|
+
*/
|
|
17620
18787
|
function resolveConfigPath(home) {
|
|
17621
18788
|
if (process.env.OPENCLAW_CONFIG) {
|
|
17622
18789
|
return resolve(process.env.OPENCLAW_CONFIG);
|
|
17623
18790
|
}
|
|
17624
|
-
return join
|
|
17625
|
-
}
|
|
17626
|
-
/** Get the package root (where this CLI lives). */
|
|
17627
|
-
function getPackageRoot() {
|
|
17628
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
17629
|
-
return resolve(dirname$1(thisFile), '..');
|
|
17630
|
-
}
|
|
17631
|
-
/** Read and parse JSON, returning null on failure. */
|
|
17632
|
-
function readJson(path) {
|
|
17633
|
-
try {
|
|
17634
|
-
return JSON.parse(readFileSync$1(path, 'utf8'));
|
|
17635
|
-
}
|
|
17636
|
-
catch {
|
|
17637
|
-
return null;
|
|
17638
|
-
}
|
|
17639
|
-
}
|
|
17640
|
-
/** Write JSON with 2-space indent + trailing newline. */
|
|
17641
|
-
function writeJson(path, data) {
|
|
17642
|
-
writeFileSync$1(path, JSON.stringify(data, null, 2) + '\n');
|
|
18791
|
+
return join(home, 'openclaw.json');
|
|
17643
18792
|
}
|
|
17644
18793
|
/**
|
|
17645
18794
|
* Patch an allowlist array: add or remove the plugin ID.
|
|
17646
|
-
*
|
|
18795
|
+
*
|
|
18796
|
+
* @returns A log message if a change was made, or undefined.
|
|
17647
18797
|
*/
|
|
17648
|
-
function patchAllowList(parent, key, label, mode) {
|
|
17649
|
-
if (!Array.isArray(parent[key]) || parent[key].length === 0)
|
|
17650
|
-
return undefined;
|
|
17651
|
-
const list = parent[key];
|
|
18798
|
+
function patchAllowList(parent, key, label, pluginId, mode) {
|
|
17652
18799
|
if (mode === 'add') {
|
|
17653
|
-
if (!
|
|
17654
|
-
|
|
17655
|
-
return `
|
|
18800
|
+
if (!Array.isArray(parent[key])) {
|
|
18801
|
+
parent[key] = [pluginId];
|
|
18802
|
+
return `Created ${label} with "${pluginId}"`;
|
|
18803
|
+
}
|
|
18804
|
+
const list = parent[key];
|
|
18805
|
+
if (!list.includes(pluginId)) {
|
|
18806
|
+
list.push(pluginId);
|
|
18807
|
+
return `Added "${pluginId}" to ${label}`;
|
|
17656
18808
|
}
|
|
17657
18809
|
}
|
|
17658
18810
|
else {
|
|
17659
|
-
|
|
18811
|
+
if (!Array.isArray(parent[key]))
|
|
18812
|
+
return undefined;
|
|
18813
|
+
const list = parent[key];
|
|
18814
|
+
const filtered = list.filter((id) => id !== pluginId);
|
|
17660
18815
|
if (filtered.length !== list.length) {
|
|
17661
18816
|
parent[key] = filtered;
|
|
17662
|
-
return `Removed "${
|
|
18817
|
+
return `Removed "${pluginId}" from ${label}`;
|
|
17663
18818
|
}
|
|
17664
18819
|
}
|
|
17665
18820
|
return undefined;
|
|
17666
18821
|
}
|
|
17667
|
-
/**
|
|
17668
|
-
|
|
18822
|
+
/**
|
|
18823
|
+
* Patch an OpenClaw config for plugin install or uninstall.
|
|
18824
|
+
*
|
|
18825
|
+
* @remarks
|
|
18826
|
+
* Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
|
|
18827
|
+
* Idempotent: adding twice produces no duplicates; removing when absent
|
|
18828
|
+
* produces no errors.
|
|
18829
|
+
*
|
|
18830
|
+
* @param config - The parsed OpenClaw config object (mutated in place).
|
|
18831
|
+
* @param pluginId - The plugin identifier.
|
|
18832
|
+
* @param mode - Whether to add or remove the plugin.
|
|
18833
|
+
* @returns Array of log messages describing changes made.
|
|
18834
|
+
*/
|
|
18835
|
+
function patchConfig(config, pluginId, mode) {
|
|
17669
18836
|
const messages = [];
|
|
17670
18837
|
// Ensure plugins section
|
|
17671
18838
|
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
17672
18839
|
config.plugins = {};
|
|
17673
18840
|
}
|
|
17674
18841
|
const plugins = config.plugins;
|
|
17675
|
-
// plugins.allow
|
|
17676
|
-
const pluginAllow = patchAllowList(plugins, 'allow', 'plugins.allow', mode);
|
|
17677
|
-
if (pluginAllow)
|
|
17678
|
-
messages.push(pluginAllow);
|
|
17679
18842
|
// plugins.entries
|
|
17680
18843
|
if (!plugins.entries || typeof plugins.entries !== 'object') {
|
|
17681
18844
|
plugins.entries = {};
|
|
17682
18845
|
}
|
|
17683
18846
|
const entries = plugins.entries;
|
|
17684
18847
|
if (mode === 'add') {
|
|
17685
|
-
if (!entries[
|
|
17686
|
-
entries[
|
|
17687
|
-
messages.push(`Added "${
|
|
18848
|
+
if (!entries[pluginId]) {
|
|
18849
|
+
entries[pluginId] = { enabled: true };
|
|
18850
|
+
messages.push(`Added "${pluginId}" to plugins.entries`);
|
|
17688
18851
|
}
|
|
17689
18852
|
}
|
|
17690
|
-
else if (
|
|
17691
|
-
Reflect.deleteProperty(entries,
|
|
17692
|
-
messages.push(`Removed "${
|
|
18853
|
+
else if (pluginId in entries) {
|
|
18854
|
+
Reflect.deleteProperty(entries, pluginId);
|
|
18855
|
+
messages.push(`Removed "${pluginId}" from plugins.entries`);
|
|
18856
|
+
}
|
|
18857
|
+
// tools.alsoAllow
|
|
18858
|
+
if (!config.tools || typeof config.tools !== 'object') {
|
|
18859
|
+
config.tools = {};
|
|
17693
18860
|
}
|
|
17694
|
-
|
|
17695
|
-
const
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
messages.push(toolAllow);
|
|
18861
|
+
const tools = config.tools;
|
|
18862
|
+
const toolAlsoAllow = patchAllowList(tools, 'alsoAllow', 'tools.alsoAllow', pluginId, mode);
|
|
18863
|
+
if (toolAlsoAllow)
|
|
18864
|
+
messages.push(toolAlsoAllow);
|
|
17699
18865
|
return messages;
|
|
17700
18866
|
}
|
|
18867
|
+
|
|
18868
|
+
/**
|
|
18869
|
+
* @module plugin/constants
|
|
18870
|
+
* Shared constants for the OpenClaw plugin package.
|
|
18871
|
+
*
|
|
18872
|
+
* @remarks
|
|
18873
|
+
* Imported by both the plugin bundle (`index.ts`) and the CLI bundle
|
|
18874
|
+
* (`cli.ts`). Rollup inlines these into each output independently.
|
|
18875
|
+
*/
|
|
18876
|
+
/** Plugin identifier used in OpenClaw config and extensions directory. */
|
|
18877
|
+
const PLUGIN_ID = 'jeeves-watcher-openclaw';
|
|
18878
|
+
|
|
18879
|
+
/**
|
|
18880
|
+
* CLI for installing/uninstalling the jeeves-watcher OpenClaw plugin.
|
|
18881
|
+
*
|
|
18882
|
+
* Usage:
|
|
18883
|
+
* npx \@karmaniverous/jeeves-watcher-openclaw install
|
|
18884
|
+
* npx \@karmaniverous/jeeves-watcher-openclaw uninstall
|
|
18885
|
+
*
|
|
18886
|
+
* Bypasses OpenClaw's `plugins install` command, which has a known
|
|
18887
|
+
* spawn EINVAL bug on Windows (https://github.com/openclaw/openclaw/issues/9224).
|
|
18888
|
+
*
|
|
18889
|
+
* Supports non-default installations via:
|
|
18890
|
+
* - OPENCLAW_CONFIG env var (path to openclaw.json)
|
|
18891
|
+
* - OPENCLAW_HOME env var (path to .openclaw directory)
|
|
18892
|
+
* - Default: ~/.openclaw/openclaw.json
|
|
18893
|
+
*
|
|
18894
|
+
* @module cli
|
|
18895
|
+
*/
|
|
18896
|
+
/** Get the package root (where this CLI lives). */
|
|
18897
|
+
function getPackageRoot() {
|
|
18898
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
18899
|
+
return resolve(dirname(thisFile), '..');
|
|
18900
|
+
}
|
|
18901
|
+
/** Read and parse JSON, returning null on failure. */
|
|
18902
|
+
function readJson(path) {
|
|
18903
|
+
try {
|
|
18904
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
18905
|
+
}
|
|
18906
|
+
catch {
|
|
18907
|
+
return null;
|
|
18908
|
+
}
|
|
18909
|
+
}
|
|
18910
|
+
/** Write JSON with 2-space indent + trailing newline. */
|
|
18911
|
+
function writeJson(path, data) {
|
|
18912
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
|
|
18913
|
+
}
|
|
17701
18914
|
/** Install the plugin into OpenClaw's extensions directory. */
|
|
17702
18915
|
function install() {
|
|
17703
18916
|
const home = resolveOpenClawHome();
|
|
17704
18917
|
const configPath = resolveConfigPath(home);
|
|
17705
|
-
const extDir = join
|
|
18918
|
+
const extDir = join(home, 'extensions', PLUGIN_ID);
|
|
17706
18919
|
const pkgRoot = getPackageRoot();
|
|
17707
18920
|
console.log(`OpenClaw home: ${home}`);
|
|
17708
18921
|
console.log(`Config: ${configPath}`);
|
|
17709
18922
|
console.log(`Extensions dir: ${extDir}`);
|
|
17710
18923
|
console.log(`Package root: ${pkgRoot}`);
|
|
17711
18924
|
console.log();
|
|
17712
|
-
if (!existsSync
|
|
18925
|
+
if (!existsSync(home)) {
|
|
17713
18926
|
console.error(`Error: OpenClaw home directory not found at ${home}`);
|
|
17714
18927
|
console.error('Set OPENCLAW_HOME or OPENCLAW_CONFIG if using a non-default installation.');
|
|
17715
18928
|
process.exit(1);
|
|
17716
18929
|
}
|
|
17717
|
-
if (!existsSync
|
|
18930
|
+
if (!existsSync(configPath)) {
|
|
17718
18931
|
console.error(`Error: OpenClaw config not found at ${configPath}`);
|
|
17719
18932
|
console.error('Set OPENCLAW_CONFIG if using a non-default config location.');
|
|
17720
18933
|
process.exit(1);
|
|
17721
18934
|
}
|
|
17722
|
-
const pluginManifestPath = join
|
|
17723
|
-
if (!existsSync
|
|
18935
|
+
const pluginManifestPath = join(pkgRoot, 'openclaw.plugin.json');
|
|
18936
|
+
if (!existsSync(pluginManifestPath)) {
|
|
17724
18937
|
console.error(`Error: openclaw.plugin.json not found at ${pluginManifestPath}`);
|
|
17725
18938
|
process.exit(1);
|
|
17726
18939
|
}
|
|
17727
18940
|
// Copy package to extensions directory
|
|
17728
18941
|
console.log('Copying plugin to extensions directory...');
|
|
17729
|
-
if (existsSync
|
|
18942
|
+
if (existsSync(extDir)) {
|
|
17730
18943
|
rmSync(extDir, { recursive: true, force: true });
|
|
17731
18944
|
}
|
|
17732
|
-
mkdirSync
|
|
18945
|
+
mkdirSync(extDir, { recursive: true });
|
|
17733
18946
|
for (const file of [
|
|
17734
18947
|
'dist',
|
|
17735
18948
|
'content',
|
|
17736
18949
|
'openclaw.plugin.json',
|
|
17737
18950
|
'package.json',
|
|
17738
18951
|
]) {
|
|
17739
|
-
const src = join
|
|
17740
|
-
const dest = join
|
|
17741
|
-
if (existsSync
|
|
18952
|
+
const src = join(pkgRoot, file);
|
|
18953
|
+
const dest = join(extDir, file);
|
|
18954
|
+
if (existsSync(src)) {
|
|
17742
18955
|
cpSync(src, dest, { recursive: true });
|
|
17743
|
-
console.log(`
|
|
18956
|
+
console.log(` \u2713 ${file}`);
|
|
17744
18957
|
}
|
|
17745
18958
|
}
|
|
17746
|
-
const nodeModulesSrc = join
|
|
17747
|
-
if (existsSync
|
|
17748
|
-
cpSync(nodeModulesSrc, join
|
|
17749
|
-
console.log('
|
|
18959
|
+
const nodeModulesSrc = join(pkgRoot, 'node_modules');
|
|
18960
|
+
if (existsSync(nodeModulesSrc)) {
|
|
18961
|
+
cpSync(nodeModulesSrc, join(extDir, 'node_modules'), { recursive: true });
|
|
18962
|
+
console.log(' \u2713 node_modules');
|
|
17750
18963
|
}
|
|
17751
18964
|
// Patch config
|
|
17752
18965
|
console.log();
|
|
@@ -17756,114 +18969,51 @@ function install() {
|
|
|
17756
18969
|
console.error(`Error: Could not parse ${configPath}`);
|
|
17757
18970
|
process.exit(1);
|
|
17758
18971
|
}
|
|
17759
|
-
for (const msg of patchConfig(config, 'add')) {
|
|
17760
|
-
console.log(`
|
|
18972
|
+
for (const msg of patchConfig(config, PLUGIN_ID, 'add')) {
|
|
18973
|
+
console.log(` \u2713 ${msg}`);
|
|
17761
18974
|
}
|
|
17762
18975
|
writeJson(configPath, config);
|
|
17763
18976
|
console.log();
|
|
17764
|
-
console.log('
|
|
18977
|
+
console.log('\u2705 Plugin installed successfully.');
|
|
17765
18978
|
console.log(' Restart the OpenClaw gateway to load the plugin.');
|
|
17766
18979
|
}
|
|
17767
18980
|
/** Uninstall the plugin from OpenClaw's extensions directory. */
|
|
17768
18981
|
async function uninstall() {
|
|
17769
18982
|
const home = resolveOpenClawHome();
|
|
17770
18983
|
const configPath = resolveConfigPath(home);
|
|
17771
|
-
const extDir = join
|
|
18984
|
+
const extDir = join(home, 'extensions', PLUGIN_ID);
|
|
17772
18985
|
console.log(`OpenClaw home: ${home}`);
|
|
17773
18986
|
console.log(`Config: ${configPath}`);
|
|
17774
18987
|
console.log(`Extensions dir: ${extDir}`);
|
|
17775
18988
|
console.log();
|
|
17776
|
-
if (existsSync
|
|
18989
|
+
if (existsSync(extDir)) {
|
|
17777
18990
|
rmSync(extDir, { recursive: true, force: true });
|
|
17778
|
-
console.log(
|
|
18991
|
+
console.log(`\u2713 Removed ${extDir}`);
|
|
17779
18992
|
}
|
|
17780
18993
|
else {
|
|
17781
|
-
console.log(
|
|
18994
|
+
console.log(' (extensions directory not found, skipping)');
|
|
17782
18995
|
}
|
|
17783
|
-
if (existsSync
|
|
18996
|
+
if (existsSync(configPath)) {
|
|
17784
18997
|
console.log('Patching OpenClaw config...');
|
|
17785
18998
|
const config = readJson(configPath);
|
|
17786
18999
|
if (config) {
|
|
17787
|
-
for (const msg of patchConfig(config, 'remove')) {
|
|
17788
|
-
console.log(`
|
|
19000
|
+
for (const msg of patchConfig(config, PLUGIN_ID, 'remove')) {
|
|
19001
|
+
console.log(` \u2713 ${msg}`);
|
|
17789
19002
|
}
|
|
17790
19003
|
writeJson(configPath, config);
|
|
17791
19004
|
}
|
|
17792
19005
|
}
|
|
17793
|
-
//
|
|
17794
|
-
|
|
19006
|
+
// Remove managed TOOLS.md section
|
|
19007
|
+
const toolsPath = join(process.cwd(), 'TOOLS.md');
|
|
19008
|
+
if (existsSync(toolsPath)) {
|
|
19009
|
+
console.log('Removing managed TOOLS.md section...');
|
|
19010
|
+
await removeManagedSection(toolsPath, { sectionId: 'Watcher' });
|
|
19011
|
+
console.log(' \u2713 Removed Watcher section from TOOLS.md');
|
|
19012
|
+
}
|
|
17795
19013
|
console.log();
|
|
17796
|
-
console.log('
|
|
19014
|
+
console.log('\u2705 Plugin uninstalled successfully.');
|
|
17797
19015
|
console.log(' Restart the OpenClaw gateway to complete removal.');
|
|
17798
19016
|
}
|
|
17799
|
-
/** Resolve the workspace directory from OpenClaw config. */
|
|
17800
|
-
function resolveWorkspaceDir(home, configPath) {
|
|
17801
|
-
const config = readJson(configPath);
|
|
17802
|
-
if (!config)
|
|
17803
|
-
return null;
|
|
17804
|
-
// Check agents.defaults.workspace
|
|
17805
|
-
const agents = config.agents;
|
|
17806
|
-
const defaults = agents?.defaults;
|
|
17807
|
-
const workspace = defaults?.workspace;
|
|
17808
|
-
if (workspace) {
|
|
17809
|
-
return resolve(workspace.replace(/^~/, homedir()));
|
|
17810
|
-
}
|
|
17811
|
-
// Default workspace location
|
|
17812
|
-
return join$1(home, 'workspace');
|
|
17813
|
-
}
|
|
17814
|
-
/**
|
|
17815
|
-
* Remove the Watcher section from TOOLS.md on uninstall.
|
|
17816
|
-
*
|
|
17817
|
-
* @remarks
|
|
17818
|
-
* Uses core's `parseManaged` to locate the managed block and its sections,
|
|
17819
|
-
* then rewrites without the Watcher section via `updateManagedSection`.
|
|
17820
|
-
* If the Watcher section is the only one, removes the entire managed block.
|
|
17821
|
-
*/
|
|
17822
|
-
async function cleanupToolsMd(home, configPath) {
|
|
17823
|
-
const workspaceDir = resolveWorkspaceDir(home, configPath);
|
|
17824
|
-
if (!workspaceDir)
|
|
17825
|
-
return;
|
|
17826
|
-
const toolsPath = join$1(workspaceDir, 'TOOLS.md');
|
|
17827
|
-
if (!existsSync$1(toolsPath))
|
|
17828
|
-
return;
|
|
17829
|
-
const content = readFileSync$1(toolsPath, 'utf8');
|
|
17830
|
-
const parsed = parseManaged(content, TOOLS_MARKERS);
|
|
17831
|
-
if (!parsed.found)
|
|
17832
|
-
return;
|
|
17833
|
-
const watcherSection = parsed.sections.find((s) => s.id === 'Watcher');
|
|
17834
|
-
if (!watcherSection)
|
|
17835
|
-
return;
|
|
17836
|
-
const remaining = parsed.sections.filter((s) => s.id !== 'Watcher');
|
|
17837
|
-
if (remaining.length === 0) {
|
|
17838
|
-
// No sections left — remove the entire managed block.
|
|
17839
|
-
const parts = [];
|
|
17840
|
-
if (parsed.beforeContent)
|
|
17841
|
-
parts.push(parsed.beforeContent);
|
|
17842
|
-
if (parsed.userContent) {
|
|
17843
|
-
if (parts.length > 0)
|
|
17844
|
-
parts.push('');
|
|
17845
|
-
parts.push(parsed.userContent);
|
|
17846
|
-
}
|
|
17847
|
-
const newContent = parts.join('\n').trim() + '\n';
|
|
17848
|
-
writeFileSync$1(toolsPath, newContent);
|
|
17849
|
-
}
|
|
17850
|
-
else {
|
|
17851
|
-
// Rewrite the managed block without the Watcher section.
|
|
17852
|
-
// We write an empty string for the Watcher section; core's
|
|
17853
|
-
// updateManagedSection will rebuild from the remaining sections.
|
|
17854
|
-
// Actually, the cleanest approach is to rebuild the section text
|
|
17855
|
-
// from the remaining sections and write the entire block.
|
|
17856
|
-
const sectionText = remaining
|
|
17857
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
17858
|
-
.join('\n\n');
|
|
17859
|
-
const body = `# ${TOOLS_MARKERS.title}\n\n${sectionText}`;
|
|
17860
|
-
await updateManagedSection(toolsPath, body, {
|
|
17861
|
-
mode: 'block',
|
|
17862
|
-
markers: TOOLS_MARKERS,
|
|
17863
|
-
});
|
|
17864
|
-
}
|
|
17865
|
-
console.log('\u2713 Cleaned up TOOLS.md (removed Watcher section)');
|
|
17866
|
-
}
|
|
17867
19017
|
// Main
|
|
17868
19018
|
const command = process.argv[2];
|
|
17869
19019
|
switch (command) {
|
|
@@ -17874,7 +19024,7 @@ switch (command) {
|
|
|
17874
19024
|
void uninstall();
|
|
17875
19025
|
break;
|
|
17876
19026
|
default:
|
|
17877
|
-
console.log(`@karmaniverous/jeeves-watcher-openclaw
|
|
19027
|
+
console.log(`@karmaniverous/jeeves-watcher-openclaw \u2014 OpenClaw plugin installer`);
|
|
17878
19028
|
console.log();
|
|
17879
19029
|
console.log('Usage:');
|
|
17880
19030
|
console.log(' npx @karmaniverous/jeeves-watcher-openclaw install Install plugin');
|
|
@@ -17894,5 +19044,3 @@ switch (command) {
|
|
|
17894
19044
|
}
|
|
17895
19045
|
break;
|
|
17896
19046
|
}
|
|
17897
|
-
|
|
17898
|
-
export { patchConfig };
|