hostctl 0.1.32
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/LICENSE +277 -0
- package/README.md +1027 -0
- package/dist/bin/hostctl.d.ts +1 -0
- package/dist/bin/hostctl.js +4962 -0
- package/dist/bin/hostctl.js.map +1 -0
- package/dist/index.d.ts +1693 -0
- package/dist/index.js +7204 -0
- package/dist/index.js.map +1 -0
- package/package.json +124 -0
|
@@ -0,0 +1,4962 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import process4 from "process";
|
|
5
|
+
import * as cmdr from "commander";
|
|
6
|
+
|
|
7
|
+
// src/app.ts
|
|
8
|
+
import process3 from "process";
|
|
9
|
+
import * as fs6 from "fs";
|
|
10
|
+
import { homedir as homedir3 } from "os";
|
|
11
|
+
|
|
12
|
+
// src/handlebars.ts
|
|
13
|
+
import Handlebars from "handlebars";
|
|
14
|
+
Handlebars.registerHelper("join", function(items, block) {
|
|
15
|
+
var delimiter = block.hash.delimiter || ",", start = block.hash.start || 0, len = items ? items.length : 0, end = block.hash.end || len, out = "";
|
|
16
|
+
if (end > len) end = len;
|
|
17
|
+
if ("function" === typeof block) {
|
|
18
|
+
for (let i = start; i < end; i++) {
|
|
19
|
+
if (i > start) out += delimiter;
|
|
20
|
+
if ("string" === typeof items[i]) out += items[i];
|
|
21
|
+
else out += block(items[i]);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
} else {
|
|
25
|
+
return [].concat(items).slice(start, end).join(delimiter);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// src/config.ts
|
|
30
|
+
import * as fs4 from "fs";
|
|
31
|
+
|
|
32
|
+
// src/config-file.ts
|
|
33
|
+
import fsa from "fs/promises";
|
|
34
|
+
import "fs";
|
|
35
|
+
import { globSync as globSync2 } from "glob";
|
|
36
|
+
import { bech32 } from "@scure/base";
|
|
37
|
+
import "rambda";
|
|
38
|
+
import yaml from "js-yaml";
|
|
39
|
+
|
|
40
|
+
// src/flex/function.ts
|
|
41
|
+
var GeneratorFunction = function* () {
|
|
42
|
+
}.constructor;
|
|
43
|
+
var MethodBag = class {
|
|
44
|
+
methods;
|
|
45
|
+
staticMethods;
|
|
46
|
+
constructor() {
|
|
47
|
+
this.methods = /* @__PURE__ */ new Map();
|
|
48
|
+
this.staticMethods = /* @__PURE__ */ new Map();
|
|
49
|
+
}
|
|
50
|
+
registerUncurriedFns(fnBag) {
|
|
51
|
+
Object.entries(fnBag).forEach(([functionName, functionObj]) => {
|
|
52
|
+
this.methods.set(functionName, [functionObj, 1]);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
registerCurriedFns(fnBag) {
|
|
56
|
+
Object.entries(fnBag).forEach(([functionName, functionObj]) => {
|
|
57
|
+
this.methods.set(functionName, [functionObj, 2]);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
registerStatic(fnBag) {
|
|
61
|
+
Object.entries(fnBag).forEach(([functionName, functionObj]) => {
|
|
62
|
+
this.staticMethods.set(functionName, functionObj);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
lookup(fnName) {
|
|
66
|
+
const pair = this.methods.get(fnName);
|
|
67
|
+
if (!pair) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
return pair;
|
|
71
|
+
}
|
|
72
|
+
lookupStatic(fnName) {
|
|
73
|
+
const fn = this.staticMethods.get(fnName);
|
|
74
|
+
if (!fn) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
return fn;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
function buildWrapperProxy() {
|
|
81
|
+
const methods = new MethodBag();
|
|
82
|
+
function func(wrappedValue) {
|
|
83
|
+
return new Proxy(wrappedValue, {
|
|
84
|
+
get(target, property, receiver) {
|
|
85
|
+
const fnArityPair = methods.lookup(property.toString());
|
|
86
|
+
if (fnArityPair === void 0) {
|
|
87
|
+
return Reflect.get(target, property, receiver);
|
|
88
|
+
}
|
|
89
|
+
const [fn, arity] = fnArityPair;
|
|
90
|
+
return function(...args) {
|
|
91
|
+
if (arity === 1) {
|
|
92
|
+
return fn.call(receiver, target);
|
|
93
|
+
} else {
|
|
94
|
+
return fn.apply(receiver, args)(target);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
Object.assign(func, {
|
|
101
|
+
registerUncurriedFns: (fnBag) => {
|
|
102
|
+
methods.registerUncurriedFns(fnBag);
|
|
103
|
+
},
|
|
104
|
+
registerCurriedFns: (fnBag) => {
|
|
105
|
+
methods.registerCurriedFns(fnBag);
|
|
106
|
+
},
|
|
107
|
+
registerStatic: (fnBag) => {
|
|
108
|
+
methods.registerStatic(fnBag);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const funcWithMethods = func;
|
|
112
|
+
return new Proxy(funcWithMethods, {
|
|
113
|
+
get(target, property, receiver) {
|
|
114
|
+
const fn = methods.lookupStatic(property.toString());
|
|
115
|
+
if (fn === void 0) {
|
|
116
|
+
return Reflect.get(target, property, receiver);
|
|
117
|
+
}
|
|
118
|
+
return fn;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function curry(func) {
|
|
123
|
+
return function curried(...args) {
|
|
124
|
+
if (args.length >= func.length) {
|
|
125
|
+
return func.apply(this, args);
|
|
126
|
+
} else {
|
|
127
|
+
return function(...args2) {
|
|
128
|
+
return curried.apply(this, args.concat(args2));
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function identity(val) {
|
|
134
|
+
return val;
|
|
135
|
+
}
|
|
136
|
+
function parameters(func) {
|
|
137
|
+
const ARROW = true;
|
|
138
|
+
const FUNC_ARGS = ARROW ? /^(function)?\s*[^\(]*\(\s*([^\)]*)\)/m : /^(function)\s*[^\(]*\(\s*([^\)]*)\)/m;
|
|
139
|
+
const FUNC_ARG_SPLIT = /,/;
|
|
140
|
+
const FUNC_ARG = /^\s*(_?)(.+?)\1\s*$/;
|
|
141
|
+
const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm;
|
|
142
|
+
return ((func || "").toString().replace(STRIP_COMMENTS, "").match(FUNC_ARGS) || ["", "", ""])[2].split(FUNC_ARG_SPLIT).map(function(arg) {
|
|
143
|
+
return arg.replace(FUNC_ARG, function(all, underscore, name) {
|
|
144
|
+
return name.split("=")[0].trim();
|
|
145
|
+
});
|
|
146
|
+
}).filter(String);
|
|
147
|
+
}
|
|
148
|
+
function thread(...args) {
|
|
149
|
+
return function(firstArg = void 0) {
|
|
150
|
+
let pipeline = args;
|
|
151
|
+
if (firstArg !== void 0) {
|
|
152
|
+
pipeline.unshift(firstArg);
|
|
153
|
+
}
|
|
154
|
+
return pipeline.reduce((arg, fn) => fn(arg));
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function threadR(...fns) {
|
|
158
|
+
return function(firstArg = void 0) {
|
|
159
|
+
let pipeline = fns;
|
|
160
|
+
if (firstArg !== void 0) {
|
|
161
|
+
pipeline.push(firstArg);
|
|
162
|
+
}
|
|
163
|
+
return pipeline.reduceRight((arg, fn) => fn(arg));
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
var F = buildWrapperProxy();
|
|
167
|
+
F.registerUncurriedFns({ parameters });
|
|
168
|
+
F.registerStatic({
|
|
169
|
+
curry,
|
|
170
|
+
identity,
|
|
171
|
+
parameters,
|
|
172
|
+
thread,
|
|
173
|
+
threadR
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// src/flex/protocol.ts
|
|
177
|
+
import { Mutex } from "async-mutex";
|
|
178
|
+
|
|
179
|
+
// src/flex/type.ts
|
|
180
|
+
import { inspect as nodeInspect } from "util";
|
|
181
|
+
var End = class _End {
|
|
182
|
+
static _instance;
|
|
183
|
+
constructor() {
|
|
184
|
+
}
|
|
185
|
+
static get instance() {
|
|
186
|
+
if (!this._instance) {
|
|
187
|
+
this._instance = new _End();
|
|
188
|
+
}
|
|
189
|
+
return this._instance;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var Undefined = class {
|
|
193
|
+
};
|
|
194
|
+
var Null = class {
|
|
195
|
+
};
|
|
196
|
+
var Class = class {
|
|
197
|
+
};
|
|
198
|
+
function inspect(valueToInspect) {
|
|
199
|
+
return nodeInspect(valueToInspect, { showProxy: true });
|
|
200
|
+
}
|
|
201
|
+
function isA(predicateType) {
|
|
202
|
+
return function(value) {
|
|
203
|
+
return value instanceof predicateType || kind(value) === predicateType || value != null && (value.constructor === predicateType || predicateType.name === "Object" && typeof value === "object");
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function isAbsent(value) {
|
|
207
|
+
return value === null || value === void 0;
|
|
208
|
+
}
|
|
209
|
+
function isDefined(value) {
|
|
210
|
+
return value !== void 0;
|
|
211
|
+
}
|
|
212
|
+
var BUILT_IN_CONSTRUCTORS = [
|
|
213
|
+
String,
|
|
214
|
+
Number,
|
|
215
|
+
Boolean,
|
|
216
|
+
Array,
|
|
217
|
+
Object,
|
|
218
|
+
Function,
|
|
219
|
+
Date,
|
|
220
|
+
RegExp,
|
|
221
|
+
Map,
|
|
222
|
+
Set,
|
|
223
|
+
Error,
|
|
224
|
+
Promise,
|
|
225
|
+
Symbol,
|
|
226
|
+
BigInt
|
|
227
|
+
];
|
|
228
|
+
function isClass(fn) {
|
|
229
|
+
if (typeof fn !== "function") {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (/^class\s/.test(Function.prototype.toString.call(fn))) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
if (BUILT_IN_CONSTRUCTORS.includes(fn)) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
function isObject(val) {
|
|
241
|
+
return val !== null && (typeof val === "function" || typeof val === "object");
|
|
242
|
+
}
|
|
243
|
+
function isPresent(value) {
|
|
244
|
+
return !isAbsent(value);
|
|
245
|
+
}
|
|
246
|
+
function kind(value) {
|
|
247
|
+
if (value === null) {
|
|
248
|
+
return Null;
|
|
249
|
+
}
|
|
250
|
+
switch (typeof value) {
|
|
251
|
+
case "undefined":
|
|
252
|
+
return Undefined;
|
|
253
|
+
case "boolean":
|
|
254
|
+
return Boolean;
|
|
255
|
+
case "number":
|
|
256
|
+
return Number;
|
|
257
|
+
case "bigint":
|
|
258
|
+
return BigInt;
|
|
259
|
+
case "string":
|
|
260
|
+
return String;
|
|
261
|
+
case "symbol":
|
|
262
|
+
return Symbol;
|
|
263
|
+
case "function":
|
|
264
|
+
if (isClass(value)) {
|
|
265
|
+
return Class;
|
|
266
|
+
} else {
|
|
267
|
+
return value.constructor;
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case "object":
|
|
271
|
+
return value.constructor;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function klass(objOrClass) {
|
|
276
|
+
if (isClass(objOrClass)) {
|
|
277
|
+
return Function;
|
|
278
|
+
}
|
|
279
|
+
const proto = Object.getPrototypeOf(objOrClass);
|
|
280
|
+
if (proto === null || proto === void 0) {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
return proto.constructor;
|
|
284
|
+
}
|
|
285
|
+
function superclass(objOrClass) {
|
|
286
|
+
if (objOrClass === Object) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (isClass(objOrClass)) {
|
|
290
|
+
const proto = Object.getPrototypeOf(objOrClass);
|
|
291
|
+
if (proto === Function.prototype) {
|
|
292
|
+
return Object;
|
|
293
|
+
}
|
|
294
|
+
return proto;
|
|
295
|
+
} else {
|
|
296
|
+
const instanceProto = Object.getPrototypeOf(objOrClass);
|
|
297
|
+
if (instanceProto === null || instanceProto === void 0) {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
const superProto = Object.getPrototypeOf(instanceProto);
|
|
301
|
+
if (superProto === null || superProto === void 0) {
|
|
302
|
+
return superProto === null ? null : void 0;
|
|
303
|
+
}
|
|
304
|
+
return superProto.constructor;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
var Value = class {
|
|
308
|
+
constructor(value) {
|
|
309
|
+
this.value = value;
|
|
310
|
+
}
|
|
311
|
+
isWrappedValue = true;
|
|
312
|
+
// isA(predicateType: any): boolean {
|
|
313
|
+
// return isA(predicateType)(this.value);
|
|
314
|
+
// }
|
|
315
|
+
// isAbsent(value: any) {
|
|
316
|
+
// return isAbsent(value);
|
|
317
|
+
// }
|
|
318
|
+
// isClass(value: unknown) {
|
|
319
|
+
// isClass(value);
|
|
320
|
+
// }
|
|
321
|
+
// isPresent(value: any) {
|
|
322
|
+
// return isPresent(value);
|
|
323
|
+
// }
|
|
324
|
+
// isError(value: unknown): value is Error {
|
|
325
|
+
// return isError(value);
|
|
326
|
+
// }
|
|
327
|
+
// inspect() {
|
|
328
|
+
// return inspect(this.value);
|
|
329
|
+
// }
|
|
330
|
+
// kind() {
|
|
331
|
+
// return kind(this.value);
|
|
332
|
+
// }
|
|
333
|
+
// klass() {
|
|
334
|
+
// return klass(this.value);
|
|
335
|
+
// }
|
|
336
|
+
// superclass() {
|
|
337
|
+
// return superclass(this.value);
|
|
338
|
+
// }
|
|
339
|
+
};
|
|
340
|
+
function buildValueWrapperProxy() {
|
|
341
|
+
const methods = new MethodBag();
|
|
342
|
+
function func(wrappedValue) {
|
|
343
|
+
if (!isObject(wrappedValue)) {
|
|
344
|
+
wrappedValue = new Value(wrappedValue);
|
|
345
|
+
}
|
|
346
|
+
return new Proxy(wrappedValue, {
|
|
347
|
+
get(target, property, receiver) {
|
|
348
|
+
const fnArityPair = methods.lookup(property.toString());
|
|
349
|
+
if (fnArityPair === void 0) {
|
|
350
|
+
if (target.isWrappedValue) {
|
|
351
|
+
return Reflect.get(target.value, property, receiver);
|
|
352
|
+
} else {
|
|
353
|
+
return Reflect.get(target, property, receiver);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const [fn, arity] = fnArityPair;
|
|
357
|
+
return function(...args) {
|
|
358
|
+
if (arity === 1) {
|
|
359
|
+
if (target.isWrappedValue) {
|
|
360
|
+
return fn.call(receiver, target.value);
|
|
361
|
+
} else {
|
|
362
|
+
return fn.call(receiver, target);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
if (target.isWrappedValue) {
|
|
366
|
+
return fn.apply(receiver, args)(target.value);
|
|
367
|
+
} else {
|
|
368
|
+
return fn.apply(receiver, args)(target);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
Object.assign(func, {
|
|
376
|
+
registerUncurriedFns(fnBag) {
|
|
377
|
+
return methods.registerUncurriedFns(fnBag);
|
|
378
|
+
},
|
|
379
|
+
registerCurriedFns(fnBag) {
|
|
380
|
+
return methods.registerCurriedFns(fnBag);
|
|
381
|
+
},
|
|
382
|
+
registerStatic(fnBag) {
|
|
383
|
+
return methods.registerStatic(fnBag);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
const funcWithMethods = func;
|
|
387
|
+
return new Proxy(funcWithMethods, {
|
|
388
|
+
get(target, property, receiver) {
|
|
389
|
+
const fn = methods.lookupStatic(property.toString());
|
|
390
|
+
if (fn === void 0) {
|
|
391
|
+
return Reflect.get(target, property, receiver);
|
|
392
|
+
}
|
|
393
|
+
return fn;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
var V = buildValueWrapperProxy();
|
|
398
|
+
V.registerUncurriedFns({
|
|
399
|
+
isAbsent,
|
|
400
|
+
isClass,
|
|
401
|
+
isDefined,
|
|
402
|
+
isObject,
|
|
403
|
+
isPresent,
|
|
404
|
+
inspect,
|
|
405
|
+
kind,
|
|
406
|
+
klass,
|
|
407
|
+
superclass
|
|
408
|
+
});
|
|
409
|
+
V.registerCurriedFns({ isA });
|
|
410
|
+
V.registerStatic({
|
|
411
|
+
isA,
|
|
412
|
+
isAbsent,
|
|
413
|
+
isClass,
|
|
414
|
+
isDefined,
|
|
415
|
+
isObject,
|
|
416
|
+
isPresent,
|
|
417
|
+
inspect,
|
|
418
|
+
kind,
|
|
419
|
+
klass,
|
|
420
|
+
superclass
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// src/flex/protocol.ts
|
|
424
|
+
var NamedImplementations = class {
|
|
425
|
+
constructor(impls = /* @__PURE__ */ new Map(), defaultImpl = "") {
|
|
426
|
+
this.impls = impls;
|
|
427
|
+
this.defaultImpl = defaultImpl;
|
|
428
|
+
}
|
|
429
|
+
register(name, implClass, makeDefault = false) {
|
|
430
|
+
if (this.impls.size === 0 || makeDefault) {
|
|
431
|
+
this.setDefaultImpl(name);
|
|
432
|
+
}
|
|
433
|
+
this.impls.set(name, implClass);
|
|
434
|
+
}
|
|
435
|
+
get(name) {
|
|
436
|
+
return this.impls.get(name || this.defaultImpl);
|
|
437
|
+
}
|
|
438
|
+
setDefaultImpl(name) {
|
|
439
|
+
this.defaultImpl = name;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var ImplementationRegistry = class {
|
|
443
|
+
constructor(name) {
|
|
444
|
+
this.name = name;
|
|
445
|
+
}
|
|
446
|
+
// implementations is a Map of (type constructor function => ProtocolImplGroup) pairs
|
|
447
|
+
// It stores all the named implementations of the typeclass/protocol associated with the type stored in the key of the map
|
|
448
|
+
implementations = /* @__PURE__ */ new Map();
|
|
449
|
+
register(classConstructor, implClass, makeDefault = false) {
|
|
450
|
+
let protocolImplGroup = this.implementations.get(classConstructor);
|
|
451
|
+
if (!protocolImplGroup) {
|
|
452
|
+
protocolImplGroup = new NamedImplementations();
|
|
453
|
+
this.implementations.set(classConstructor, protocolImplGroup);
|
|
454
|
+
}
|
|
455
|
+
protocolImplGroup.register(implClass.name, implClass, makeDefault);
|
|
456
|
+
}
|
|
457
|
+
use(classConstructor, implClass) {
|
|
458
|
+
this.register(classConstructor, implClass, true);
|
|
459
|
+
}
|
|
460
|
+
clear() {
|
|
461
|
+
this.implementations.clear();
|
|
462
|
+
}
|
|
463
|
+
// returns a class object that implements the typeclass/protocol - this is an implementation of the typeclass/protocol for a given type
|
|
464
|
+
get(classConstructor, name) {
|
|
465
|
+
const protocolImplGroup = this.implementations.get(classConstructor);
|
|
466
|
+
if (!protocolImplGroup) {
|
|
467
|
+
throw new Error(`Protocol ${this.name} not implemented by ${classConstructor}`);
|
|
468
|
+
}
|
|
469
|
+
const impl = protocolImplGroup.get(name);
|
|
470
|
+
if (!impl) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Named implementation '${name || protocolImplGroup.defaultImpl}' of protocol ${this.name} not implemented by ${classConstructor}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return impl;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var Protocol = class _Protocol {
|
|
479
|
+
static protocolRegistryLock;
|
|
480
|
+
static registry;
|
|
481
|
+
static {
|
|
482
|
+
_Protocol.protocolRegistryLock = _Protocol.protocolRegistryLock || new Mutex();
|
|
483
|
+
_Protocol.registry = _Protocol.registry || /* @__PURE__ */ new Map();
|
|
484
|
+
}
|
|
485
|
+
static getTypeclass() {
|
|
486
|
+
if (this.registry.has(this.name)) {
|
|
487
|
+
return this.registry.get(this.name);
|
|
488
|
+
} else {
|
|
489
|
+
const newTypeclass = new ImplementationRegistry(this.name);
|
|
490
|
+
this.registry.set(this.name, newTypeclass);
|
|
491
|
+
return newTypeclass;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// static getTypeclass(): ImplementationRegistry {
|
|
495
|
+
// if (global.protocolRegistry.has(this.name)) {
|
|
496
|
+
// return global.protocolRegistry.get(this.name) as ImplementationRegistry;
|
|
497
|
+
// } else {
|
|
498
|
+
// const newTypeclass = new ImplementationRegistry(this.name);
|
|
499
|
+
// global.protocolRegistry.set(this.name, newTypeclass);
|
|
500
|
+
// return newTypeclass;
|
|
501
|
+
// }
|
|
502
|
+
// }
|
|
503
|
+
static async register(classConstructor, implClass, makeDefault = false) {
|
|
504
|
+
await _Protocol.protocolRegistryLock.runExclusive(async () => {
|
|
505
|
+
let typeclass = this.getTypeclass();
|
|
506
|
+
typeclass.register(classConstructor, implClass, makeDefault);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
static async use(classConstructor, implClass) {
|
|
510
|
+
await _Protocol.protocolRegistryLock.runExclusive(async () => {
|
|
511
|
+
let typeclass = this.getTypeclass();
|
|
512
|
+
typeclass.use(classConstructor, implClass);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// returns a class object that implements the typeclass - this is an implementation of the typeclass for a given type
|
|
516
|
+
static get(classConstructor, name) {
|
|
517
|
+
const typeclass = this.getTypeclass();
|
|
518
|
+
return typeclass.get(classConstructor, name);
|
|
519
|
+
}
|
|
520
|
+
// returns an instance of the class that implements the typeclass
|
|
521
|
+
// For example, Enumerable.for([1,2,3]) returns a new instance of the EnumerableArray implementation of the
|
|
522
|
+
// Enumerable typeclass on the Array type, initialized with [1,2,3] in the constructor
|
|
523
|
+
static for(instance, explicitImplClass) {
|
|
524
|
+
let finalImplConstructor;
|
|
525
|
+
if (explicitImplClass) {
|
|
526
|
+
finalImplConstructor = explicitImplClass;
|
|
527
|
+
} else {
|
|
528
|
+
finalImplConstructor = this.get(
|
|
529
|
+
kind(instance)
|
|
530
|
+
// No name needed here for the default lookup via this.get(typeConstructor)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return new finalImplConstructor(instance);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/flex/compactable.ts
|
|
538
|
+
var Compactable = class extends Protocol {
|
|
539
|
+
constructor(self) {
|
|
540
|
+
super();
|
|
541
|
+
this.self = self;
|
|
542
|
+
}
|
|
543
|
+
compact(omit) {
|
|
544
|
+
throw new Error("not implemented");
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
function compact(omit) {
|
|
548
|
+
return function(compactableVal) {
|
|
549
|
+
return Compactable.for(compactableVal).compact(omit);
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/flex/enumerator.ts
|
|
554
|
+
var Enumerator = class _Enumerator {
|
|
555
|
+
constructor(iterator) {
|
|
556
|
+
this.iterator = iterator;
|
|
557
|
+
}
|
|
558
|
+
static for(iterator) {
|
|
559
|
+
return new _Enumerator(iterator);
|
|
560
|
+
}
|
|
561
|
+
all(predFn) {
|
|
562
|
+
for (const elem of this) {
|
|
563
|
+
if (!predFn(elem)) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
any(predFn) {
|
|
570
|
+
for (const elem of this) {
|
|
571
|
+
if (predFn(elem)) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
each(visitorFn) {
|
|
578
|
+
for (const e of this) {
|
|
579
|
+
visitorFn(e);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
first(predFn = identity, defaultIfAbsesnt = void 0) {
|
|
583
|
+
for (const e of this) {
|
|
584
|
+
if (predFn(e)) return e;
|
|
585
|
+
}
|
|
586
|
+
return defaultIfAbsesnt;
|
|
587
|
+
}
|
|
588
|
+
map(mapFn) {
|
|
589
|
+
function* gen(iterator) {
|
|
590
|
+
for (const e of iterator) {
|
|
591
|
+
yield mapFn(e);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return _Enumerator.for(gen(this));
|
|
595
|
+
}
|
|
596
|
+
// per https://typescript.tv/hands-on/all-you-need-to-know-about-iterators-and-generators/#the-iterator-protocol
|
|
597
|
+
// implement the IterableIterator interface by implementing [Symbol.iterator]() and next()
|
|
598
|
+
[Symbol.iterator]() {
|
|
599
|
+
return this;
|
|
600
|
+
}
|
|
601
|
+
// per https://typescript.tv/hands-on/all-you-need-to-know-about-iterators-and-generators/#the-iterator-protocol
|
|
602
|
+
// implement the IterableIterator interface by implementing [Symbol.iterator]() and next()
|
|
603
|
+
next() {
|
|
604
|
+
const val = this.produce();
|
|
605
|
+
if (val instanceof End) {
|
|
606
|
+
return { done: true, value: End.instance };
|
|
607
|
+
}
|
|
608
|
+
return { done: false, value: val };
|
|
609
|
+
}
|
|
610
|
+
// this produces either the next value in the Enumerator, or it returns End
|
|
611
|
+
produce() {
|
|
612
|
+
const val = this.iterator.next();
|
|
613
|
+
if (val.done) {
|
|
614
|
+
return End.instance;
|
|
615
|
+
}
|
|
616
|
+
return val.value;
|
|
617
|
+
}
|
|
618
|
+
select(predFn) {
|
|
619
|
+
function* gen(iterator) {
|
|
620
|
+
for (const e of iterator) {
|
|
621
|
+
if (predFn(e)) yield e;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return _Enumerator.for(gen(this));
|
|
625
|
+
}
|
|
626
|
+
toArray() {
|
|
627
|
+
return Array.from(this);
|
|
628
|
+
}
|
|
629
|
+
toMap(kvMapFn) {
|
|
630
|
+
return new Map(this.map(kvMapFn));
|
|
631
|
+
}
|
|
632
|
+
toSet() {
|
|
633
|
+
return new Set(this);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/flex/enumerable.ts
|
|
638
|
+
var Enumerable = class extends Protocol {
|
|
639
|
+
constructor(self) {
|
|
640
|
+
super();
|
|
641
|
+
this.self = self;
|
|
642
|
+
}
|
|
643
|
+
static each = each;
|
|
644
|
+
each(visitorFn) {
|
|
645
|
+
return this.toEnumerator().each(visitorFn);
|
|
646
|
+
}
|
|
647
|
+
*emit() {
|
|
648
|
+
throw new Error("emit not implemented");
|
|
649
|
+
}
|
|
650
|
+
toArray() {
|
|
651
|
+
return this.toEnumerator().toArray();
|
|
652
|
+
}
|
|
653
|
+
toEnumerator() {
|
|
654
|
+
return Enumerator.for(this.emit());
|
|
655
|
+
}
|
|
656
|
+
toSet() {
|
|
657
|
+
return this.toEnumerator().toSet();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
function each(visitorFn) {
|
|
661
|
+
return function(enumerableVal, implClass) {
|
|
662
|
+
return Enumerable.for(enumerableVal, implClass).each(visitorFn);
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/flex/mappable.ts
|
|
667
|
+
var Mappable = class extends Protocol {
|
|
668
|
+
constructor(self) {
|
|
669
|
+
super();
|
|
670
|
+
this.self = self;
|
|
671
|
+
}
|
|
672
|
+
map(mapFn) {
|
|
673
|
+
throw new Error("not implemented");
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
function map(mapFn) {
|
|
677
|
+
return function(mappableVal, explicitImplClass) {
|
|
678
|
+
return Mappable.for(mappableVal, explicitImplClass).map(mapFn);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
var AsyncMappable = class extends Protocol {
|
|
682
|
+
constructor(self) {
|
|
683
|
+
super();
|
|
684
|
+
this.self = self;
|
|
685
|
+
}
|
|
686
|
+
async map(mapFn) {
|
|
687
|
+
throw new Error("not implemented");
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
function asyncMap(mapFn) {
|
|
691
|
+
return async function(mappableVal, explicitImplClass) {
|
|
692
|
+
return await AsyncMappable.for(mappableVal, explicitImplClass).map(mapFn);
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/flex/selectable.ts
|
|
697
|
+
var Selectable = class extends Protocol {
|
|
698
|
+
constructor(self) {
|
|
699
|
+
super();
|
|
700
|
+
this.self = self;
|
|
701
|
+
}
|
|
702
|
+
select(predFn) {
|
|
703
|
+
throw new Error("not implemented");
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
function select(predFn) {
|
|
707
|
+
return function(selectableVal, implClass) {
|
|
708
|
+
return Selectable.for(selectableVal, implClass).select(predFn);
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/flex/array-protocols.ts
|
|
713
|
+
var CompactableArray = class extends Compactable {
|
|
714
|
+
compact(omit) {
|
|
715
|
+
omit ||= [null, void 0];
|
|
716
|
+
const omitSet = toSet(omit);
|
|
717
|
+
const newArr = [];
|
|
718
|
+
for (const val of this.self) {
|
|
719
|
+
if (!omitSet.has(val)) {
|
|
720
|
+
newArr.push(val);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return newArr;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
await Compactable.register(Array, CompactableArray, true);
|
|
727
|
+
var MappableArray = class extends Mappable {
|
|
728
|
+
map(mapFn) {
|
|
729
|
+
return this.self.map(mapFn);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
await Mappable.register(Array, MappableArray, true);
|
|
733
|
+
var AsyncMappableArray = class extends AsyncMappable {
|
|
734
|
+
async map(mapFn) {
|
|
735
|
+
const arr = [];
|
|
736
|
+
for await (const v of this.self) {
|
|
737
|
+
const mappedValue = await mapFn(v);
|
|
738
|
+
arr.push(mappedValue);
|
|
739
|
+
}
|
|
740
|
+
return arr;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
await AsyncMappable.register(Array, AsyncMappableArray, true);
|
|
744
|
+
var EnumerableArray = class extends Enumerable {
|
|
745
|
+
*emit() {
|
|
746
|
+
for (const e of this.self) yield e;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
await Enumerable.register(Array, EnumerableArray, true);
|
|
750
|
+
var SelectableArray = class extends Selectable {
|
|
751
|
+
select(predFn) {
|
|
752
|
+
return this.self.filter(predFn);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
await Selectable.register(Array, SelectableArray, true);
|
|
756
|
+
|
|
757
|
+
// src/flex/map.ts
|
|
758
|
+
function each2(eachFn) {
|
|
759
|
+
return function(map6) {
|
|
760
|
+
map6.forEach((value, key) => {
|
|
761
|
+
eachFn([key, value]);
|
|
762
|
+
});
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function isEmpty(map6) {
|
|
766
|
+
return map6.size === 0;
|
|
767
|
+
}
|
|
768
|
+
function keys(map6) {
|
|
769
|
+
return Array.from(map6.keys());
|
|
770
|
+
}
|
|
771
|
+
function map2(mapFn) {
|
|
772
|
+
return function(map6) {
|
|
773
|
+
const m = /* @__PURE__ */ new Map();
|
|
774
|
+
map6.forEach((value, key) => {
|
|
775
|
+
const [newKey, newValue] = mapFn([key, value]);
|
|
776
|
+
m.set(newKey, newValue);
|
|
777
|
+
});
|
|
778
|
+
return m;
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function select2(predFn) {
|
|
782
|
+
return function(map6) {
|
|
783
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
784
|
+
map6.forEach((value, key) => {
|
|
785
|
+
if (predFn([key, value])) {
|
|
786
|
+
newMap.set(key, value);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
return newMap;
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function toObject(map6) {
|
|
793
|
+
return Object.fromEntries(map6.entries());
|
|
794
|
+
}
|
|
795
|
+
function values(map6) {
|
|
796
|
+
return Array.from(map6.values());
|
|
797
|
+
}
|
|
798
|
+
var M = buildWrapperProxy();
|
|
799
|
+
M.registerUncurriedFns({ isEmpty, keys, toObject, values });
|
|
800
|
+
M.registerCurriedFns({ each: each2, map: map2, select: select2 });
|
|
801
|
+
M.registerStatic({
|
|
802
|
+
each: each2,
|
|
803
|
+
isEmpty,
|
|
804
|
+
keys,
|
|
805
|
+
map: map2,
|
|
806
|
+
select: select2,
|
|
807
|
+
toObject,
|
|
808
|
+
values
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// src/flex/map-protocols.ts
|
|
812
|
+
var MapToMap = class extends Mappable {
|
|
813
|
+
map(mapFn) {
|
|
814
|
+
const m = /* @__PURE__ */ new Map();
|
|
815
|
+
this.self.forEach((value, key) => {
|
|
816
|
+
const [newKey, newValue] = mapFn([key, value]);
|
|
817
|
+
m.set(newKey, newValue);
|
|
818
|
+
});
|
|
819
|
+
return m;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
var MapToArray = class extends Mappable {
|
|
823
|
+
map(mapFn) {
|
|
824
|
+
const arr = [];
|
|
825
|
+
this.self.forEach((value, key) => {
|
|
826
|
+
const mappedValue = mapFn([key, value]);
|
|
827
|
+
arr.push(mappedValue);
|
|
828
|
+
});
|
|
829
|
+
return arr;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
await Mappable.register(Map, MapToMap, true);
|
|
833
|
+
await Mappable.register(Map, MapToArray);
|
|
834
|
+
var AsyncMapToMap = class extends AsyncMappable {
|
|
835
|
+
async map(mapFn) {
|
|
836
|
+
const m = /* @__PURE__ */ new Map();
|
|
837
|
+
for await (const [key, value] of this.self) {
|
|
838
|
+
const [newKey, newValue] = await mapFn([key, value]);
|
|
839
|
+
m.set(newKey, newValue);
|
|
840
|
+
}
|
|
841
|
+
return m;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var AsyncMapToArray = class extends AsyncMappable {
|
|
845
|
+
async map(mapFn) {
|
|
846
|
+
const arr = [];
|
|
847
|
+
for await (const [key, value] of this.self) {
|
|
848
|
+
const mappedValue = await mapFn([key, value]);
|
|
849
|
+
arr.push(mappedValue);
|
|
850
|
+
}
|
|
851
|
+
return arr;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
await AsyncMappable.register(Map, AsyncMapToMap, true);
|
|
855
|
+
await AsyncMappable.register(Map, AsyncMapToArray);
|
|
856
|
+
var EnumerablePair = class extends Enumerable {
|
|
857
|
+
*emit() {
|
|
858
|
+
for (const [k, v] of this.self.entries()) {
|
|
859
|
+
yield [k, v];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
var EnumerableKey = class extends Enumerable {
|
|
864
|
+
*emit() {
|
|
865
|
+
for (const k of this.self.keys()) {
|
|
866
|
+
yield k;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var EnumerableValue = class extends Enumerable {
|
|
871
|
+
*emit() {
|
|
872
|
+
for (const v of this.self.values()) {
|
|
873
|
+
yield v;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
await Enumerable.register(Map, EnumerablePair, true);
|
|
878
|
+
await Enumerable.register(Map, EnumerableKey);
|
|
879
|
+
await Enumerable.register(Map, EnumerableValue);
|
|
880
|
+
var SelectableMap = class extends Selectable {
|
|
881
|
+
select(predFn) {
|
|
882
|
+
return M(this.self).select(predFn);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
await Selectable.register(Map, SelectableMap, true);
|
|
886
|
+
|
|
887
|
+
// src/flex/object.ts
|
|
888
|
+
function each3(eachFn) {
|
|
889
|
+
return function(object) {
|
|
890
|
+
Object.entries(object).forEach((kvPair) => {
|
|
891
|
+
eachFn(kvPair);
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function keys2(object) {
|
|
896
|
+
return Object.keys(object);
|
|
897
|
+
}
|
|
898
|
+
function map3(mapFn) {
|
|
899
|
+
return function(object) {
|
|
900
|
+
const obj = {};
|
|
901
|
+
Object.entries(object).forEach((kvPair) => {
|
|
902
|
+
const [newKey, newValue] = mapFn(kvPair);
|
|
903
|
+
obj[newKey] = newValue;
|
|
904
|
+
});
|
|
905
|
+
return obj;
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function pick(keysToPick) {
|
|
909
|
+
return select3(([key, _]) => keysToPick.has(key));
|
|
910
|
+
}
|
|
911
|
+
function select3(predFn) {
|
|
912
|
+
return function(object) {
|
|
913
|
+
const obj = {};
|
|
914
|
+
Object.entries(object).forEach((kvPair) => {
|
|
915
|
+
if (predFn(kvPair)) {
|
|
916
|
+
const [k, v] = kvPair;
|
|
917
|
+
obj[k] = v;
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return obj;
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function toMap(object) {
|
|
924
|
+
return new Map(Object.entries(object));
|
|
925
|
+
}
|
|
926
|
+
function values2(object) {
|
|
927
|
+
return Object.values(object);
|
|
928
|
+
}
|
|
929
|
+
var O = buildWrapperProxy();
|
|
930
|
+
O.registerUncurriedFns({ keys: keys2, toMap, values: values2 });
|
|
931
|
+
O.registerCurriedFns({ each: each3, map: map3, pick, select: select3 });
|
|
932
|
+
O.registerStatic({
|
|
933
|
+
each: each3,
|
|
934
|
+
// (eachFn)(obj) => ...
|
|
935
|
+
keys: keys2,
|
|
936
|
+
// (obj) => ...
|
|
937
|
+
map: map3,
|
|
938
|
+
pick,
|
|
939
|
+
select: select3,
|
|
940
|
+
toMap,
|
|
941
|
+
values: values2
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// src/flex/object-protocols.ts
|
|
945
|
+
var SelectableObject = class extends Selectable {
|
|
946
|
+
select(predFn) {
|
|
947
|
+
return O(this.self).select(predFn);
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
await Selectable.register(Object, SelectableObject, true);
|
|
951
|
+
|
|
952
|
+
// src/flex/path.ts
|
|
953
|
+
import { copyFile, cp } from "fs/promises";
|
|
954
|
+
import { existsSync, lstatSync } from "fs";
|
|
955
|
+
import { win32, posix } from "path";
|
|
956
|
+
import { homedir } from "os";
|
|
957
|
+
import { globSync } from "glob";
|
|
958
|
+
|
|
959
|
+
// src/flex/platform.ts
|
|
960
|
+
import { platform as nodePlatform } from "process";
|
|
961
|
+
function isWindows() {
|
|
962
|
+
return platform() == "win32";
|
|
963
|
+
}
|
|
964
|
+
function platform() {
|
|
965
|
+
return nodePlatform;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/flex/string.ts
|
|
969
|
+
function isEmpty2(str) {
|
|
970
|
+
return str.length == 0;
|
|
971
|
+
}
|
|
972
|
+
function isNumeric(str) {
|
|
973
|
+
if (typeof str !== "string") return false;
|
|
974
|
+
const trimmedStr = str.trim();
|
|
975
|
+
if (trimmedStr === "") return false;
|
|
976
|
+
const num = Number(trimmedStr);
|
|
977
|
+
return !isNaN(num) && isFinite(num) && String(num) === trimmedStr;
|
|
978
|
+
}
|
|
979
|
+
function matches(regexp) {
|
|
980
|
+
const globalRegex = regexp.global ? regexp : new RegExp(regexp.source, regexp.flags + "g");
|
|
981
|
+
return function(str) {
|
|
982
|
+
const matches2 = [...str.matchAll(globalRegex)];
|
|
983
|
+
if (matches2.length == 0) return null;
|
|
984
|
+
return matches2;
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
var StringProxy = class {
|
|
988
|
+
constructor(str) {
|
|
989
|
+
this.str = str;
|
|
990
|
+
}
|
|
991
|
+
// Add common string properties/methods for TypeScript
|
|
992
|
+
get length() {
|
|
993
|
+
return this.str.length;
|
|
994
|
+
}
|
|
995
|
+
toUpperCase() {
|
|
996
|
+
return this.str.toUpperCase();
|
|
997
|
+
}
|
|
998
|
+
isEmpty() {
|
|
999
|
+
return isEmpty2(this.str);
|
|
1000
|
+
}
|
|
1001
|
+
isNumeric() {
|
|
1002
|
+
return isNumeric(this.str);
|
|
1003
|
+
}
|
|
1004
|
+
matches(regexp) {
|
|
1005
|
+
return matches(regexp)(this.str);
|
|
1006
|
+
}
|
|
1007
|
+
trimPrefix(prefix) {
|
|
1008
|
+
if (this.str.startsWith(prefix)) {
|
|
1009
|
+
return this.str.slice(prefix.length);
|
|
1010
|
+
}
|
|
1011
|
+
return this.str;
|
|
1012
|
+
}
|
|
1013
|
+
trimSuffix(suffix) {
|
|
1014
|
+
if (this.str.endsWith(suffix)) {
|
|
1015
|
+
return this.str.slice(0, this.str.length - suffix.length);
|
|
1016
|
+
}
|
|
1017
|
+
return this.str;
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
var Str = function(str) {
|
|
1021
|
+
const handler = {
|
|
1022
|
+
get(target, property, receiver) {
|
|
1023
|
+
const propOnTarget = Reflect.get(target, property, receiver);
|
|
1024
|
+
if (V.isPresent(propOnTarget)) {
|
|
1025
|
+
return propOnTarget;
|
|
1026
|
+
}
|
|
1027
|
+
const underlyingString = target.str;
|
|
1028
|
+
const valueFromPrimitive = underlyingString[property];
|
|
1029
|
+
if (typeof valueFromPrimitive === "function") {
|
|
1030
|
+
return valueFromPrimitive.bind(underlyingString);
|
|
1031
|
+
}
|
|
1032
|
+
return valueFromPrimitive;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
return new Proxy(new StringProxy(str), handler);
|
|
1036
|
+
};
|
|
1037
|
+
Object.assign(Str, {
|
|
1038
|
+
isEmpty: isEmpty2,
|
|
1039
|
+
isNumeric,
|
|
1040
|
+
matches
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// src/flex/range.ts
|
|
1044
|
+
var Range = class _Range {
|
|
1045
|
+
constructor(start, end, inclusive = true) {
|
|
1046
|
+
this.start = start;
|
|
1047
|
+
this.end = end;
|
|
1048
|
+
this.inclusive = inclusive;
|
|
1049
|
+
}
|
|
1050
|
+
static new(start, end, inclusive = true) {
|
|
1051
|
+
return new _Range(start, end, inclusive);
|
|
1052
|
+
}
|
|
1053
|
+
each(visitorFn) {
|
|
1054
|
+
const exclusiveEnd = this.inclusive ? this.end + 1 : this.end;
|
|
1055
|
+
for (let i = this.start; i < exclusiveEnd; i++) {
|
|
1056
|
+
visitorFn(i);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
equals(other) {
|
|
1060
|
+
if (!(other instanceof _Range)) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
return this.start === other.start && this.end === other.end && this.inclusive === other.inclusive;
|
|
1064
|
+
}
|
|
1065
|
+
map(mapFn) {
|
|
1066
|
+
const arr = [];
|
|
1067
|
+
this.each((num) => {
|
|
1068
|
+
arr.push(mapFn(num));
|
|
1069
|
+
});
|
|
1070
|
+
return arr;
|
|
1071
|
+
}
|
|
1072
|
+
toArray() {
|
|
1073
|
+
return Enumerable.for(this).toArray();
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// src/flex/equal.ts
|
|
1078
|
+
var Equal = class extends Protocol {
|
|
1079
|
+
constructor(self) {
|
|
1080
|
+
super();
|
|
1081
|
+
this.self = self;
|
|
1082
|
+
}
|
|
1083
|
+
equal(val) {
|
|
1084
|
+
throw new Error("equal not implemented");
|
|
1085
|
+
}
|
|
1086
|
+
notEqual(val) {
|
|
1087
|
+
return !this.equal(val);
|
|
1088
|
+
}
|
|
1089
|
+
eq(val) {
|
|
1090
|
+
return this.equal(val);
|
|
1091
|
+
}
|
|
1092
|
+
neq(val) {
|
|
1093
|
+
return this.notEqual(val);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// src/flex/path.ts
|
|
1098
|
+
var Path = class _Path {
|
|
1099
|
+
constructor(path3, isWindowsPath = isWindows()) {
|
|
1100
|
+
this.path = path3;
|
|
1101
|
+
this.isWindowsPath = isWindowsPath;
|
|
1102
|
+
}
|
|
1103
|
+
static new(path3, isWindowsPath = isWindows()) {
|
|
1104
|
+
if (path3 instanceof _Path) {
|
|
1105
|
+
return path3;
|
|
1106
|
+
}
|
|
1107
|
+
return new _Path(path3, isWindowsPath);
|
|
1108
|
+
}
|
|
1109
|
+
static cwd() {
|
|
1110
|
+
return _Path.new(process.cwd());
|
|
1111
|
+
}
|
|
1112
|
+
static homeDir() {
|
|
1113
|
+
return _Path.new(homedir());
|
|
1114
|
+
}
|
|
1115
|
+
static sep(isWindowsPath = isWindows()) {
|
|
1116
|
+
if (isWindowsPath) {
|
|
1117
|
+
return win32.sep;
|
|
1118
|
+
} else {
|
|
1119
|
+
return posix.sep;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
absolute() {
|
|
1123
|
+
return this.resolve();
|
|
1124
|
+
}
|
|
1125
|
+
ancestors() {
|
|
1126
|
+
if (this.isRoot()) {
|
|
1127
|
+
return [];
|
|
1128
|
+
}
|
|
1129
|
+
const ancestors = [];
|
|
1130
|
+
let current = this.directory();
|
|
1131
|
+
while (true) {
|
|
1132
|
+
ancestors.push(current);
|
|
1133
|
+
if (current.isRoot()) {
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
current = current.parent();
|
|
1137
|
+
}
|
|
1138
|
+
return ancestors;
|
|
1139
|
+
}
|
|
1140
|
+
basename(suffix) {
|
|
1141
|
+
if (this.isWindowsPath) {
|
|
1142
|
+
return this.build(win32.basename(this.path, suffix));
|
|
1143
|
+
} else {
|
|
1144
|
+
return this.build(posix.basename(this.path, suffix));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
build(path3) {
|
|
1148
|
+
return new _Path(path3, this.isWindowsPath);
|
|
1149
|
+
}
|
|
1150
|
+
// returns the path to the destination on success; null otherwise
|
|
1151
|
+
async copy(destPath, mode) {
|
|
1152
|
+
destPath = _Path.new(destPath).normalize();
|
|
1153
|
+
try {
|
|
1154
|
+
if (this.isDirectory()) {
|
|
1155
|
+
await cp(this.toString(), destPath.toString(), { mode, recursive: true });
|
|
1156
|
+
} else {
|
|
1157
|
+
await copyFile(this.toString(), destPath.toString(), mode);
|
|
1158
|
+
}
|
|
1159
|
+
return destPath;
|
|
1160
|
+
} catch (e) {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
dirContains(filename) {
|
|
1165
|
+
return this.isDirectory() && this.glob(filename).length > 0;
|
|
1166
|
+
}
|
|
1167
|
+
directory() {
|
|
1168
|
+
return this.absolute().dirname();
|
|
1169
|
+
}
|
|
1170
|
+
// given a path like "bar/bas", this method converts the path to an absolute path (e.g. "/foo/bar/bas"),
|
|
1171
|
+
// and then returns the directory tree as an array of the form ["foo", "bar", "bas"]
|
|
1172
|
+
directorySegments() {
|
|
1173
|
+
return this.absolute().dirname().split();
|
|
1174
|
+
}
|
|
1175
|
+
dirname() {
|
|
1176
|
+
if (this.isWindowsPath) {
|
|
1177
|
+
return this.build(win32.dirname(this.path));
|
|
1178
|
+
} else {
|
|
1179
|
+
return this.build(posix.dirname(this.path));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
equals(other) {
|
|
1183
|
+
if (!(other instanceof _Path)) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
return this.path === other.path && this.isWindowsPath === other.isWindowsPath;
|
|
1187
|
+
}
|
|
1188
|
+
exists() {
|
|
1189
|
+
return existsSync(this.path);
|
|
1190
|
+
}
|
|
1191
|
+
ext() {
|
|
1192
|
+
return this.parse().ext;
|
|
1193
|
+
}
|
|
1194
|
+
glob(pattern) {
|
|
1195
|
+
const cwd = this.absolute().toString();
|
|
1196
|
+
return globSync(pattern, { cwd }).map((path3) => this.build(path3));
|
|
1197
|
+
}
|
|
1198
|
+
isAbsolute() {
|
|
1199
|
+
if (this.isWindowsPath) {
|
|
1200
|
+
return win32.isAbsolute(this.path);
|
|
1201
|
+
} else {
|
|
1202
|
+
return posix.isAbsolute(this.path);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
isDirectory() {
|
|
1206
|
+
return this.exists() && lstatSync(this.path).isDirectory();
|
|
1207
|
+
}
|
|
1208
|
+
isFile() {
|
|
1209
|
+
return this.exists() && lstatSync(this.path).isFile();
|
|
1210
|
+
}
|
|
1211
|
+
isPosix() {
|
|
1212
|
+
return !this.isWindowsPath;
|
|
1213
|
+
}
|
|
1214
|
+
isRoot() {
|
|
1215
|
+
return this.parse().root === this.normalize().toString();
|
|
1216
|
+
}
|
|
1217
|
+
isWindows() {
|
|
1218
|
+
return this.isWindowsPath;
|
|
1219
|
+
}
|
|
1220
|
+
join(...paths) {
|
|
1221
|
+
if (this.isWindowsPath) {
|
|
1222
|
+
return this.build(win32.join(this.path, ...paths));
|
|
1223
|
+
} else {
|
|
1224
|
+
return this.build(posix.join(this.path, ...paths));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
normalize() {
|
|
1228
|
+
if (this.isWindowsPath) {
|
|
1229
|
+
return this.build(win32.normalize(this.path));
|
|
1230
|
+
} else {
|
|
1231
|
+
return this.build(posix.normalize(this.path));
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
parent(count = 1) {
|
|
1235
|
+
let path3 = this.absolute();
|
|
1236
|
+
Range.new(1, count).each((i) => {
|
|
1237
|
+
path3 = path3.resolve("..");
|
|
1238
|
+
});
|
|
1239
|
+
return path3;
|
|
1240
|
+
}
|
|
1241
|
+
// returns an object of the form: { root, dir, base, ext, name }
|
|
1242
|
+
//
|
|
1243
|
+
// Posix:
|
|
1244
|
+
// >>> path.parse('/home/user/dir/file.txt')
|
|
1245
|
+
// { root: '/',
|
|
1246
|
+
// dir: '/home/user/dir',
|
|
1247
|
+
// base: 'file.txt',
|
|
1248
|
+
// ext: '.txt',
|
|
1249
|
+
// name: 'file' }
|
|
1250
|
+
// ┌─────────────────────┬────────────┐
|
|
1251
|
+
// │ dir │ base │
|
|
1252
|
+
// ├──────┬ ├──────┬─────┤
|
|
1253
|
+
// │ root │ │ name │ ext │
|
|
1254
|
+
// " / home/user/dir / file .txt "
|
|
1255
|
+
// └──────┴──────────────┴──────┴─────┘
|
|
1256
|
+
// (All spaces in the "" line should be ignored. They are purely for formatting.)
|
|
1257
|
+
//
|
|
1258
|
+
// Windows:
|
|
1259
|
+
// >>> path.parse('C:\\path\\dir\\file.txt');
|
|
1260
|
+
// { root: 'C:\\',
|
|
1261
|
+
// dir: 'C:\\path\\dir',
|
|
1262
|
+
// base: 'file.txt',
|
|
1263
|
+
// ext: '.txt',
|
|
1264
|
+
// name: 'file' }
|
|
1265
|
+
// ┌─────────────────────┬────────────┐
|
|
1266
|
+
// │ dir │ base │
|
|
1267
|
+
// ├──────┬ ├──────┬─────┤
|
|
1268
|
+
// │ root │ │ name │ ext │
|
|
1269
|
+
// " C:\ path\dir \ file .txt "
|
|
1270
|
+
// └──────┴──────────────┴──────┴─────┘
|
|
1271
|
+
// (All spaces in the "" line should be ignored. They are purely for formatting.)
|
|
1272
|
+
parse() {
|
|
1273
|
+
if (this.isWindowsPath) {
|
|
1274
|
+
return win32.parse(this.path);
|
|
1275
|
+
} else {
|
|
1276
|
+
return posix.parse(this.path);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
pop(count = 1) {
|
|
1280
|
+
return this.parent(count);
|
|
1281
|
+
}
|
|
1282
|
+
relative(to) {
|
|
1283
|
+
if (this.isWindowsPath) {
|
|
1284
|
+
return this.build(win32.relative(this.path, to));
|
|
1285
|
+
} else {
|
|
1286
|
+
return this.build(posix.relative(this.path, to));
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
resolve(...paths) {
|
|
1290
|
+
if (this.isWindowsPath) {
|
|
1291
|
+
return this.build(win32.resolve(this.path, ...paths));
|
|
1292
|
+
} else {
|
|
1293
|
+
return this.build(posix.resolve(this.path, ...paths));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
root() {
|
|
1297
|
+
const { root } = this.parse();
|
|
1298
|
+
return this.build(root);
|
|
1299
|
+
}
|
|
1300
|
+
selfAndAncestors() {
|
|
1301
|
+
const ancestors = [];
|
|
1302
|
+
let current = this.absolute();
|
|
1303
|
+
while (true) {
|
|
1304
|
+
ancestors.push(current);
|
|
1305
|
+
if (current.isRoot()) {
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
current = current.parent();
|
|
1309
|
+
}
|
|
1310
|
+
return ancestors;
|
|
1311
|
+
}
|
|
1312
|
+
// returns the parts of the path, excluding the root if applicable
|
|
1313
|
+
// /home/user/dir/file.txt -> ["home", "user", "dir", "file.txt"]
|
|
1314
|
+
// C:\home\user\dir\file.txt -> ["home", "user", "dir", "file.txt"]
|
|
1315
|
+
// user/dir/file.txt -> ["user", "dir", "file.txt"]
|
|
1316
|
+
// user\dir\file.txt -> ["user", "dir", "file.txt"]
|
|
1317
|
+
split() {
|
|
1318
|
+
const normalized = this.normalize();
|
|
1319
|
+
if (normalized.isAbsolute()) {
|
|
1320
|
+
const { root } = normalized.parse();
|
|
1321
|
+
const relPath = Str(normalized.toString()).trimPrefix(root);
|
|
1322
|
+
return relPath.split(_Path.sep(this.isWindowsPath));
|
|
1323
|
+
} else {
|
|
1324
|
+
return normalized.toString().split(_Path.sep(this.isWindowsPath));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
toString() {
|
|
1328
|
+
return this.path;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
var PathEqualImpl = class extends Equal {
|
|
1332
|
+
equal(val) {
|
|
1333
|
+
return this.self.equals(val);
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
await Equal.register(Path, PathEqualImpl, true);
|
|
1337
|
+
|
|
1338
|
+
// src/flex/range-protocols.ts
|
|
1339
|
+
var MappableRange = class extends Mappable {
|
|
1340
|
+
map(mapFn) {
|
|
1341
|
+
return this.self.map(mapFn);
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
await Mappable.register(Range, MappableRange, true);
|
|
1345
|
+
var EnumerableRange = class extends Enumerable {
|
|
1346
|
+
*emit() {
|
|
1347
|
+
const exclusiveEnd = this.self.inclusive ? this.self.end + 1 : this.self.end;
|
|
1348
|
+
for (let i = this.self.start; i < exclusiveEnd; i++) {
|
|
1349
|
+
yield i;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
await Enumerable.register(Range, EnumerableRange, true);
|
|
1354
|
+
var EqualRange = class extends Equal {
|
|
1355
|
+
equal(other) {
|
|
1356
|
+
return this.self.equals(other);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
await Equal.register(Range, EqualRange, true);
|
|
1360
|
+
var SelectableRange = class extends Selectable {
|
|
1361
|
+
select(predFn) {
|
|
1362
|
+
let arr = [];
|
|
1363
|
+
this.self.each((num) => {
|
|
1364
|
+
if (predFn(num)) {
|
|
1365
|
+
arr.push(num);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
return arr;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
await Selectable.register(Range, SelectableRange, true);
|
|
1372
|
+
|
|
1373
|
+
// src/flex/set-protocols.ts
|
|
1374
|
+
var MappableSet = class extends Mappable {
|
|
1375
|
+
map(mapFn) {
|
|
1376
|
+
const s = /* @__PURE__ */ new Set();
|
|
1377
|
+
this.self.forEach((value) => {
|
|
1378
|
+
const newValue = mapFn(value);
|
|
1379
|
+
s.add(newValue);
|
|
1380
|
+
});
|
|
1381
|
+
return s;
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
await Mappable.register(Set, MappableSet, true);
|
|
1385
|
+
var EnumerableSet = class extends Enumerable {
|
|
1386
|
+
// each(visitorFn: (v: T) => any): any {
|
|
1387
|
+
// return this.self.forEach((v) => visitorFn(v));
|
|
1388
|
+
// }
|
|
1389
|
+
*emit() {
|
|
1390
|
+
for (const e of this.self) {
|
|
1391
|
+
yield e;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
await Enumerable.register(Set, EnumerableSet, true);
|
|
1396
|
+
|
|
1397
|
+
// src/flex/array.ts
|
|
1398
|
+
function compact2(omit = [null, void 0]) {
|
|
1399
|
+
return function(arr) {
|
|
1400
|
+
const omitSet = toSet(omit);
|
|
1401
|
+
const newArr = [];
|
|
1402
|
+
for (const val of arr) {
|
|
1403
|
+
if (!omitSet.has(val)) {
|
|
1404
|
+
newArr.push(val);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return newArr;
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function concat(tail) {
|
|
1411
|
+
return function(arr) {
|
|
1412
|
+
return arr.concat(tail);
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function each4(eachFn) {
|
|
1416
|
+
return function(arr) {
|
|
1417
|
+
arr.forEach((value) => {
|
|
1418
|
+
eachFn(value);
|
|
1419
|
+
});
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function find(predicateFn) {
|
|
1423
|
+
return function(arr) {
|
|
1424
|
+
for (const val of arr) {
|
|
1425
|
+
if (predicateFn(val)) {
|
|
1426
|
+
return val;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return void 0;
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function first(n) {
|
|
1433
|
+
return function(arr) {
|
|
1434
|
+
return arr.slice(0, n);
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function head(arr) {
|
|
1438
|
+
return arr[0];
|
|
1439
|
+
}
|
|
1440
|
+
function isEmpty3(arr) {
|
|
1441
|
+
return arr.length === 0;
|
|
1442
|
+
}
|
|
1443
|
+
function join(separator) {
|
|
1444
|
+
return function(arr) {
|
|
1445
|
+
return arr.join(separator);
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function last(n) {
|
|
1449
|
+
return function(arr) {
|
|
1450
|
+
return arr.slice(arr.length - n, arr.length);
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function map4(mapFn) {
|
|
1454
|
+
return function(arr) {
|
|
1455
|
+
return arr.map(mapFn);
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function nth(n) {
|
|
1459
|
+
return function(arr) {
|
|
1460
|
+
return arr[n];
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
function partition(predFn) {
|
|
1464
|
+
return function(arr) {
|
|
1465
|
+
const trueArr = [];
|
|
1466
|
+
const falseArr = [];
|
|
1467
|
+
for (const val of arr) {
|
|
1468
|
+
if (predFn(val)) {
|
|
1469
|
+
trueArr.push(val);
|
|
1470
|
+
} else {
|
|
1471
|
+
falseArr.push(val);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return [trueArr, falseArr];
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function prepend(...values3) {
|
|
1478
|
+
return function(arr) {
|
|
1479
|
+
arr.splice(0, 0, ...values3);
|
|
1480
|
+
return arr;
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function select4(predFn) {
|
|
1484
|
+
return function(arr) {
|
|
1485
|
+
return arr.filter(predFn);
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function skipFirst(n) {
|
|
1489
|
+
return function(arr) {
|
|
1490
|
+
if (n > arr.length) {
|
|
1491
|
+
n = arr.length;
|
|
1492
|
+
}
|
|
1493
|
+
return arr.slice(n, arr.length);
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function skipLast(n) {
|
|
1497
|
+
return function(arr) {
|
|
1498
|
+
if (n > arr.length) {
|
|
1499
|
+
n = arr.length;
|
|
1500
|
+
}
|
|
1501
|
+
return arr.slice(0, arr.length - n);
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function toSet(arr) {
|
|
1505
|
+
return new Set(arr);
|
|
1506
|
+
}
|
|
1507
|
+
function uniq(arr) {
|
|
1508
|
+
return uniqBy(identity)(arr);
|
|
1509
|
+
}
|
|
1510
|
+
function uniqBy(comparisonValueFn) {
|
|
1511
|
+
return function(arr) {
|
|
1512
|
+
const map6 = /* @__PURE__ */ new Map();
|
|
1513
|
+
for (const val of arr) {
|
|
1514
|
+
const key = comparisonValueFn(val);
|
|
1515
|
+
if (!map6.has(key)) {
|
|
1516
|
+
map6.set(key, val);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return Array.from(map6.values());
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function zip(other) {
|
|
1523
|
+
return function(arr) {
|
|
1524
|
+
var len = Math.min(arr.length, other.length);
|
|
1525
|
+
var result = Array(len);
|
|
1526
|
+
var idx = 0;
|
|
1527
|
+
while (idx < len) {
|
|
1528
|
+
result[idx] = [arr[idx], other[idx]];
|
|
1529
|
+
idx += 1;
|
|
1530
|
+
}
|
|
1531
|
+
return result;
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
var A2 = buildWrapperProxy();
|
|
1535
|
+
A2.registerUncurriedFns({ head, isEmpty: isEmpty3, toSet, uniq });
|
|
1536
|
+
A2.registerCurriedFns({
|
|
1537
|
+
compact: compact2,
|
|
1538
|
+
concat,
|
|
1539
|
+
each: each4,
|
|
1540
|
+
filter: select4,
|
|
1541
|
+
find,
|
|
1542
|
+
first,
|
|
1543
|
+
join,
|
|
1544
|
+
last,
|
|
1545
|
+
map: map4,
|
|
1546
|
+
nth,
|
|
1547
|
+
partition,
|
|
1548
|
+
prepend,
|
|
1549
|
+
select: select4,
|
|
1550
|
+
skipFirst,
|
|
1551
|
+
skipLast,
|
|
1552
|
+
uniqBy,
|
|
1553
|
+
zip
|
|
1554
|
+
});
|
|
1555
|
+
A2.registerStatic({
|
|
1556
|
+
compact: compact2,
|
|
1557
|
+
concat,
|
|
1558
|
+
each: each4,
|
|
1559
|
+
filter: select4,
|
|
1560
|
+
find,
|
|
1561
|
+
first,
|
|
1562
|
+
head,
|
|
1563
|
+
isEmpty: isEmpty3,
|
|
1564
|
+
join,
|
|
1565
|
+
last,
|
|
1566
|
+
map: map4,
|
|
1567
|
+
nth,
|
|
1568
|
+
partition,
|
|
1569
|
+
prepend,
|
|
1570
|
+
select: select4,
|
|
1571
|
+
skipFirst,
|
|
1572
|
+
skipLast,
|
|
1573
|
+
toSet,
|
|
1574
|
+
uniq,
|
|
1575
|
+
uniqBy,
|
|
1576
|
+
zip
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
// src/flex/set.ts
|
|
1580
|
+
function addAll(enumerableVal) {
|
|
1581
|
+
const enumerable = Enumerable.for(enumerableVal);
|
|
1582
|
+
return function(set) {
|
|
1583
|
+
enumerable.each((item) => set.add(item));
|
|
1584
|
+
return set;
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function each5(eachFn) {
|
|
1588
|
+
return function(set) {
|
|
1589
|
+
set.forEach((value) => {
|
|
1590
|
+
eachFn(value);
|
|
1591
|
+
});
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function intersection(enumerableVal) {
|
|
1595
|
+
const enumerable = Enumerable.for(enumerableVal);
|
|
1596
|
+
return function(set) {
|
|
1597
|
+
return set.intersection(enumerable.toSet());
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function isEmpty4(set) {
|
|
1601
|
+
return set.size === 0;
|
|
1602
|
+
}
|
|
1603
|
+
function isSubsetOf(enumerableSuperset) {
|
|
1604
|
+
const enumerable = Enumerable.for(enumerableSuperset);
|
|
1605
|
+
return function(set) {
|
|
1606
|
+
return set.isSubsetOf(enumerable.toSet());
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function isSupersetOf(enumerableSubset) {
|
|
1610
|
+
const enumerable = Enumerable.for(enumerableSubset);
|
|
1611
|
+
return function(set) {
|
|
1612
|
+
return set.isSupersetOf(enumerable.toSet());
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function map5(mapFn) {
|
|
1616
|
+
return function(set) {
|
|
1617
|
+
const s = /* @__PURE__ */ new Set();
|
|
1618
|
+
set.forEach((value) => {
|
|
1619
|
+
const newValue = mapFn(value);
|
|
1620
|
+
s.add(newValue);
|
|
1621
|
+
});
|
|
1622
|
+
return s;
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function toArray(set) {
|
|
1626
|
+
return Array.from(set.values());
|
|
1627
|
+
}
|
|
1628
|
+
function union(enumerableVal) {
|
|
1629
|
+
const enumerable = Enumerable.for(enumerableVal);
|
|
1630
|
+
return function(set) {
|
|
1631
|
+
return set.union(enumerable.toSet());
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
var S = buildWrapperProxy();
|
|
1635
|
+
S.registerUncurriedFns({ isEmpty: isEmpty4, toArray });
|
|
1636
|
+
S.registerCurriedFns({ addAll, each: each5, intersection, map: map5, union, isSubsetOf, isSupersetOf });
|
|
1637
|
+
S.registerStatic({
|
|
1638
|
+
addAll,
|
|
1639
|
+
each: each5,
|
|
1640
|
+
intersection,
|
|
1641
|
+
isEmpty: isEmpty4,
|
|
1642
|
+
isSubsetOf,
|
|
1643
|
+
isSupersetOf,
|
|
1644
|
+
map: map5,
|
|
1645
|
+
toArray,
|
|
1646
|
+
union
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// src/flex/pipe.ts
|
|
1650
|
+
var Pipe = class {
|
|
1651
|
+
constructor(value) {
|
|
1652
|
+
this.value = value;
|
|
1653
|
+
}
|
|
1654
|
+
// e.g. convertMethodsToPipeline(A([1,2,3]))
|
|
1655
|
+
// returns a function that proxies method invocations through to the A([1,2,3]) object but assigns their return value
|
|
1656
|
+
// to this.value in the pipeline, and then returns the pipe object, enabling stuff like this:
|
|
1657
|
+
// P([1,2,3]).A.compact().A.first().value
|
|
1658
|
+
_convertMethodsToPipeline(wrappedValue, nameOfMethodBag) {
|
|
1659
|
+
const self = this;
|
|
1660
|
+
return new Proxy(wrappedValue, {
|
|
1661
|
+
get(target, property, receiver) {
|
|
1662
|
+
const fn = Reflect.get(target, property, receiver);
|
|
1663
|
+
if (fn === void 0) {
|
|
1664
|
+
throw Error(`${nameOfMethodBag} does not have a method named ${String(property)}`);
|
|
1665
|
+
}
|
|
1666
|
+
return function(...args) {
|
|
1667
|
+
self.value = fn.apply(receiver, args);
|
|
1668
|
+
return self;
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
get A() {
|
|
1674
|
+
return A2(this.value);
|
|
1675
|
+
}
|
|
1676
|
+
get a() {
|
|
1677
|
+
return this._convertMethodsToPipeline(A2(this.value), "A");
|
|
1678
|
+
}
|
|
1679
|
+
compact(...args) {
|
|
1680
|
+
this.value = compact(...args)(this.value);
|
|
1681
|
+
return this;
|
|
1682
|
+
}
|
|
1683
|
+
each(visitorFn) {
|
|
1684
|
+
this.value = each(visitorFn)(this.value);
|
|
1685
|
+
return this;
|
|
1686
|
+
}
|
|
1687
|
+
get F() {
|
|
1688
|
+
return F(this.value);
|
|
1689
|
+
}
|
|
1690
|
+
get f() {
|
|
1691
|
+
return this._convertMethodsToPipeline(F(this.value), "F");
|
|
1692
|
+
}
|
|
1693
|
+
// enumerable() {
|
|
1694
|
+
// this.value = Enumerable.for(this.value);
|
|
1695
|
+
// return this;
|
|
1696
|
+
// }
|
|
1697
|
+
// enumerator() {
|
|
1698
|
+
// this.value = Enumerator.for(this.value);
|
|
1699
|
+
// return this;
|
|
1700
|
+
// }
|
|
1701
|
+
log() {
|
|
1702
|
+
console.log(V.inspect(this.value));
|
|
1703
|
+
return this;
|
|
1704
|
+
}
|
|
1705
|
+
get M() {
|
|
1706
|
+
return M(this.value);
|
|
1707
|
+
}
|
|
1708
|
+
get m() {
|
|
1709
|
+
return this._convertMethodsToPipeline(M(this.value), "M");
|
|
1710
|
+
}
|
|
1711
|
+
map(mapFn, implClass) {
|
|
1712
|
+
this.value = map(mapFn)(this.value, implClass);
|
|
1713
|
+
return this;
|
|
1714
|
+
}
|
|
1715
|
+
asyncMap(mapFn, implClass) {
|
|
1716
|
+
this.value = asyncMap(mapFn)(this.value, implClass);
|
|
1717
|
+
return this;
|
|
1718
|
+
}
|
|
1719
|
+
get O() {
|
|
1720
|
+
return O(this.value);
|
|
1721
|
+
}
|
|
1722
|
+
get o() {
|
|
1723
|
+
return this._convertMethodsToPipeline(O(this.value), "O");
|
|
1724
|
+
}
|
|
1725
|
+
pipe(fn) {
|
|
1726
|
+
this.value = fn(this.value);
|
|
1727
|
+
return this;
|
|
1728
|
+
}
|
|
1729
|
+
get S() {
|
|
1730
|
+
return S(this.value);
|
|
1731
|
+
}
|
|
1732
|
+
get s() {
|
|
1733
|
+
return this._convertMethodsToPipeline(S(this.value), "S");
|
|
1734
|
+
}
|
|
1735
|
+
get Str() {
|
|
1736
|
+
return Str(this.value);
|
|
1737
|
+
}
|
|
1738
|
+
get str() {
|
|
1739
|
+
return this._convertMethodsToPipeline(Str(this.value), "Str");
|
|
1740
|
+
}
|
|
1741
|
+
select(predFn, implClass) {
|
|
1742
|
+
this.value = select(predFn)(this.value, implClass);
|
|
1743
|
+
return this;
|
|
1744
|
+
}
|
|
1745
|
+
toArray() {
|
|
1746
|
+
this.value = Enumerable.for(this.value).toArray();
|
|
1747
|
+
return this;
|
|
1748
|
+
}
|
|
1749
|
+
waitAll() {
|
|
1750
|
+
this.value = Promise.allSettled(this.value).then((results) => {
|
|
1751
|
+
return results.map((result) => {
|
|
1752
|
+
if (result.status === "fulfilled") {
|
|
1753
|
+
return result.value;
|
|
1754
|
+
} else {
|
|
1755
|
+
return new Error(result.reason);
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
return this;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
var VP = function(initial) {
|
|
1763
|
+
return new Pipe(initial);
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// src/tempfile.ts
|
|
1767
|
+
import "os";
|
|
1768
|
+
import fs from "fs";
|
|
1769
|
+
import os from "os";
|
|
1770
|
+
import path from "path";
|
|
1771
|
+
import process2 from "process";
|
|
1772
|
+
|
|
1773
|
+
// src/flex/file.ts
|
|
1774
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1775
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1776
|
+
import { win32 as win322, posix as posix2 } from "path";
|
|
1777
|
+
function exists(path3) {
|
|
1778
|
+
return existsSync2(path3);
|
|
1779
|
+
}
|
|
1780
|
+
var File = class {
|
|
1781
|
+
static absolutePath(...paths) {
|
|
1782
|
+
if (isWindows()) {
|
|
1783
|
+
return win322.resolve(...paths);
|
|
1784
|
+
} else {
|
|
1785
|
+
return posix2.resolve(...paths);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
// basename("c:\\foo\\bar\\baz.txt") => "baz.txt"
|
|
1789
|
+
// basename("/tmp/myfile.html") => "myfile.html"
|
|
1790
|
+
static basename(path3, suffix) {
|
|
1791
|
+
if (isWindows()) {
|
|
1792
|
+
return win322.basename(path3, suffix);
|
|
1793
|
+
} else {
|
|
1794
|
+
return posix2.basename(path3, suffix);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
static exists(path3) {
|
|
1798
|
+
return exists(path3);
|
|
1799
|
+
}
|
|
1800
|
+
static join(...paths) {
|
|
1801
|
+
if (isWindows()) {
|
|
1802
|
+
return win322.join(...paths);
|
|
1803
|
+
} else {
|
|
1804
|
+
return posix2.join(...paths);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
static readSync(path3) {
|
|
1808
|
+
return readFileSync2(path3, {
|
|
1809
|
+
encoding: "utf8"
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
static async readAsync(path3) {
|
|
1813
|
+
return await readFile2(path3, {
|
|
1814
|
+
encoding: "utf8"
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/tempfile.ts
|
|
1820
|
+
var TmpFileRegistry = class _TmpFileRegistry {
|
|
1821
|
+
static _instance;
|
|
1822
|
+
static get instance() {
|
|
1823
|
+
const path3 = File.join(osHomeDir(), ".hostctl", "tmpstatic");
|
|
1824
|
+
this._instance ??= new _TmpFileRegistry(path3);
|
|
1825
|
+
return this._instance;
|
|
1826
|
+
}
|
|
1827
|
+
rootPath;
|
|
1828
|
+
tempFilePaths;
|
|
1829
|
+
constructor(rootPath) {
|
|
1830
|
+
this.rootPath = Path.new(rootPath);
|
|
1831
|
+
this.tempFilePaths = [];
|
|
1832
|
+
process2.on("exit", (code) => this.exitCallback());
|
|
1833
|
+
}
|
|
1834
|
+
randName() {
|
|
1835
|
+
return Math.random().toString(36).slice(-5) + Math.random().toString(36).slice(-5);
|
|
1836
|
+
}
|
|
1837
|
+
tmpPath(tmpName) {
|
|
1838
|
+
tmpName ??= this.randName();
|
|
1839
|
+
return this.rootPath.join(tmpName);
|
|
1840
|
+
}
|
|
1841
|
+
// this directory will be automatically cleaned up at program exit
|
|
1842
|
+
createNamedTmpDir(subDirName) {
|
|
1843
|
+
const path3 = this.tmpPath(subDirName);
|
|
1844
|
+
fs.mkdirSync(path3.toString(), { recursive: true });
|
|
1845
|
+
this.registerTempFileOrDir(path3.toString());
|
|
1846
|
+
return path3;
|
|
1847
|
+
}
|
|
1848
|
+
// this file will be automatically cleaned up at program exit
|
|
1849
|
+
writeTmpFile(fileContent) {
|
|
1850
|
+
const path3 = this.tmpPath();
|
|
1851
|
+
fs.writeFileSync(path3.toString(), fileContent);
|
|
1852
|
+
this.registerTempFileOrDir(path3.toString());
|
|
1853
|
+
return path3;
|
|
1854
|
+
}
|
|
1855
|
+
exitCallback() {
|
|
1856
|
+
this.cleanupTempFiles();
|
|
1857
|
+
}
|
|
1858
|
+
registerTempFileOrDir(path3) {
|
|
1859
|
+
this.tempFilePaths.push(path3);
|
|
1860
|
+
}
|
|
1861
|
+
cleanupTempFiles() {
|
|
1862
|
+
this.tempFilePaths.forEach((path3) => {
|
|
1863
|
+
this.rmFile(path3);
|
|
1864
|
+
});
|
|
1865
|
+
this.tempFilePaths = [];
|
|
1866
|
+
}
|
|
1867
|
+
rmFile(path3) {
|
|
1868
|
+
try {
|
|
1869
|
+
fs.rmSync(path3, { force: true, recursive: true });
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
function osHomeDir() {
|
|
1875
|
+
return path.resolve(os.homedir());
|
|
1876
|
+
}
|
|
1877
|
+
function writeTmpFile(fileContents) {
|
|
1878
|
+
return TmpFileRegistry.instance.writeTmpFile(fileContents);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// src/host.ts
|
|
1882
|
+
var Host = class {
|
|
1883
|
+
constructor(config, opts) {
|
|
1884
|
+
this.config = config;
|
|
1885
|
+
this.hostname = opts.hostname;
|
|
1886
|
+
this.alias = opts.alias;
|
|
1887
|
+
this.port = opts.port || 22;
|
|
1888
|
+
this.user = opts.user;
|
|
1889
|
+
this.password = opts.password;
|
|
1890
|
+
this.sshKey = opts.sshKey;
|
|
1891
|
+
this.tags = opts.tags ?? [];
|
|
1892
|
+
this.tagSet = new Set(this.tags);
|
|
1893
|
+
}
|
|
1894
|
+
hostname;
|
|
1895
|
+
// The network hostname or IP address, from the YAML hosts map key
|
|
1896
|
+
alias;
|
|
1897
|
+
port;
|
|
1898
|
+
user;
|
|
1899
|
+
password;
|
|
1900
|
+
sshKey;
|
|
1901
|
+
tags;
|
|
1902
|
+
tagSet;
|
|
1903
|
+
async decryptedPassword() {
|
|
1904
|
+
switch (kind(this.password)) {
|
|
1905
|
+
case String:
|
|
1906
|
+
return this.password;
|
|
1907
|
+
case SecretRef:
|
|
1908
|
+
const secretRef = this.password;
|
|
1909
|
+
const secret = this.config.getSecret(secretRef.name);
|
|
1910
|
+
return await secret?.plaintext();
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
async decryptedSshKey() {
|
|
1914
|
+
if (V(this.sshKey).isA(SecretRef)) {
|
|
1915
|
+
const secretRef = this.sshKey;
|
|
1916
|
+
const secret = this.config.getSecret(secretRef.name);
|
|
1917
|
+
return await secret?.plaintext();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
toYAML() {
|
|
1921
|
+
let passwordForYaml = this.password;
|
|
1922
|
+
if (this.password && V(this.password).isA(SecretRef)) {
|
|
1923
|
+
passwordForYaml = this.password.toYAML();
|
|
1924
|
+
}
|
|
1925
|
+
let sshKeyForYaml = this.sshKey;
|
|
1926
|
+
if (this.sshKey && V(this.sshKey).isA(SecretRef)) {
|
|
1927
|
+
sshKeyForYaml = this.sshKey.toYAML();
|
|
1928
|
+
}
|
|
1929
|
+
return {
|
|
1930
|
+
alias: this.alias,
|
|
1931
|
+
user: this.user,
|
|
1932
|
+
port: this.port === 22 ? void 0 : this.port,
|
|
1933
|
+
// Only include port if not default
|
|
1934
|
+
password: passwordForYaml,
|
|
1935
|
+
"ssh-key": sshKeyForYaml,
|
|
1936
|
+
tags: this.tags
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
// domain logic
|
|
1940
|
+
get shortName() {
|
|
1941
|
+
return this.alias || this.hostname;
|
|
1942
|
+
}
|
|
1943
|
+
isLocal() {
|
|
1944
|
+
return this.hostname === "localhost" || this.hostname === "127.0.0.1";
|
|
1945
|
+
}
|
|
1946
|
+
get uri() {
|
|
1947
|
+
return this.hostname;
|
|
1948
|
+
}
|
|
1949
|
+
// returns the temporary path to the decrypted ssh key
|
|
1950
|
+
async writeTmpDecryptedSshKey() {
|
|
1951
|
+
const content = await this.decryptedSshKey();
|
|
1952
|
+
if (content) {
|
|
1953
|
+
return writeTmpFile(content).toString();
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
_plaintextSshKeyPath = void 0;
|
|
1957
|
+
async plaintextSshKeyPath() {
|
|
1958
|
+
if (this._plaintextSshKeyPath) {
|
|
1959
|
+
return this._plaintextSshKeyPath;
|
|
1960
|
+
}
|
|
1961
|
+
switch (kind(this.sshKey)) {
|
|
1962
|
+
case String:
|
|
1963
|
+
this._plaintextSshKeyPath = this.sshKey;
|
|
1964
|
+
break;
|
|
1965
|
+
case SecretRef:
|
|
1966
|
+
this._plaintextSshKeyPath = await this.writeTmpDecryptedSshKey();
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
return this._plaintextSshKeyPath;
|
|
1970
|
+
}
|
|
1971
|
+
toObject() {
|
|
1972
|
+
return {
|
|
1973
|
+
hostname: this.hostname,
|
|
1974
|
+
port: this.port,
|
|
1975
|
+
alias: this.alias,
|
|
1976
|
+
user: this.user,
|
|
1977
|
+
tags: this.tags
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
hasTag(tag) {
|
|
1981
|
+
return this.tagSet.has(tag);
|
|
1982
|
+
}
|
|
1983
|
+
// each tag is a string of the form:
|
|
1984
|
+
// - foo
|
|
1985
|
+
// - bar,baz
|
|
1986
|
+
// the interpretation of a 'bar,baz' tag is that the host must have either the 'bar' tag or the 'baz' tag in order to consider the 'bar,baz' tag present
|
|
1987
|
+
hasAllTags(tags) {
|
|
1988
|
+
for (const tag of tags) {
|
|
1989
|
+
const tagParts = tag.split(",");
|
|
1990
|
+
const anyTagPartPresent = S(this.tagSet).intersection(tagParts).size > 0;
|
|
1991
|
+
if (!anyTagPartPresent) return false;
|
|
1992
|
+
}
|
|
1993
|
+
return true;
|
|
1994
|
+
}
|
|
1995
|
+
// each tag is a string of the form:
|
|
1996
|
+
// - foo
|
|
1997
|
+
// - bar+baz
|
|
1998
|
+
// the interpretation of a 'bar+baz' tag is that the host must have both the 'bar' tag and the 'baz' tag in order to consider the 'bar+baz' tag present
|
|
1999
|
+
hasAnyTag(tags) {
|
|
2000
|
+
const effectiveTagsForMatching = S(new Set(this.tagSet)).addAll(A2([this.hostname, this.alias]).compact());
|
|
2001
|
+
for (const tag of tags) {
|
|
2002
|
+
const tagParts = tag.split("+");
|
|
2003
|
+
const allTagPartsPresent = S(effectiveTagsForMatching).isSupersetOf(tagParts);
|
|
2004
|
+
if (allTagPartsPresent) return true;
|
|
2005
|
+
}
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// src/age-encryption.ts
|
|
2011
|
+
import fs2 from "fs";
|
|
2012
|
+
import * as age from "age-encryption";
|
|
2013
|
+
|
|
2014
|
+
// src/process.ts
|
|
2015
|
+
import spawnAsync from "@expo/spawn-async";
|
|
2016
|
+
|
|
2017
|
+
// src/age-encryption.ts
|
|
2018
|
+
function readIdentityStringFromFile(path3) {
|
|
2019
|
+
const contents = fs2.readFileSync(path3, {
|
|
2020
|
+
encoding: "utf8"
|
|
2021
|
+
});
|
|
2022
|
+
const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
|
|
2023
|
+
if (!identityString) throw new Error(`Unable to read identity from file: ${path3}`);
|
|
2024
|
+
return identityString;
|
|
2025
|
+
}
|
|
2026
|
+
var LibraryDriver = class {
|
|
2027
|
+
async encrypt(valueToEncrypt, publicKeys) {
|
|
2028
|
+
const e = new age.Encrypter();
|
|
2029
|
+
publicKeys.forEach((key) => e.addRecipient(key));
|
|
2030
|
+
const ciphertextBytes = await e.encrypt(valueToEncrypt);
|
|
2031
|
+
return age.armor.encode(ciphertextBytes);
|
|
2032
|
+
}
|
|
2033
|
+
async decrypt(valueToDecryptArmored, privateKeyFilePaths) {
|
|
2034
|
+
if (privateKeyFilePaths.length === 0) {
|
|
2035
|
+
throw new Error("Unable to decrypt: No age encryption identity (private key) specified.");
|
|
2036
|
+
}
|
|
2037
|
+
const d = new age.Decrypter();
|
|
2038
|
+
let identitiesAdded = 0;
|
|
2039
|
+
for (const path3 of privateKeyFilePaths) {
|
|
2040
|
+
try {
|
|
2041
|
+
const identityString = readIdentityStringFromFile(path3);
|
|
2042
|
+
d.addIdentity(identityString);
|
|
2043
|
+
identitiesAdded++;
|
|
2044
|
+
} catch (err) {
|
|
2045
|
+
console.warn(`Failed to read or parse identity file ${path3}, skipping: ${err.message}`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (identitiesAdded === 0) {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
const ciphertextBytes = age.armor.decode(valueToDecryptArmored);
|
|
2053
|
+
const decryptedText = await d.decrypt(ciphertextBytes, "text");
|
|
2054
|
+
return decryptedText;
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
var Identity = class {
|
|
2061
|
+
identityFilePath;
|
|
2062
|
+
identity;
|
|
2063
|
+
// either the path to an identity file or an identity string must be supplied
|
|
2064
|
+
constructor({ path: path3, identity: identity2 }) {
|
|
2065
|
+
if (identity2) {
|
|
2066
|
+
this.identity = identity2;
|
|
2067
|
+
this.identityFilePath = this.writeTmpIdentityFile(identity2);
|
|
2068
|
+
} else if (path3) {
|
|
2069
|
+
this.identity = this.readIdentityFromFile(path3);
|
|
2070
|
+
this.identityFilePath = path3;
|
|
2071
|
+
} else {
|
|
2072
|
+
throw "Either an identity string or an identity file path must be supplied to create an Age Encryption identity";
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
get recipient() {
|
|
2076
|
+
return age.identityToRecipient(this.identity);
|
|
2077
|
+
}
|
|
2078
|
+
// returns the path to the temp file
|
|
2079
|
+
writeTmpIdentityFile(identity2) {
|
|
2080
|
+
return writeTmpFile(identity2).toString();
|
|
2081
|
+
}
|
|
2082
|
+
readIdentityFromFile(path3) {
|
|
2083
|
+
const contents = fs2.readFileSync(path3, {
|
|
2084
|
+
encoding: "utf8"
|
|
2085
|
+
});
|
|
2086
|
+
const identityString = contents.split(/\r?\n|\r|\n/g).map((line) => line.trim()).find((line) => line.startsWith("AGE-SECRET-KEY-1"));
|
|
2087
|
+
if (!identityString) throw new Error(`Unable to read identity file: ${path3}`);
|
|
2088
|
+
return identityString;
|
|
2089
|
+
}
|
|
2090
|
+
get privateKey() {
|
|
2091
|
+
return this.identity;
|
|
2092
|
+
}
|
|
2093
|
+
get publicKey() {
|
|
2094
|
+
return this.recipient;
|
|
2095
|
+
}
|
|
2096
|
+
decrypt(encryptedString) {
|
|
2097
|
+
return AgeEncryption.decrypt(encryptedString, [this.identityFilePath]);
|
|
2098
|
+
}
|
|
2099
|
+
async encrypt(plaintext) {
|
|
2100
|
+
return AgeEncryption.encrypt(plaintext, [await this.recipient]);
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
var AgeEncryption = class {
|
|
2104
|
+
static instance = new LibraryDriver();
|
|
2105
|
+
// Use LibraryDriver by default
|
|
2106
|
+
static async encrypt(valueToEncrypt, publicKeys) {
|
|
2107
|
+
const encrypted = await this.instance.encrypt(valueToEncrypt, publicKeys);
|
|
2108
|
+
return encrypted;
|
|
2109
|
+
}
|
|
2110
|
+
static async decrypt(valueToDecrypt, privateKeyFilePaths) {
|
|
2111
|
+
return await this.instance.decrypt(valueToDecrypt, privateKeyFilePaths);
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
// src/config-file.ts
|
|
2116
|
+
var SecretRef = class {
|
|
2117
|
+
constructor(name) {
|
|
2118
|
+
this.name = name;
|
|
2119
|
+
}
|
|
2120
|
+
toYAML() {
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
var SecretValue2 = class {
|
|
2125
|
+
constructor(value) {
|
|
2126
|
+
this.value = value;
|
|
2127
|
+
}
|
|
2128
|
+
async encrypt(recipients) {
|
|
2129
|
+
if (!this.isEncrypted()) {
|
|
2130
|
+
this.value = await AgeEncryption.encrypt(this.value, recipients);
|
|
2131
|
+
}
|
|
2132
|
+
return this.value;
|
|
2133
|
+
}
|
|
2134
|
+
async decrypt(identities) {
|
|
2135
|
+
if (this.isEncrypted()) {
|
|
2136
|
+
const decryptedValue = await AgeEncryption.decrypt(
|
|
2137
|
+
this.value,
|
|
2138
|
+
identities.map((i) => i.identityFilePath)
|
|
2139
|
+
);
|
|
2140
|
+
if (!decryptedValue) {
|
|
2141
|
+
return void 0;
|
|
2142
|
+
}
|
|
2143
|
+
this.value = decryptedValue;
|
|
2144
|
+
}
|
|
2145
|
+
return this.value;
|
|
2146
|
+
}
|
|
2147
|
+
isEncrypted() {
|
|
2148
|
+
return this.value.includes("AGE ENCRYPTED FILE");
|
|
2149
|
+
}
|
|
2150
|
+
toYAML() {
|
|
2151
|
+
return this.value;
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
var Secret2 = class {
|
|
2155
|
+
constructor(config, name, ids, value) {
|
|
2156
|
+
this.config = config;
|
|
2157
|
+
this.name = name;
|
|
2158
|
+
this.ids = ids;
|
|
2159
|
+
this.value = value;
|
|
2160
|
+
}
|
|
2161
|
+
recipients() {
|
|
2162
|
+
return this.ids.recipients();
|
|
2163
|
+
}
|
|
2164
|
+
async ciphertext() {
|
|
2165
|
+
return await this.value.encrypt(this.recipients());
|
|
2166
|
+
}
|
|
2167
|
+
async plaintext() {
|
|
2168
|
+
const identities = this.config.loadPrivateKeys();
|
|
2169
|
+
return await this.value.decrypt(identities);
|
|
2170
|
+
}
|
|
2171
|
+
async encrypt() {
|
|
2172
|
+
return await this.ciphertext();
|
|
2173
|
+
}
|
|
2174
|
+
async decrypt() {
|
|
2175
|
+
return await this.plaintext();
|
|
2176
|
+
}
|
|
2177
|
+
toYAML() {
|
|
2178
|
+
return {
|
|
2179
|
+
ids: this.ids.toYAML(),
|
|
2180
|
+
value: this.value.toYAML()
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
var RecipientGroup = class {
|
|
2185
|
+
constructor(config, publicKeys, idRefs) {
|
|
2186
|
+
this.config = config;
|
|
2187
|
+
this.publicKeys = publicKeys;
|
|
2188
|
+
this.idRefs = idRefs;
|
|
2189
|
+
}
|
|
2190
|
+
// returns an array of public keys associated with all immediate and transitive recipients
|
|
2191
|
+
recipients() {
|
|
2192
|
+
const accumulatedPublicKeys = A2(this.publicKeys).toSet();
|
|
2193
|
+
this.walkRecipientGroups((rg) => {
|
|
2194
|
+
S(accumulatedPublicKeys).addAll(rg.publicKeys);
|
|
2195
|
+
});
|
|
2196
|
+
return S(accumulatedPublicKeys).toArray();
|
|
2197
|
+
}
|
|
2198
|
+
walkRecipientGroups(visitFn, visited = /* @__PURE__ */ new Set()) {
|
|
2199
|
+
if (visited.has(this)) return;
|
|
2200
|
+
visited.add(this);
|
|
2201
|
+
visitFn(this);
|
|
2202
|
+
this.config.getRecipientGroups(this.idRefs).forEach((rg) => rg.walkRecipientGroups(visitFn, visited));
|
|
2203
|
+
}
|
|
2204
|
+
toYAML() {
|
|
2205
|
+
return this.publicKeys.concat(this.idRefs);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
var NamedRecipientGroup = class extends RecipientGroup {
|
|
2209
|
+
constructor(config, name, publicKeys, idRefs) {
|
|
2210
|
+
super(config, publicKeys, idRefs);
|
|
2211
|
+
this.config = config;
|
|
2212
|
+
this.name = name;
|
|
2213
|
+
this.publicKeys = publicKeys;
|
|
2214
|
+
this.idRefs = idRefs;
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
var SecretRefYamlType = new yaml.Type("!secret", {
|
|
2218
|
+
kind: "scalar",
|
|
2219
|
+
// 'scalar' means string
|
|
2220
|
+
// Loader must check if the input object is suitable for this type.
|
|
2221
|
+
resolve(data) {
|
|
2222
|
+
return !!data && typeof data === "string" && data.trim().length > 0;
|
|
2223
|
+
},
|
|
2224
|
+
// If a node is resolved, use it to create a SecretRef instance.
|
|
2225
|
+
construct(yamlObj) {
|
|
2226
|
+
return new SecretRef(yamlObj);
|
|
2227
|
+
},
|
|
2228
|
+
// Dumper must process instances of Point by rules of this YAML type.
|
|
2229
|
+
instanceOf: SecretRef,
|
|
2230
|
+
// Dumper must represent SecretRef objects as string in YAML.
|
|
2231
|
+
represent(secretRef) {
|
|
2232
|
+
return secretRef.name;
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
var HOSTCTL_CONFIG_SCHEMA = yaml.DEFAULT_SCHEMA.extend([SecretRefYamlType]);
|
|
2236
|
+
var ConfigFile2 = class {
|
|
2237
|
+
constructor(path3) {
|
|
2238
|
+
this.path = path3;
|
|
2239
|
+
this._hosts = /* @__PURE__ */ new Map();
|
|
2240
|
+
this._ids = /* @__PURE__ */ new Map();
|
|
2241
|
+
this._secrets = /* @__PURE__ */ new Map();
|
|
2242
|
+
}
|
|
2243
|
+
_hosts;
|
|
2244
|
+
_ids;
|
|
2245
|
+
_secrets;
|
|
2246
|
+
// serialization and deserialization
|
|
2247
|
+
async load() {
|
|
2248
|
+
if (!File.exists(this.path)) {
|
|
2249
|
+
throw new Error(`Config file ${this.path} doesn't exist. Unable to load.`);
|
|
2250
|
+
}
|
|
2251
|
+
const yamlFileContents = await fsa.readFile(this.path, {
|
|
2252
|
+
encoding: "utf8"
|
|
2253
|
+
});
|
|
2254
|
+
const yamlDocument = yaml.load(yamlFileContents, {
|
|
2255
|
+
schema: HOSTCTL_CONFIG_SCHEMA
|
|
2256
|
+
});
|
|
2257
|
+
this.parse(yamlDocument);
|
|
2258
|
+
}
|
|
2259
|
+
parse(yamlDocument) {
|
|
2260
|
+
this._hosts = this.parseHosts(yamlDocument.hosts || {});
|
|
2261
|
+
this._ids = this.parseIdsMap(yamlDocument.ids || {});
|
|
2262
|
+
this._secrets = this.parseSecrets(yamlDocument.secrets || {});
|
|
2263
|
+
}
|
|
2264
|
+
parseSecretValue(yamlSecretValue) {
|
|
2265
|
+
switch (V(yamlSecretValue).kind()) {
|
|
2266
|
+
case Undefined:
|
|
2267
|
+
return yamlSecretValue;
|
|
2268
|
+
case SecretRef:
|
|
2269
|
+
return yamlSecretValue;
|
|
2270
|
+
case String:
|
|
2271
|
+
return yamlSecretValue;
|
|
2272
|
+
// return new SecretValue(yamlSecretValue as string);
|
|
2273
|
+
default:
|
|
2274
|
+
throw new Error(`unknown secret type: ${V.inspect(yamlSecretValue)}`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
// yamlHosts is an object
|
|
2278
|
+
parseHosts(yamlHosts) {
|
|
2279
|
+
const hosts = Object.entries(yamlHosts).reduce((hostMap, [hostUri, hostObj]) => {
|
|
2280
|
+
hostObj ||= {};
|
|
2281
|
+
const password = this.parseSecretValue(hostObj.password);
|
|
2282
|
+
const sshKey = this.parseSecretValue(hostObj["ssh-key"]);
|
|
2283
|
+
hostMap.set(
|
|
2284
|
+
hostUri,
|
|
2285
|
+
new Host(this, {
|
|
2286
|
+
...hostObj,
|
|
2287
|
+
// Use 'as any' to allow spread of hostObj properties
|
|
2288
|
+
hostname: hostUri,
|
|
2289
|
+
// The YAML map key is always the hostname
|
|
2290
|
+
password,
|
|
2291
|
+
sshKey
|
|
2292
|
+
})
|
|
2293
|
+
);
|
|
2294
|
+
return hostMap;
|
|
2295
|
+
}, /* @__PURE__ */ new Map());
|
|
2296
|
+
return hosts;
|
|
2297
|
+
}
|
|
2298
|
+
parseIdsMap(yamlIds) {
|
|
2299
|
+
const ids = Object.entries(yamlIds).reduce((secretsMap, [name, value]) => {
|
|
2300
|
+
value ||= [];
|
|
2301
|
+
const rg = this.parseRecipients(value);
|
|
2302
|
+
secretsMap.set(name, new NamedRecipientGroup(this, name, rg.publicKeys, rg.idRefs));
|
|
2303
|
+
return secretsMap;
|
|
2304
|
+
}, /* @__PURE__ */ new Map());
|
|
2305
|
+
return ids;
|
|
2306
|
+
}
|
|
2307
|
+
parseRecipients(ids) {
|
|
2308
|
+
let idStrings;
|
|
2309
|
+
if (V(ids).isA(String)) {
|
|
2310
|
+
idStrings = [ids];
|
|
2311
|
+
} else if (Array.isArray(ids) && ids.every((item) => V(item).isA(String))) {
|
|
2312
|
+
idStrings = ids;
|
|
2313
|
+
} else if (Array.isArray(ids)) {
|
|
2314
|
+
idStrings = ids.flat(1);
|
|
2315
|
+
} else {
|
|
2316
|
+
idStrings = [];
|
|
2317
|
+
}
|
|
2318
|
+
const publicKeysAndIdRefs = idStrings.flatMap((str) => str.split(",")).map((str) => str.trim());
|
|
2319
|
+
const [publicKeys, idRefs] = A2(publicKeysAndIdRefs).partition(this.isValidPublicKey);
|
|
2320
|
+
return new RecipientGroup(this, publicKeys, idRefs);
|
|
2321
|
+
}
|
|
2322
|
+
// returns true if the key is an Age encryption public key (i.e. a bech32 key with a prefix of 'age')
|
|
2323
|
+
isValidPublicKey(str) {
|
|
2324
|
+
try {
|
|
2325
|
+
const { prefix, words } = bech32.decode(str);
|
|
2326
|
+
return prefix === "age";
|
|
2327
|
+
} catch (e) {
|
|
2328
|
+
return false;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
// yamlSecrets is an object
|
|
2332
|
+
parseSecrets(yamlSecrets) {
|
|
2333
|
+
const secrets = Object.entries(yamlSecrets).reduce((secretsMap, [name, yamlSecret]) => {
|
|
2334
|
+
const recipients = this.parseRecipients(yamlSecret.ids);
|
|
2335
|
+
secretsMap.set(name, new Secret2(this, name, recipients, new SecretValue2(yamlSecret.value)));
|
|
2336
|
+
return secretsMap;
|
|
2337
|
+
}, /* @__PURE__ */ new Map());
|
|
2338
|
+
return secrets;
|
|
2339
|
+
}
|
|
2340
|
+
async save(destinationPath = this.path) {
|
|
2341
|
+
const hosts = this.serializeHosts();
|
|
2342
|
+
const secrets = this.serializeSecrets();
|
|
2343
|
+
const ids = this.serializeIds();
|
|
2344
|
+
const yamlFileObj = {
|
|
2345
|
+
hosts,
|
|
2346
|
+
secrets,
|
|
2347
|
+
ids
|
|
2348
|
+
};
|
|
2349
|
+
const yamlFileContents = yaml.dump(yamlFileObj, {
|
|
2350
|
+
schema: HOSTCTL_CONFIG_SCHEMA,
|
|
2351
|
+
noRefs: true
|
|
2352
|
+
});
|
|
2353
|
+
await fsa.writeFile(destinationPath, yamlFileContents);
|
|
2354
|
+
}
|
|
2355
|
+
serializeHosts() {
|
|
2356
|
+
return VP(this._hosts).map(([name, host]) => [name, host.toYAML()]).m.toObject().value;
|
|
2357
|
+
}
|
|
2358
|
+
serializeSecrets() {
|
|
2359
|
+
return VP(this._secrets).map(([name, secret]) => [name, secret.toYAML()]).m.toObject().value;
|
|
2360
|
+
}
|
|
2361
|
+
serializeIds() {
|
|
2362
|
+
return VP(this._ids).map(([name, id]) => [name, id.toYAML()]).m.toObject().value;
|
|
2363
|
+
}
|
|
2364
|
+
// domain logic
|
|
2365
|
+
// loads private keys from the AGE_IDS environment variable
|
|
2366
|
+
loadPrivateKeys() {
|
|
2367
|
+
const ageIds = process.env.AGE_IDS;
|
|
2368
|
+
if (ageIds) {
|
|
2369
|
+
const paths = globSync2(ageIds);
|
|
2370
|
+
const ids = paths.map((path3) => new Identity({ path: path3 }));
|
|
2371
|
+
return ids;
|
|
2372
|
+
}
|
|
2373
|
+
return [];
|
|
2374
|
+
}
|
|
2375
|
+
getSecret(name) {
|
|
2376
|
+
return this._secrets.get(name);
|
|
2377
|
+
}
|
|
2378
|
+
getRecipientGroups(idRefs) {
|
|
2379
|
+
return VP(idRefs).map((idRef) => this._ids.get(idRef)).compact().value;
|
|
2380
|
+
}
|
|
2381
|
+
async decryptAllIfPossible() {
|
|
2382
|
+
await VP(this._secrets).m.values().map((secret) => secret.decrypt()).waitAll().value;
|
|
2383
|
+
}
|
|
2384
|
+
async encryptAll() {
|
|
2385
|
+
await VP(this._secrets).m.values().map((secret) => secret.encrypt()).waitAll().value;
|
|
2386
|
+
}
|
|
2387
|
+
// Config interface
|
|
2388
|
+
hosts() {
|
|
2389
|
+
return M(this._hosts).values();
|
|
2390
|
+
}
|
|
2391
|
+
secrets() {
|
|
2392
|
+
return this._secrets;
|
|
2393
|
+
}
|
|
2394
|
+
ids() {
|
|
2395
|
+
return this._ids;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// src/config-url.ts
|
|
2400
|
+
var ConfigUrl = class {
|
|
2401
|
+
constructor(url) {
|
|
2402
|
+
this.url = url;
|
|
2403
|
+
this._hosts = /* @__PURE__ */ new Map();
|
|
2404
|
+
this._secrets = /* @__PURE__ */ new Map();
|
|
2405
|
+
this._ids = /* @__PURE__ */ new Map();
|
|
2406
|
+
}
|
|
2407
|
+
_hosts;
|
|
2408
|
+
_secrets;
|
|
2409
|
+
_ids;
|
|
2410
|
+
// Config interface
|
|
2411
|
+
hosts() {
|
|
2412
|
+
return [];
|
|
2413
|
+
}
|
|
2414
|
+
getSecret(name) {
|
|
2415
|
+
return void 0;
|
|
2416
|
+
}
|
|
2417
|
+
getRecipientGroups(idRefs) {
|
|
2418
|
+
return [];
|
|
2419
|
+
}
|
|
2420
|
+
secrets() {
|
|
2421
|
+
return this._secrets;
|
|
2422
|
+
}
|
|
2423
|
+
ids() {
|
|
2424
|
+
return this._ids;
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
// src/config.ts
|
|
2429
|
+
async function load(configRef) {
|
|
2430
|
+
if (configRef && fs4.existsSync(configRef)) {
|
|
2431
|
+
const configFile = new ConfigFile2(configRef);
|
|
2432
|
+
await configFile.load();
|
|
2433
|
+
return configFile;
|
|
2434
|
+
} else if (configRef) {
|
|
2435
|
+
return new ConfigUrl(configRef);
|
|
2436
|
+
} else {
|
|
2437
|
+
throw new Error("No config specified.");
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// src/interaction-handler.ts
|
|
2442
|
+
import { Readable } from "stream";
|
|
2443
|
+
import { StreamRegex } from "stream-regex";
|
|
2444
|
+
var InteractionHandler = class _InteractionHandler {
|
|
2445
|
+
static with(inputs) {
|
|
2446
|
+
return new _InteractionHandler(new Map(Object.entries(inputs)));
|
|
2447
|
+
}
|
|
2448
|
+
patternInputMappings;
|
|
2449
|
+
// pairs of pattern -> response
|
|
2450
|
+
inputStreamForRegexMatcher;
|
|
2451
|
+
// private streamRegex: StreamRegex;
|
|
2452
|
+
// private matches: Readable;
|
|
2453
|
+
writeFn;
|
|
2454
|
+
constructor(patternInputMappings = /* @__PURE__ */ new Map()) {
|
|
2455
|
+
this.patternInputMappings = M(patternInputMappings).map(([pattern, v]) => [
|
|
2456
|
+
pattern instanceof RegExp ? pattern.source : pattern,
|
|
2457
|
+
v
|
|
2458
|
+
]);
|
|
2459
|
+
}
|
|
2460
|
+
clone() {
|
|
2461
|
+
return new _InteractionHandler(this.patternInputMappings);
|
|
2462
|
+
}
|
|
2463
|
+
with(inputs) {
|
|
2464
|
+
const clone = this.clone();
|
|
2465
|
+
clone.mapInput(inputs);
|
|
2466
|
+
return clone;
|
|
2467
|
+
}
|
|
2468
|
+
mapInput(inputMap) {
|
|
2469
|
+
Object.entries(inputMap).forEach(([pattern, response]) => {
|
|
2470
|
+
this.map(pattern, response);
|
|
2471
|
+
});
|
|
2472
|
+
return this;
|
|
2473
|
+
}
|
|
2474
|
+
map(pattern, response) {
|
|
2475
|
+
if (pattern instanceof RegExp) {
|
|
2476
|
+
this.patternInputMappings.set(pattern.source, response);
|
|
2477
|
+
} else {
|
|
2478
|
+
this.patternInputMappings.set(pattern, response);
|
|
2479
|
+
}
|
|
2480
|
+
return this;
|
|
2481
|
+
}
|
|
2482
|
+
// builds a regex of the form /(alternative 1)|(alternative 2)|...|(alternative N)/
|
|
2483
|
+
compositeRegex() {
|
|
2484
|
+
if (M(this.patternInputMappings).isEmpty()) return void 0;
|
|
2485
|
+
const compositePattern = VP(this.patternInputMappings).m.keys().map((pattern) => `(${pattern})`).a.join("|").value;
|
|
2486
|
+
return new RegExp(compositePattern, "g");
|
|
2487
|
+
}
|
|
2488
|
+
// returns the first pattern that matches
|
|
2489
|
+
// the matching pattern will be a key in ioMappings
|
|
2490
|
+
firstMatchingPattern(matchStr) {
|
|
2491
|
+
return Array.from(this.patternInputMappings.keys()).find((pattern) => matchStr.match(pattern));
|
|
2492
|
+
}
|
|
2493
|
+
findReplacementForMatch(matchStr) {
|
|
2494
|
+
const matchingPattern = this.firstMatchingPattern(matchStr);
|
|
2495
|
+
if (!matchingPattern) return void 0;
|
|
2496
|
+
const replacementValue = this.patternInputMappings.get(matchingPattern);
|
|
2497
|
+
return replacementValue;
|
|
2498
|
+
}
|
|
2499
|
+
begin() {
|
|
2500
|
+
this.inputStreamForRegexMatcher = new Readable();
|
|
2501
|
+
this.inputStreamForRegexMatcher._read = () => {
|
|
2502
|
+
};
|
|
2503
|
+
const compositeRegex = this.compositeRegex();
|
|
2504
|
+
if (compositeRegex) {
|
|
2505
|
+
const streamRegex = new StreamRegex(compositeRegex);
|
|
2506
|
+
const matches2 = streamRegex.match(this.inputStreamForRegexMatcher);
|
|
2507
|
+
matches2.on("data", (matchStr) => {
|
|
2508
|
+
const inputToWrite = this.findReplacementForMatch(matchStr);
|
|
2509
|
+
if (inputToWrite !== void 0) {
|
|
2510
|
+
this.emitInput(inputToWrite);
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
handleInput(inputChunk) {
|
|
2516
|
+
if (this.inputStreamForRegexMatcher) {
|
|
2517
|
+
this.inputStreamForRegexMatcher.push(inputChunk);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
// either use setOutputToNewReadable XOR setOutputWriter; not both
|
|
2521
|
+
setOutputToNewReadable() {
|
|
2522
|
+
const stdinReadable = new Readable();
|
|
2523
|
+
this.setOutputWriter((str) => stdinReadable.push(str));
|
|
2524
|
+
stdinReadable._read = () => {
|
|
2525
|
+
};
|
|
2526
|
+
return stdinReadable;
|
|
2527
|
+
}
|
|
2528
|
+
// either use setOutputToNewReadable XOR setOutputWriter; not both
|
|
2529
|
+
setOutputWriter(writeFn) {
|
|
2530
|
+
this.writeFn = writeFn;
|
|
2531
|
+
}
|
|
2532
|
+
emitInput(input) {
|
|
2533
|
+
if (this.writeFn) {
|
|
2534
|
+
this.writeFn(input);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
end() {
|
|
2538
|
+
if (this.inputStreamForRegexMatcher) {
|
|
2539
|
+
this.inputStreamForRegexMatcher.push(null);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
// src/app.ts
|
|
2545
|
+
import * as util2 from "util";
|
|
2546
|
+
import chalk4 from "chalk";
|
|
2547
|
+
import { password as promptPassword } from "@inquirer/prompts";
|
|
2548
|
+
import { Listr } from "listr2";
|
|
2549
|
+
|
|
2550
|
+
// src/task.model.ts
|
|
2551
|
+
var Task = class {
|
|
2552
|
+
constructor(runFn, taskModuleAbsolutePath, description, name) {
|
|
2553
|
+
this.runFn = runFn;
|
|
2554
|
+
this.taskModuleAbsolutePath = taskModuleAbsolutePath;
|
|
2555
|
+
this.description = description;
|
|
2556
|
+
this.name = name;
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2560
|
+
// src/stacktrace.ts
|
|
2561
|
+
import StackTracey from "stacktracey";
|
|
2562
|
+
function callstack() {
|
|
2563
|
+
return new StackTracey();
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/runtime-base.ts
|
|
2567
|
+
import { Mutex as Mutex2 } from "async-mutex";
|
|
2568
|
+
|
|
2569
|
+
// src/command.ts
|
|
2570
|
+
import "shell-quote";
|
|
2571
|
+
import Shellwords from "shellwords-ts";
|
|
2572
|
+
var CommandResult = class {
|
|
2573
|
+
constructor(stdout, stderr, exitCode, signal) {
|
|
2574
|
+
this.stdout = stdout;
|
|
2575
|
+
this.stderr = stderr;
|
|
2576
|
+
this.exitCode = exitCode;
|
|
2577
|
+
this.signal = signal;
|
|
2578
|
+
}
|
|
2579
|
+
toJSON() {
|
|
2580
|
+
return {
|
|
2581
|
+
stdout: this.stdout,
|
|
2582
|
+
stderr: this.stderr,
|
|
2583
|
+
exitCode: this.exitCode,
|
|
2584
|
+
signal: this.signal,
|
|
2585
|
+
success: this.success,
|
|
2586
|
+
failure: this.failure
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
get out() {
|
|
2590
|
+
return this.stdout;
|
|
2591
|
+
}
|
|
2592
|
+
get err() {
|
|
2593
|
+
return this.stderr;
|
|
2594
|
+
}
|
|
2595
|
+
get exit() {
|
|
2596
|
+
return this.exitCode;
|
|
2597
|
+
}
|
|
2598
|
+
get sig() {
|
|
2599
|
+
return this.signal;
|
|
2600
|
+
}
|
|
2601
|
+
get success() {
|
|
2602
|
+
return this.exitCode === 0;
|
|
2603
|
+
}
|
|
2604
|
+
get failure() {
|
|
2605
|
+
return !this.success;
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
var Command = class {
|
|
2609
|
+
static parse(commandString, env) {
|
|
2610
|
+
if (!commandString || commandString.trim() === "") {
|
|
2611
|
+
throw new Error("Command string cannot be empty.");
|
|
2612
|
+
}
|
|
2613
|
+
const parts = Shellwords.split(commandString);
|
|
2614
|
+
const cmd_unsafe = A2.head(parts);
|
|
2615
|
+
if (cmd_unsafe === void 0) {
|
|
2616
|
+
throw new Error(`Failed to parse command from string: "${commandString}". Resulted in no command part.`);
|
|
2617
|
+
}
|
|
2618
|
+
const cmd = cmd_unsafe;
|
|
2619
|
+
const parsedArgs = A2(parts).skipFirst(1);
|
|
2620
|
+
const args = A2(parsedArgs).compact();
|
|
2621
|
+
return {
|
|
2622
|
+
cmd,
|
|
2623
|
+
args,
|
|
2624
|
+
env
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
cmd;
|
|
2628
|
+
args;
|
|
2629
|
+
cwd;
|
|
2630
|
+
env;
|
|
2631
|
+
result;
|
|
2632
|
+
constructor(opts) {
|
|
2633
|
+
const { cmd, args, cwd, env, result } = opts;
|
|
2634
|
+
this.cmd = cmd;
|
|
2635
|
+
this.args = args;
|
|
2636
|
+
this.cwd = cwd;
|
|
2637
|
+
this.env = env;
|
|
2638
|
+
this.result = result;
|
|
2639
|
+
}
|
|
2640
|
+
isRunning() {
|
|
2641
|
+
return !!this.result;
|
|
2642
|
+
}
|
|
2643
|
+
toJSON() {
|
|
2644
|
+
return {
|
|
2645
|
+
cmd: this.cmd,
|
|
2646
|
+
args: this.args,
|
|
2647
|
+
cwd: this.cwd,
|
|
2648
|
+
env: this.env,
|
|
2649
|
+
result: this.result?.toJSON()
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
// src/runtime-base.ts
|
|
2655
|
+
import { StringBuilder } from "typescript-string-operations";
|
|
2656
|
+
import chalk from "chalk";
|
|
2657
|
+
function task(runFn, options) {
|
|
2658
|
+
const moduleThatTaskIsDefinedIn = Path.new(callstack().items[2].file).absolute().toString();
|
|
2659
|
+
const taskName = options?.name || "anonymous_task";
|
|
2660
|
+
const taskInstance = new Task(runFn, moduleThatTaskIsDefinedIn, options?.description, taskName);
|
|
2661
|
+
const taskFnObject = function(params) {
|
|
2662
|
+
return function(parentInvocation) {
|
|
2663
|
+
return parentInvocation.invokeChildTask(taskFnObject, params ?? {});
|
|
2664
|
+
};
|
|
2665
|
+
};
|
|
2666
|
+
Object.assign(taskFnObject, { task: taskInstance });
|
|
2667
|
+
return taskFnObject;
|
|
2668
|
+
}
|
|
2669
|
+
var Invocation = class {
|
|
2670
|
+
constructor(runtime, taskFnDefinition, parent) {
|
|
2671
|
+
this.runtime = runtime;
|
|
2672
|
+
this.taskFnDefinition = taskFnDefinition;
|
|
2673
|
+
this.parent = parent;
|
|
2674
|
+
this.id = crypto.randomUUID();
|
|
2675
|
+
this.children = [];
|
|
2676
|
+
this.childrenMutex = new Mutex2();
|
|
2677
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
2678
|
+
this.result = promise;
|
|
2679
|
+
this.resolveResult = resolve;
|
|
2680
|
+
this.rejectResult = reject;
|
|
2681
|
+
this.stdoutSB = new StringBuilder();
|
|
2682
|
+
this.stderrSB = new StringBuilder();
|
|
2683
|
+
}
|
|
2684
|
+
id;
|
|
2685
|
+
children;
|
|
2686
|
+
// Children can have any params
|
|
2687
|
+
childrenMutex;
|
|
2688
|
+
description;
|
|
2689
|
+
stderrSB;
|
|
2690
|
+
stdoutSB;
|
|
2691
|
+
host;
|
|
2692
|
+
result;
|
|
2693
|
+
resolveResult;
|
|
2694
|
+
rejectResult;
|
|
2695
|
+
// Constructor and fields are above
|
|
2696
|
+
// Concrete method implementations
|
|
2697
|
+
getPassword() {
|
|
2698
|
+
return this.runtime.getPassword();
|
|
2699
|
+
}
|
|
2700
|
+
async getSecret(name) {
|
|
2701
|
+
const secret = this.runtime.app.config.getSecret(name);
|
|
2702
|
+
if (!secret) {
|
|
2703
|
+
return void 0;
|
|
2704
|
+
}
|
|
2705
|
+
return secret.plaintext();
|
|
2706
|
+
}
|
|
2707
|
+
get stderr() {
|
|
2708
|
+
return this.stderrSB.toString();
|
|
2709
|
+
}
|
|
2710
|
+
get stdout() {
|
|
2711
|
+
return this.stdoutSB.toString();
|
|
2712
|
+
}
|
|
2713
|
+
info(...message) {
|
|
2714
|
+
this.log(Verbosity.INFO, ...message);
|
|
2715
|
+
}
|
|
2716
|
+
debug(...message) {
|
|
2717
|
+
this.log(Verbosity.DEBUG, ...message);
|
|
2718
|
+
}
|
|
2719
|
+
warn(...message) {
|
|
2720
|
+
this.log(Verbosity.WARN, ...message);
|
|
2721
|
+
}
|
|
2722
|
+
error(...message) {
|
|
2723
|
+
this.log(Verbosity.ERROR, ...message);
|
|
2724
|
+
}
|
|
2725
|
+
async resultError() {
|
|
2726
|
+
try {
|
|
2727
|
+
await this.result;
|
|
2728
|
+
return void 0;
|
|
2729
|
+
} catch (e) {
|
|
2730
|
+
return e;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
setDescription(description) {
|
|
2734
|
+
this.description = description;
|
|
2735
|
+
}
|
|
2736
|
+
setFutureResult(resultPromise) {
|
|
2737
|
+
resultPromise.then(this.resolveResult, this.rejectResult);
|
|
2738
|
+
}
|
|
2739
|
+
getDescription() {
|
|
2740
|
+
return this.description;
|
|
2741
|
+
}
|
|
2742
|
+
getChildren() {
|
|
2743
|
+
return this.children;
|
|
2744
|
+
}
|
|
2745
|
+
getResultPromise() {
|
|
2746
|
+
return this.result;
|
|
2747
|
+
}
|
|
2748
|
+
exit(exitCode, message) {
|
|
2749
|
+
if (message) {
|
|
2750
|
+
this.runtime.app.warn(
|
|
2751
|
+
chalk.yellow(`Task ${this.description || this.id} requested exit with code ${exitCode}: ${message}`)
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
process.exit(exitCode);
|
|
2755
|
+
}
|
|
2756
|
+
inventory(tags) {
|
|
2757
|
+
return this.runtime.inventory(tags);
|
|
2758
|
+
}
|
|
2759
|
+
async appendChildInvocation(childInvocation) {
|
|
2760
|
+
await this.childrenMutex.runExclusive(async () => {
|
|
2761
|
+
this.children.push(childInvocation);
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
// Note: 'exit' has a concrete implementation above, so it's not abstract here.
|
|
2765
|
+
// Note: 'inventory' has a concrete implementation above, so it's not abstract here.
|
|
2766
|
+
};
|
|
2767
|
+
|
|
2768
|
+
// src/local-runtime.ts
|
|
2769
|
+
import Handlebars2 from "handlebars";
|
|
2770
|
+
import * as fs5 from "fs";
|
|
2771
|
+
|
|
2772
|
+
// src/ruspty-command.ts
|
|
2773
|
+
import { Pty } from "@replit/ruspty";
|
|
2774
|
+
import { StringBuilder as StringBuilder2 } from "typescript-string-operations";
|
|
2775
|
+
import "human-signals";
|
|
2776
|
+
var RusPtyCommand = class _RusPtyCommand extends Command {
|
|
2777
|
+
static fromArray(commandWithArgs, env, cwd) {
|
|
2778
|
+
const envObj = O(env || {}).map(([key, value]) => [key, String(value)]);
|
|
2779
|
+
const cmd = commandWithArgs[0] || "";
|
|
2780
|
+
const parsedArgs = commandWithArgs.slice(1);
|
|
2781
|
+
const args = parsedArgs.filter((arg) => arg !== null && arg !== void 0 && arg !== "");
|
|
2782
|
+
return new _RusPtyCommand({ cmd, args, cwd, env: envObj });
|
|
2783
|
+
}
|
|
2784
|
+
static fromString(command, env, cwd) {
|
|
2785
|
+
const envObj = O(env || {}).map(([key, value]) => [key, String(value)]);
|
|
2786
|
+
const { cmd, args } = this.parse(command, envObj);
|
|
2787
|
+
return new _RusPtyCommand({ cmd, args, cwd, env: envObj });
|
|
2788
|
+
}
|
|
2789
|
+
process;
|
|
2790
|
+
// Definite assignment assertion
|
|
2791
|
+
async run(interactionHandler = new InteractionHandler(), stdin) {
|
|
2792
|
+
const self = this;
|
|
2793
|
+
return new Promise(function(resolve, reject) {
|
|
2794
|
+
const stdout = new StringBuilder2();
|
|
2795
|
+
self.process = new Pty({
|
|
2796
|
+
command: self.cmd,
|
|
2797
|
+
args: self.args,
|
|
2798
|
+
dir: self.cwd,
|
|
2799
|
+
envs: self.env,
|
|
2800
|
+
interactive: true,
|
|
2801
|
+
size: { rows: 30, cols: 80 },
|
|
2802
|
+
onExit: (err, exitCode) => {
|
|
2803
|
+
interactionHandler.end();
|
|
2804
|
+
const signalObj = void 0;
|
|
2805
|
+
const commandResult = new CommandResult(stdout.toString(), "", exitCode, signalObj);
|
|
2806
|
+
self.result = commandResult;
|
|
2807
|
+
resolve(commandResult);
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
const read = self.process.read;
|
|
2811
|
+
const write = self.process.write;
|
|
2812
|
+
if (stdin) {
|
|
2813
|
+
write.write(stdin);
|
|
2814
|
+
}
|
|
2815
|
+
interactionHandler.begin();
|
|
2816
|
+
interactionHandler.setOutputWriter((str) => write.write(str));
|
|
2817
|
+
read.on("data", (chunk) => {
|
|
2818
|
+
stdout.append(chunk);
|
|
2819
|
+
interactionHandler.handleInput(chunk);
|
|
2820
|
+
});
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
// src/local-runtime.ts
|
|
2826
|
+
import chalk2 from "chalk";
|
|
2827
|
+
|
|
2828
|
+
// src/util/input-helpers.ts
|
|
2829
|
+
var CHECKMARK = "\u2713";
|
|
2830
|
+
var XMARK = "\u2717";
|
|
2831
|
+
var SUDO_PROMPT_REGEX = {
|
|
2832
|
+
/**
|
|
2833
|
+
* Matches the default sudo prompt on most Linux distributions.
|
|
2834
|
+
* Example: `[sudo] password for <username>:`
|
|
2835
|
+
*/
|
|
2836
|
+
LINUX: "\\[sudo\\] password",
|
|
2837
|
+
/**
|
|
2838
|
+
* Matches the default sudo prompt on macOS and some BSD systems.
|
|
2839
|
+
* Example: `Password:`
|
|
2840
|
+
*/
|
|
2841
|
+
MAC_BSD: "^Password:",
|
|
2842
|
+
/**
|
|
2843
|
+
* Matches the default prompt for `doas` on OpenBSD.
|
|
2844
|
+
* Example: `doas (username@hostname) password:`
|
|
2845
|
+
*/
|
|
2846
|
+
DOAS: "doas \\(.*\\) password:"
|
|
2847
|
+
};
|
|
2848
|
+
function withSudo(password, existingInputMap = {}) {
|
|
2849
|
+
if (!password) {
|
|
2850
|
+
return existingInputMap;
|
|
2851
|
+
}
|
|
2852
|
+
const passwordResponse = `${password}
|
|
2853
|
+
`;
|
|
2854
|
+
return {
|
|
2855
|
+
...existingInputMap,
|
|
2856
|
+
[SUDO_PROMPT_REGEX.LINUX]: passwordResponse,
|
|
2857
|
+
[SUDO_PROMPT_REGEX.MAC_BSD]: passwordResponse,
|
|
2858
|
+
[SUDO_PROMPT_REGEX.DOAS]: passwordResponse
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/local-runtime.ts
|
|
2863
|
+
var LocalInvocation = class _LocalInvocation extends Invocation {
|
|
2864
|
+
constructor(runtime, taskFnDefinition, params, parent) {
|
|
2865
|
+
super(runtime, taskFnDefinition, parent);
|
|
2866
|
+
this.runtime = runtime;
|
|
2867
|
+
this.taskFnDefinition = taskFnDefinition;
|
|
2868
|
+
this.description = Handlebars2.compile(
|
|
2869
|
+
taskFnDefinition.task.description || taskFnDefinition.task.name || "anonymous_local_task"
|
|
2870
|
+
)(params);
|
|
2871
|
+
this.config = this.runtime.app.config;
|
|
2872
|
+
this.file = {
|
|
2873
|
+
read: async (path3) => fs5.promises.readFile(path3, "utf-8"),
|
|
2874
|
+
write: async (path3, content, options) => fs5.promises.writeFile(path3, content, { mode: options?.mode }),
|
|
2875
|
+
exists: async (path3) => {
|
|
2876
|
+
try {
|
|
2877
|
+
await fs5.promises.access(path3);
|
|
2878
|
+
return true;
|
|
2879
|
+
} catch {
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
mkdir: async (path3, options) => {
|
|
2884
|
+
await fs5.promises.mkdir(path3, options);
|
|
2885
|
+
},
|
|
2886
|
+
rm: async (path3, options) => fs5.promises.rm(path3, options)
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
config;
|
|
2890
|
+
file;
|
|
2891
|
+
log(level, ...message) {
|
|
2892
|
+
this.runtime.app.log(level, ...message);
|
|
2893
|
+
}
|
|
2894
|
+
async exec(command, options = {}) {
|
|
2895
|
+
options = options || {};
|
|
2896
|
+
const interactionHandler = this.runtime.interactionHandler.clone();
|
|
2897
|
+
const stdinForCommand = options.stdin;
|
|
2898
|
+
if (options.input && typeof options.input === "object") {
|
|
2899
|
+
O(options.input).each(([pattern, value]) => {
|
|
2900
|
+
interactionHandler.map(pattern, value);
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
if (options.sudo) {
|
|
2904
|
+
const hostPassword = await this.runtime.getPassword();
|
|
2905
|
+
interactionHandler.mapInput(withSudo(hostPassword));
|
|
2906
|
+
}
|
|
2907
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2908
|
+
let finalEnv = {};
|
|
2909
|
+
if (options.env) {
|
|
2910
|
+
finalEnv = options.env;
|
|
2911
|
+
} else {
|
|
2912
|
+
Object.keys(process.env).forEach((key) => {
|
|
2913
|
+
const value = process.env[key];
|
|
2914
|
+
if (value !== void 0) {
|
|
2915
|
+
finalEnv[key] = value;
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
let commandInstance;
|
|
2920
|
+
if (command instanceof Array) {
|
|
2921
|
+
commandInstance = RusPtyCommand.fromArray(command, finalEnv, cwd);
|
|
2922
|
+
} else {
|
|
2923
|
+
commandInstance = RusPtyCommand.fromString(command, finalEnv, cwd);
|
|
2924
|
+
}
|
|
2925
|
+
this.log(
|
|
2926
|
+
Verbosity.DEBUG,
|
|
2927
|
+
`Executing command: ${Array.isArray(command) ? command.join(" ") : command}${options.stdin ? " with stdin" : ""}${options.input ? " with interaction patterns" : ""}`
|
|
2928
|
+
);
|
|
2929
|
+
await commandInstance.run(interactionHandler, stdinForCommand);
|
|
2930
|
+
const cmdResult = commandInstance.result;
|
|
2931
|
+
if (!cmdResult) {
|
|
2932
|
+
this.log(Verbosity.ERROR, "Command execution resulted in undefined result.");
|
|
2933
|
+
return new CommandResult("", "Command execution resulted in undefined result.", -1, void 0);
|
|
2934
|
+
}
|
|
2935
|
+
return cmdResult;
|
|
2936
|
+
}
|
|
2937
|
+
async ssh(tags, remoteTaskFn) {
|
|
2938
|
+
const targetHosts = this.runtime.app.querySelectedInventory(tags);
|
|
2939
|
+
if (targetHosts.length === 0) {
|
|
2940
|
+
this.runtime.app.warn(`No hosts found for tags: ${tags.join(", ")} in ssh call from ${this.id}`);
|
|
2941
|
+
return {};
|
|
2942
|
+
}
|
|
2943
|
+
const entryPromises = VP(targetHosts).map(async (host) => {
|
|
2944
|
+
const result = await this.runRemoteTaskOnHost(host, remoteTaskFn);
|
|
2945
|
+
return [host.hostname, result];
|
|
2946
|
+
}).value;
|
|
2947
|
+
const entries = await Promise.all(entryPromises);
|
|
2948
|
+
const record = Object.fromEntries(entries);
|
|
2949
|
+
return record;
|
|
2950
|
+
}
|
|
2951
|
+
async runRemoteTaskOnHost(host, remoteTaskFn) {
|
|
2952
|
+
this.debug(`Run function on: ${chalk2.yellow(V.inspect(host.toObject()))}`);
|
|
2953
|
+
const interactionHandler = new InteractionHandler(this.runtime.app.getSecretsForHost(host.hostname));
|
|
2954
|
+
const remoteRuntime = new RemoteRuntime(
|
|
2955
|
+
this.runtime.app,
|
|
2956
|
+
host,
|
|
2957
|
+
this.runtime,
|
|
2958
|
+
// The LocalRuntime instance of this LocalInvocation
|
|
2959
|
+
interactionHandler
|
|
2960
|
+
);
|
|
2961
|
+
try {
|
|
2962
|
+
const connected = await remoteRuntime.connect();
|
|
2963
|
+
if (!connected) {
|
|
2964
|
+
throw new Error(`Unable to connect to ${host.uri}`);
|
|
2965
|
+
}
|
|
2966
|
+
const adHocRemoteInvocationTaskFn = task(
|
|
2967
|
+
async function(context) {
|
|
2968
|
+
return remoteTaskFn(context);
|
|
2969
|
+
},
|
|
2970
|
+
{ description: `Ad-hoc task on ${host.uri} via ssh from ${this.taskFnDefinition.task.name} (${this.id})` }
|
|
2971
|
+
);
|
|
2972
|
+
const rootRemoteInvocation = new RemoteInvocation(
|
|
2973
|
+
remoteRuntime,
|
|
2974
|
+
adHocRemoteInvocationTaskFn,
|
|
2975
|
+
// This TaskFn is for the container of the ad-hoc operation on this host
|
|
2976
|
+
{},
|
|
2977
|
+
this,
|
|
2978
|
+
// Parent is the current LocalInvocation
|
|
2979
|
+
host
|
|
2980
|
+
);
|
|
2981
|
+
await this.appendChildInvocation(rootRemoteInvocation);
|
|
2982
|
+
const rootRemoteContext = this.runtime.app.taskContextForRunFn(
|
|
2983
|
+
rootRemoteInvocation,
|
|
2984
|
+
{},
|
|
2985
|
+
host
|
|
2986
|
+
);
|
|
2987
|
+
const resultPromise = remoteTaskFn(rootRemoteContext);
|
|
2988
|
+
rootRemoteInvocation.setFutureResult(resultPromise);
|
|
2989
|
+
rootRemoteInvocation.setDescription(`SSH to ${host.uri}`);
|
|
2990
|
+
return await resultPromise;
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2993
|
+
this.runtime.app.error(
|
|
2994
|
+
`Error during SSH operation to ${host.uri}: ${errorMessage}`,
|
|
2995
|
+
error instanceof Error ? error.stack : void 0
|
|
2996
|
+
);
|
|
2997
|
+
return error instanceof Error ? error : new Error(errorMessage);
|
|
2998
|
+
} finally {
|
|
2999
|
+
await remoteRuntime.disconnect();
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
exit(exitCode, message) {
|
|
3003
|
+
if (message) {
|
|
3004
|
+
this.log(exitCode === 0 ? Verbosity.INFO : Verbosity.ERROR, message);
|
|
3005
|
+
}
|
|
3006
|
+
this.runtime.app.warn(`Task ${this.id} requested exit with code ${exitCode}. Forcing process exit.`);
|
|
3007
|
+
process.exit(exitCode);
|
|
3008
|
+
}
|
|
3009
|
+
inventory(tags) {
|
|
3010
|
+
return this.runtime.app.queryInventory(new Set(tags));
|
|
3011
|
+
}
|
|
3012
|
+
async run(taskPartialFn) {
|
|
3013
|
+
return taskPartialFn(this);
|
|
3014
|
+
}
|
|
3015
|
+
async invokeChildTask(childTaskFnDefinition, params) {
|
|
3016
|
+
const childInvocation = new _LocalInvocation(this.runtime, childTaskFnDefinition, params, this);
|
|
3017
|
+
await this.appendChildInvocation(childInvocation);
|
|
3018
|
+
const loggedChildResultPromise = childInvocation.result.then((value) => {
|
|
3019
|
+
if (value instanceof Error) return value;
|
|
3020
|
+
if (value === void 0 || value === null) return void 0;
|
|
3021
|
+
return String(value);
|
|
3022
|
+
}).catch((error) => error);
|
|
3023
|
+
this.runtime.app.taskTree.add(
|
|
3024
|
+
childInvocation.id,
|
|
3025
|
+
this.id,
|
|
3026
|
+
childTaskFnDefinition.task.name || "anonymous_child_task",
|
|
3027
|
+
loggedChildResultPromise
|
|
3028
|
+
);
|
|
3029
|
+
const taskContext = this.runtime.app.taskContextForRunFn(
|
|
3030
|
+
childInvocation,
|
|
3031
|
+
params,
|
|
3032
|
+
void 0
|
|
3033
|
+
);
|
|
3034
|
+
try {
|
|
3035
|
+
const result = await childTaskFnDefinition.task.runFn.call(childInvocation, taskContext);
|
|
3036
|
+
childInvocation.resolveResult(result);
|
|
3037
|
+
return result;
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
childInvocation.rejectResult(error);
|
|
3040
|
+
throw error;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
var LocalRuntime = class {
|
|
3045
|
+
constructor(app, localBundlePath, interactionHandler = new InteractionHandler()) {
|
|
3046
|
+
this.app = app;
|
|
3047
|
+
this.localBundlePath = localBundlePath;
|
|
3048
|
+
this.interactionHandler = interactionHandler;
|
|
3049
|
+
const appConfigInstance = this.app.config;
|
|
3050
|
+
if (appConfigInstance instanceof ConfigFile2) {
|
|
3051
|
+
this.host = new Host(appConfigInstance, { hostname: "localhost" });
|
|
3052
|
+
} else {
|
|
3053
|
+
const configType = appConfigInstance?.constructor?.name || typeof appConfigInstance;
|
|
3054
|
+
this.app.error(
|
|
3055
|
+
`CRITICAL ERROR: LocalRuntime could not initialize its 'localhost' Host object. The application's configuration (type: ${configType}) is not a file-based configuration (expected ConfigFile).`
|
|
3056
|
+
);
|
|
3057
|
+
throw new Error(
|
|
3058
|
+
`LocalRuntime init failed: Expected app.config to be an instance of ConfigFile for 'localhost' Host, but got ${configType}.`
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
this.config = {
|
|
3062
|
+
cwd: process.cwd(),
|
|
3063
|
+
configFile: appConfigInstance instanceof ConfigFile2 ? appConfigInstance : void 0
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
config;
|
|
3067
|
+
host;
|
|
3068
|
+
// Represents the local machine
|
|
3069
|
+
memoizedPassword;
|
|
3070
|
+
get tmpDirRootPath() {
|
|
3071
|
+
return this.app.hostctlTmpDir();
|
|
3072
|
+
}
|
|
3073
|
+
async getPassword() {
|
|
3074
|
+
if (this.memoizedPassword) {
|
|
3075
|
+
return this.memoizedPassword;
|
|
3076
|
+
}
|
|
3077
|
+
this.memoizedPassword = await this.app.promptPassword("Enter local sudo password:");
|
|
3078
|
+
return this.memoizedPassword;
|
|
3079
|
+
}
|
|
3080
|
+
async getSecret(name) {
|
|
3081
|
+
const secret = this.app.config.getSecret(name);
|
|
3082
|
+
if (!secret) {
|
|
3083
|
+
this.app.warn(`Secret ${name} not found in config.`);
|
|
3084
|
+
return void 0;
|
|
3085
|
+
}
|
|
3086
|
+
return secret.plaintext();
|
|
3087
|
+
}
|
|
3088
|
+
getTmpDir() {
|
|
3089
|
+
return this.app.createNamedTmpDir();
|
|
3090
|
+
}
|
|
3091
|
+
getTmpFile(prefix, postfix, dir) {
|
|
3092
|
+
const tmpDir = dir || this.getTmpDir();
|
|
3093
|
+
const randomName = prefix ? `${prefix}-${crypto.randomUUID()}` : crypto.randomUUID();
|
|
3094
|
+
const finalName = postfix ? `${randomName}-${postfix}` : randomName;
|
|
3095
|
+
return tmpDir.join(finalName);
|
|
3096
|
+
}
|
|
3097
|
+
inventory(tags) {
|
|
3098
|
+
return this.app.queryInventory(new Set(tags));
|
|
3099
|
+
}
|
|
3100
|
+
async invokeRootTask(taskFnDefinition, params, description) {
|
|
3101
|
+
const rootInvocation = new LocalInvocation(
|
|
3102
|
+
this,
|
|
3103
|
+
taskFnDefinition,
|
|
3104
|
+
params,
|
|
3105
|
+
void 0
|
|
3106
|
+
);
|
|
3107
|
+
if (description) {
|
|
3108
|
+
rootInvocation.description = description;
|
|
3109
|
+
}
|
|
3110
|
+
const loggedRootResultPromise = rootInvocation.result.then((value) => {
|
|
3111
|
+
if (value instanceof Error) return value;
|
|
3112
|
+
if (value === void 0 || value === null) return void 0;
|
|
3113
|
+
return String(value);
|
|
3114
|
+
}).catch((error) => error);
|
|
3115
|
+
this.app.taskTree.add(
|
|
3116
|
+
rootInvocation.id,
|
|
3117
|
+
null,
|
|
3118
|
+
taskFnDefinition.task.name || "anonymous_root_task",
|
|
3119
|
+
loggedRootResultPromise
|
|
3120
|
+
);
|
|
3121
|
+
const taskContext = this.app.taskContextForRunFn(
|
|
3122
|
+
rootInvocation,
|
|
3123
|
+
params,
|
|
3124
|
+
void 0
|
|
3125
|
+
);
|
|
3126
|
+
taskFnDefinition.task.runFn.call(rootInvocation, taskContext).then(rootInvocation.resolveResult).catch(rootInvocation.rejectResult);
|
|
3127
|
+
return rootInvocation;
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
// src/remote-runtime.ts
|
|
3132
|
+
import Handlebars3 from "handlebars";
|
|
3133
|
+
import "path";
|
|
3134
|
+
|
|
3135
|
+
// src/ssh-session.ts
|
|
3136
|
+
import { signalsByName } from "human-signals";
|
|
3137
|
+
import { NodeSSH } from "node-ssh";
|
|
3138
|
+
import { PassThrough } from "stream";
|
|
3139
|
+
var SSHSession = class {
|
|
3140
|
+
ssh;
|
|
3141
|
+
constructor() {
|
|
3142
|
+
this.ssh = new NodeSSH();
|
|
3143
|
+
}
|
|
3144
|
+
connect(sshConnectOpts) {
|
|
3145
|
+
return this.ssh.connect(sshConnectOpts);
|
|
3146
|
+
}
|
|
3147
|
+
async deleteFile(remotePath) {
|
|
3148
|
+
const self = this;
|
|
3149
|
+
return new Promise(function(resolve, reject) {
|
|
3150
|
+
self.ssh.withSFTP(async (sftp) => {
|
|
3151
|
+
sftp.unlink(remotePath, (err) => {
|
|
3152
|
+
if (err) {
|
|
3153
|
+
const errnoError = err;
|
|
3154
|
+
if (errnoError.errno === 2 || errnoError.code === "ENOENT") {
|
|
3155
|
+
resolve(true);
|
|
3156
|
+
} else {
|
|
3157
|
+
const errnoError2 = err;
|
|
3158
|
+
const errorCodeDisplay = errnoError2.code !== void 0 ? errnoError2.code : errnoError2.errno !== void 0 ? errnoError2.errno : "N/A";
|
|
3159
|
+
reject(`${errnoError2.message} (code ${errorCodeDisplay}) ${remotePath}`);
|
|
3160
|
+
}
|
|
3161
|
+
} else {
|
|
3162
|
+
resolve(true);
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
});
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
async sftp() {
|
|
3169
|
+
return this.ssh.requestSFTP();
|
|
3170
|
+
}
|
|
3171
|
+
async execCommand(command, options) {
|
|
3172
|
+
const nodeSshCmdOptions = {
|
|
3173
|
+
// Using 'any' temporarily to build up, then will cast or ensure type compatibility
|
|
3174
|
+
cwd: options?.cwd,
|
|
3175
|
+
stdin: typeof options?.stdin === "string" ? options.stdin : Buffer.isBuffer(options?.stdin) ? options.stdin.toString() : void 0
|
|
3176
|
+
};
|
|
3177
|
+
if (options?.pty !== void 0) {
|
|
3178
|
+
nodeSshCmdOptions.options = {
|
|
3179
|
+
...nodeSshCmdOptions.options || {},
|
|
3180
|
+
// Preserve other potential ssh2 options if they were there
|
|
3181
|
+
pty: options.pty
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
const result = await this.ssh.execCommand(command, nodeSshCmdOptions);
|
|
3185
|
+
const exitCode = result.code === null ? -1 : result.code;
|
|
3186
|
+
const signalString = result.signal;
|
|
3187
|
+
const signalObject = signalString ? signalsByName[signalString] : void 0;
|
|
3188
|
+
const commandResult = new CommandResult(result.stdout, result.stderr, exitCode, signalObject);
|
|
3189
|
+
return new Command({ cmd: command, args: [], cwd: options?.cwd, env: options?.env, result: commandResult });
|
|
3190
|
+
}
|
|
3191
|
+
async getFile(remotePath, localPath) {
|
|
3192
|
+
await this.ssh.getFile(localPath, remotePath);
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Executes a command on the remote host.
|
|
3196
|
+
* @param command The command to execute. Can be a string or an array of strings. If an array, the first element is
|
|
3197
|
+
* the command and the rest are arguments. Otherwise, the string is parsed as a command.
|
|
3198
|
+
* @param interactionHandler The interaction handler to use.
|
|
3199
|
+
* @param execOptions The options for the execution.
|
|
3200
|
+
* @returns A promise that resolves to the result of the command.
|
|
3201
|
+
*/
|
|
3202
|
+
async run(command, interactionHandler = new InteractionHandler(), execOptions = {}) {
|
|
3203
|
+
const self = this;
|
|
3204
|
+
const combinedStdin = new PassThrough();
|
|
3205
|
+
if (execOptions.stdin) {
|
|
3206
|
+
combinedStdin.write(execOptions.stdin);
|
|
3207
|
+
}
|
|
3208
|
+
interactionHandler.setOutputToNewReadable().pipe(combinedStdin);
|
|
3209
|
+
interactionHandler.begin();
|
|
3210
|
+
let cmd, args;
|
|
3211
|
+
if (command instanceof Array) {
|
|
3212
|
+
cmd = command[0] || "";
|
|
3213
|
+
args = command.slice(1).filter((arg) => !!arg);
|
|
3214
|
+
} else {
|
|
3215
|
+
const parsedCommand = Command.parse(command);
|
|
3216
|
+
cmd = parsedCommand.cmd;
|
|
3217
|
+
args = parsedCommand.args;
|
|
3218
|
+
}
|
|
3219
|
+
const ptyOption = execOptions.pty ?? true;
|
|
3220
|
+
const { stdout, stderr, code, signal } = await self.ssh.exec(cmd, args, {
|
|
3221
|
+
cwd: execOptions.cwd,
|
|
3222
|
+
execOptions: { pty: ptyOption, env: execOptions.env },
|
|
3223
|
+
stream: "both",
|
|
3224
|
+
stdin: combinedStdin,
|
|
3225
|
+
onStdout: (chunk) => {
|
|
3226
|
+
const output = chunk.toString("utf8");
|
|
3227
|
+
interactionHandler.handleInput(output);
|
|
3228
|
+
},
|
|
3229
|
+
onStderr: (chunk) => {
|
|
3230
|
+
const errorOutput = chunk.toString("utf8");
|
|
3231
|
+
interactionHandler.handleInput(errorOutput);
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
interactionHandler.end();
|
|
3235
|
+
const signalName = signal;
|
|
3236
|
+
const signalObj = signalName ? signalsByName[signalName] : void 0;
|
|
3237
|
+
const result = new CommandResult(stdout, stderr, code || 0, signalObj);
|
|
3238
|
+
return new Command({ cmd, args, cwd: execOptions.cwd, env: execOptions.env, result });
|
|
3239
|
+
}
|
|
3240
|
+
async sendFile(localPath, remotePath) {
|
|
3241
|
+
try {
|
|
3242
|
+
await this.ssh.putFile(localPath, remotePath);
|
|
3243
|
+
return true;
|
|
3244
|
+
} catch (e) {
|
|
3245
|
+
return false;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
async disconnect() {
|
|
3249
|
+
this.ssh.dispose();
|
|
3250
|
+
}
|
|
3251
|
+
isConnected() {
|
|
3252
|
+
return this.ssh.isConnected();
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
|
|
3256
|
+
// src/remote-runtime.ts
|
|
3257
|
+
import "chalk";
|
|
3258
|
+
import "util";
|
|
3259
|
+
import "typescript-string-operations";
|
|
3260
|
+
import { match, P } from "ts-pattern";
|
|
3261
|
+
var RemoteInvocation = class _RemoteInvocation extends Invocation {
|
|
3262
|
+
// sshSession is removed from RemoteInvocation, it will be managed by RemoteRuntime
|
|
3263
|
+
constructor(runtime, taskFnDefinition, params, parent, hostInstance) {
|
|
3264
|
+
super(runtime, taskFnDefinition, parent);
|
|
3265
|
+
this.taskFnDefinition = taskFnDefinition;
|
|
3266
|
+
this.parent = parent;
|
|
3267
|
+
this.hostInstance = hostInstance;
|
|
3268
|
+
this.description = Handlebars3.compile(
|
|
3269
|
+
taskFnDefinition.task.description || taskFnDefinition.task.name || "anonymous_remote_task"
|
|
3270
|
+
)(params);
|
|
3271
|
+
this.host = hostInstance || parent.host;
|
|
3272
|
+
if (!this.host) {
|
|
3273
|
+
throw new Error("RemoteInvocation must have a host.");
|
|
3274
|
+
}
|
|
3275
|
+
this.config = this.runtime.app.config;
|
|
3276
|
+
this.file = {
|
|
3277
|
+
read: async (filePath) => {
|
|
3278
|
+
const sftp = await this.ensureSftp();
|
|
3279
|
+
const remotePath = this.resolveRemotePath(filePath);
|
|
3280
|
+
return new Promise((resolve, reject) => {
|
|
3281
|
+
sftp.readFile(remotePath, (err, data) => {
|
|
3282
|
+
if (err) {
|
|
3283
|
+
return reject(err);
|
|
3284
|
+
}
|
|
3285
|
+
if (!data) {
|
|
3286
|
+
return reject(new Error(`SFTP readFile returned no data for ${remotePath}`));
|
|
3287
|
+
}
|
|
3288
|
+
resolve(data.toString("utf-8"));
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
},
|
|
3292
|
+
write: async (filePath, content, options) => {
|
|
3293
|
+
const sftp = await this.ensureSftp();
|
|
3294
|
+
const remotePath = this.resolveRemotePath(filePath);
|
|
3295
|
+
await sftp.writeFile(remotePath, content, { mode: options?.mode });
|
|
3296
|
+
},
|
|
3297
|
+
exists: async (filePath) => {
|
|
3298
|
+
const sftp = await this.ensureSftp();
|
|
3299
|
+
const remotePath = this.resolveRemotePath(filePath);
|
|
3300
|
+
return new Promise((resolve, reject) => {
|
|
3301
|
+
sftp.stat(remotePath, (err, stats) => {
|
|
3302
|
+
if (err) {
|
|
3303
|
+
const sftpError = err;
|
|
3304
|
+
if (sftpError.code === 2 || sftpError.code === "ENOENT" || typeof err.message === "string" && (err.message.includes("No such file") || err.message.includes("No such entity"))) {
|
|
3305
|
+
resolve(false);
|
|
3306
|
+
} else {
|
|
3307
|
+
reject(err);
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
resolve(true);
|
|
3311
|
+
}
|
|
3312
|
+
});
|
|
3313
|
+
});
|
|
3314
|
+
},
|
|
3315
|
+
mkdir: async (dirPath, options) => {
|
|
3316
|
+
const remoteDir = this.resolveRemotePath(dirPath);
|
|
3317
|
+
let command = `mkdir ${options?.recursive ? "-p " : ""}${this.quoteRemoteArg(remoteDir)}`;
|
|
3318
|
+
await this.exec(command);
|
|
3319
|
+
if (options?.mode) {
|
|
3320
|
+
await this.exec(`chmod ${this.quoteRemoteArg(String(options.mode))} ${this.quoteRemoteArg(remoteDir)}`);
|
|
3321
|
+
}
|
|
3322
|
+
},
|
|
3323
|
+
rm: async (itemPath, options) => {
|
|
3324
|
+
const remoteItem = this.resolveRemotePath(itemPath);
|
|
3325
|
+
const command = `rm ${options?.recursive ? "-r " : ""}${options?.force ? "-f " : ""}${this.quoteRemoteArg(remoteItem)}`;
|
|
3326
|
+
await this.exec(command);
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
config;
|
|
3331
|
+
file;
|
|
3332
|
+
async ensureSftp() {
|
|
3333
|
+
if (!(this.runtime instanceof RemoteRuntime)) {
|
|
3334
|
+
throw new Error("Runtime is not a RemoteRuntime instance, cannot ensure SFTP.");
|
|
3335
|
+
}
|
|
3336
|
+
return this.runtime.getSftpClient();
|
|
3337
|
+
}
|
|
3338
|
+
resolveRemotePath(p) {
|
|
3339
|
+
return p;
|
|
3340
|
+
}
|
|
3341
|
+
quoteRemoteArg(arg) {
|
|
3342
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
3343
|
+
}
|
|
3344
|
+
log(level, ...message) {
|
|
3345
|
+
const hostUri = this.host ? `${this.host.user || ""}@${this.host.hostname}` : "unknown-host";
|
|
3346
|
+
this.runtime.app.log(level, `[${hostUri}|${this.id}|${this.description || "task"}]`, ...message);
|
|
3347
|
+
}
|
|
3348
|
+
uri() {
|
|
3349
|
+
if (!this.host) throw new Error("Host is not defined for this remote invocation.");
|
|
3350
|
+
return `${this.host.user || this.runtime.app.defaultSshUser()}@${this.host.hostname}`;
|
|
3351
|
+
}
|
|
3352
|
+
async exec(command, options) {
|
|
3353
|
+
if (!this.host) throw new Error("Host is not defined for exec command in RemoteInvocation.");
|
|
3354
|
+
if (!(this.runtime instanceof RemoteRuntime)) {
|
|
3355
|
+
throw new Error("Runtime is not a RemoteRuntime instance, cannot execute remote command.");
|
|
3356
|
+
}
|
|
3357
|
+
const stdinForRemoteCommand = options?.stdin;
|
|
3358
|
+
const execSpecificInteractionHandler = this.runtime.interactionHandler.clone();
|
|
3359
|
+
if (options?.input) {
|
|
3360
|
+
execSpecificInteractionHandler.mapInput(options.input);
|
|
3361
|
+
}
|
|
3362
|
+
if (options?.sudo) {
|
|
3363
|
+
const hostPassword = await this.runtime.getPassword();
|
|
3364
|
+
execSpecificInteractionHandler.mapInput(withSudo(hostPassword));
|
|
3365
|
+
}
|
|
3366
|
+
this.debug(`Executing remote command on ${this.host.hostname}: ${command}`);
|
|
3367
|
+
const cmdOrErr = await this.runtime.executeCommand(command, {
|
|
3368
|
+
cwd: options?.cwd,
|
|
3369
|
+
stdin: stdinForRemoteCommand,
|
|
3370
|
+
pty: options?.pty,
|
|
3371
|
+
env: options?.env,
|
|
3372
|
+
interactionHandler: execSpecificInteractionHandler
|
|
3373
|
+
// Pass the potentially augmented handler
|
|
3374
|
+
});
|
|
3375
|
+
return match(cmdOrErr).with(P.instanceOf(Error), (err) => {
|
|
3376
|
+
throw err;
|
|
3377
|
+
}).with(P.instanceOf(Command), (command2) => {
|
|
3378
|
+
return command2.result;
|
|
3379
|
+
}).exhaustive();
|
|
3380
|
+
}
|
|
3381
|
+
exit(exitCode, message) {
|
|
3382
|
+
const hostUri = this.host ? `${this.host.user || ""}@${this.host.hostname}` : "unknown-host";
|
|
3383
|
+
const msg = `Remote task ${this.id} on ${hostUri} requested exit with code ${exitCode}${message ? ": " + message : ""}`;
|
|
3384
|
+
this.log(Verbosity.WARN, msg);
|
|
3385
|
+
throw new Error(msg);
|
|
3386
|
+
}
|
|
3387
|
+
inventory(tags) {
|
|
3388
|
+
return this.runtime.app.queryInventory(new Set(tags));
|
|
3389
|
+
}
|
|
3390
|
+
async run(taskPartialFn) {
|
|
3391
|
+
return taskPartialFn(this);
|
|
3392
|
+
}
|
|
3393
|
+
async invokeChildTask(childTaskFnDefinition, params) {
|
|
3394
|
+
if (!this.host) throw new Error("Host is not defined for invokeChildTask in RemoteInvocation.");
|
|
3395
|
+
const childInvocation = new _RemoteInvocation(this.runtime, childTaskFnDefinition, params, this, this.host);
|
|
3396
|
+
await this.appendChildInvocation(childInvocation);
|
|
3397
|
+
this.runtime.app.taskTree.add(
|
|
3398
|
+
childInvocation.id,
|
|
3399
|
+
this.id,
|
|
3400
|
+
childTaskFnDefinition.task.name || "anonymous_remote_child_task",
|
|
3401
|
+
childInvocation.result.then((r) => r === null ? void 0 : r)
|
|
3402
|
+
);
|
|
3403
|
+
const taskContext = this.runtime.app.taskContextForRunFn(
|
|
3404
|
+
childInvocation,
|
|
3405
|
+
params,
|
|
3406
|
+
this.host
|
|
3407
|
+
);
|
|
3408
|
+
try {
|
|
3409
|
+
const result = await childTaskFnDefinition.task.runFn.call(childInvocation, taskContext);
|
|
3410
|
+
childInvocation.resolveResult(result);
|
|
3411
|
+
return result;
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
childInvocation.rejectResult(error);
|
|
3414
|
+
throw error;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
// End of invokeChildTask method
|
|
3418
|
+
ssh(tags, remoteTaskFn) {
|
|
3419
|
+
throw new Error("ssh from a remote host is not currently supported.");
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
var RemoteRuntime = class {
|
|
3423
|
+
sftpClientInstance;
|
|
3424
|
+
// Type should be SFTPWrapper from 'node-ssh'
|
|
3425
|
+
app;
|
|
3426
|
+
host;
|
|
3427
|
+
interactionHandler;
|
|
3428
|
+
sshSession;
|
|
3429
|
+
constructor(app, host, _localRuntime, interactionHandler = new InteractionHandler()) {
|
|
3430
|
+
this.app = app;
|
|
3431
|
+
this.host = host;
|
|
3432
|
+
this.interactionHandler = interactionHandler;
|
|
3433
|
+
this.sshSession = new SSHSession();
|
|
3434
|
+
}
|
|
3435
|
+
get tmpDirRootPath() {
|
|
3436
|
+
return new Path("/tmp");
|
|
3437
|
+
}
|
|
3438
|
+
async getPassword() {
|
|
3439
|
+
return await this.host.decryptedPassword();
|
|
3440
|
+
}
|
|
3441
|
+
async getSecret(name) {
|
|
3442
|
+
const secret = this.app.config.getSecret(name);
|
|
3443
|
+
if (!secret) {
|
|
3444
|
+
this.app.warn(`Secret ${name} not found in config.`);
|
|
3445
|
+
return void 0;
|
|
3446
|
+
}
|
|
3447
|
+
return secret.plaintext();
|
|
3448
|
+
}
|
|
3449
|
+
getTmpDir() {
|
|
3450
|
+
return this.tmpDirRootPath.join(this.app.randName());
|
|
3451
|
+
}
|
|
3452
|
+
getTmpFile(prefix, postfix, dir) {
|
|
3453
|
+
const tmpDir = dir || this.getTmpDir();
|
|
3454
|
+
return tmpDir.join(this.app.randName());
|
|
3455
|
+
}
|
|
3456
|
+
inventory(tags) {
|
|
3457
|
+
return this.app.queryInventory(new Set(tags));
|
|
3458
|
+
}
|
|
3459
|
+
async connect() {
|
|
3460
|
+
if (this.sshSession.isConnected() && this.sftpClientInstance) {
|
|
3461
|
+
this.app.debug(`RemoteRuntime: SSH session and SFTP already connected to ${this.host.uri}`);
|
|
3462
|
+
return true;
|
|
3463
|
+
}
|
|
3464
|
+
this.app.debug(`RemoteRuntime: Connecting SSH to ${this.host.uri}...`);
|
|
3465
|
+
const sshConnectOpts = {
|
|
3466
|
+
host: this.host.hostname,
|
|
3467
|
+
// Changed from this.host.host
|
|
3468
|
+
username: this.host.user || this.app.defaultSshUser(),
|
|
3469
|
+
// Assuming defaultSshUser on App
|
|
3470
|
+
port: this.host.port,
|
|
3471
|
+
// node-ssh parses port from host string if present
|
|
3472
|
+
privateKey: await this.host.plaintextSshKeyPath(),
|
|
3473
|
+
password: await this.host.decryptedPassword()
|
|
3474
|
+
};
|
|
3475
|
+
try {
|
|
3476
|
+
if (!this.sshSession.isConnected()) {
|
|
3477
|
+
await this.sshSession.connect(sshConnectOpts);
|
|
3478
|
+
this.app.debug(`RemoteRuntime: SSH connected to ${this.host.uri}`);
|
|
3479
|
+
}
|
|
3480
|
+
this.app.debug(`RemoteRuntime: Requesting SFTP client for ${this.host.uri}`);
|
|
3481
|
+
this.sftpClientInstance = await this.sshSession.sftp();
|
|
3482
|
+
this.app.debug(`RemoteRuntime: SFTP client ready for ${this.host.uri}`);
|
|
3483
|
+
} catch (e) {
|
|
3484
|
+
this.app.error(`RemoteRuntime: SSH connection or SFTP setup to ${this.host.uri} failed: ${e.message}`);
|
|
3485
|
+
if (this.sshSession.isConnected()) {
|
|
3486
|
+
await this.sshSession.disconnect().catch((err) => {
|
|
3487
|
+
this.app.error(`RemoteRuntime: Error during disconnect after failed connect/sftp: ${err.message}`);
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
this.sftpClientInstance = void 0;
|
|
3491
|
+
throw e;
|
|
3492
|
+
}
|
|
3493
|
+
return this.sshSession.isConnected();
|
|
3494
|
+
}
|
|
3495
|
+
async getSftpClient() {
|
|
3496
|
+
await this.connect();
|
|
3497
|
+
if (!this.sftpClientInstance) {
|
|
3498
|
+
throw new Error(`SFTP client not available on SSHSession for ${this.host.uri} after connect call.`);
|
|
3499
|
+
}
|
|
3500
|
+
return this.sftpClientInstance;
|
|
3501
|
+
}
|
|
3502
|
+
async executeCommand(command, options) {
|
|
3503
|
+
this.app.debug(`RemoteRuntime: Executing on ${this.host.uri}: ${command}`);
|
|
3504
|
+
const handlerToUse = options?.interactionHandler || this.interactionHandler;
|
|
3505
|
+
let cmdOrErr;
|
|
3506
|
+
try {
|
|
3507
|
+
if (options?.pty || handlerToUse !== this.interactionHandler) {
|
|
3508
|
+
this.app.debug(`RemoteRuntime: Executing command on ${this.host.uri} via sshSession.run (PTY/custom handler)`);
|
|
3509
|
+
const commandObj = await this.sshSession.run(command, handlerToUse, {
|
|
3510
|
+
cwd: options?.cwd,
|
|
3511
|
+
stdin: options?.stdin,
|
|
3512
|
+
pty: options?.pty,
|
|
3513
|
+
env: options?.env
|
|
3514
|
+
});
|
|
3515
|
+
cmdOrErr = commandObj;
|
|
3516
|
+
} else {
|
|
3517
|
+
this.app.debug(
|
|
3518
|
+
`RemoteRuntime: Executing command on ${this.host.uri} via sshSession.execCommand (no PTY/default handler)`
|
|
3519
|
+
);
|
|
3520
|
+
const actualCommand = Array.isArray(command) ? command.join(" ") : command;
|
|
3521
|
+
const commandObj = await this.sshSession.execCommand(actualCommand, {
|
|
3522
|
+
cwd: options?.cwd,
|
|
3523
|
+
stdin: options?.stdin,
|
|
3524
|
+
env: options?.env
|
|
3525
|
+
});
|
|
3526
|
+
cmdOrErr = commandObj;
|
|
3527
|
+
}
|
|
3528
|
+
this.app.logHostCommandResult(this.host, command, cmdOrErr);
|
|
3529
|
+
} catch (e) {
|
|
3530
|
+
this.app.error(`RemoteRuntime: executeCommand failed for '${command}': ${e.message}`);
|
|
3531
|
+
this.app.error(e.stack);
|
|
3532
|
+
cmdOrErr = e;
|
|
3533
|
+
this.app.logHostCommandResult(this.host, command, cmdOrErr);
|
|
3534
|
+
}
|
|
3535
|
+
return cmdOrErr;
|
|
3536
|
+
}
|
|
3537
|
+
async disconnect() {
|
|
3538
|
+
if (this.sshSession.isConnected()) {
|
|
3539
|
+
this.app.debug(`RemoteRuntime: Disconnecting SSH from ${this.host.uri}`);
|
|
3540
|
+
this.sftpClientInstance = void 0;
|
|
3541
|
+
await this.sshSession.disconnect();
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
async invokeRootTask(taskFnDefinition, params) {
|
|
3545
|
+
await this.connect();
|
|
3546
|
+
if (!this.sshSession) throw new Error("SSH session not established for invokeRootTask.");
|
|
3547
|
+
const rootInvocation = new RemoteInvocation(
|
|
3548
|
+
this,
|
|
3549
|
+
taskFnDefinition,
|
|
3550
|
+
params,
|
|
3551
|
+
{ runtime: this, host: this.host },
|
|
3552
|
+
this.host
|
|
3553
|
+
);
|
|
3554
|
+
this.app.taskTree.add(
|
|
3555
|
+
rootInvocation.id,
|
|
3556
|
+
null,
|
|
3557
|
+
taskFnDefinition.task.name || `remote_root_task_on_${this.host.hostname}`,
|
|
3558
|
+
// Changed host to hostname
|
|
3559
|
+
rootInvocation.result.then((r) => r === null ? void 0 : r)
|
|
3560
|
+
);
|
|
3561
|
+
const taskContext = this.app.taskContextForRunFn(rootInvocation, params, this.host);
|
|
3562
|
+
taskFnDefinition.task.runFn.call(rootInvocation, taskContext).then(rootInvocation.resolveResult).catch(rootInvocation.rejectResult);
|
|
3563
|
+
return rootInvocation;
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
|
|
3567
|
+
// src/node-runtime.ts
|
|
3568
|
+
import os2 from "os";
|
|
3569
|
+
import axios from "axios";
|
|
3570
|
+
import * as cheerio from "cheerio";
|
|
3571
|
+
import { match as match3 } from "ts-pattern";
|
|
3572
|
+
import which from "which";
|
|
3573
|
+
|
|
3574
|
+
// src/http.ts
|
|
3575
|
+
import { createWriteStream, unlink } from "fs";
|
|
3576
|
+
import { get } from "https";
|
|
3577
|
+
async function downloadFile(url, dest) {
|
|
3578
|
+
return new Promise((resolve, reject) => {
|
|
3579
|
+
const file = createWriteStream(dest, { flags: "wx" });
|
|
3580
|
+
const request = get(url, (response) => {
|
|
3581
|
+
if (response.statusCode === 200) {
|
|
3582
|
+
response.pipe(file);
|
|
3583
|
+
} else {
|
|
3584
|
+
file.close();
|
|
3585
|
+
unlink(dest, () => {
|
|
3586
|
+
});
|
|
3587
|
+
reject(`Server responded with ${response.statusCode}: ${response.statusMessage}`);
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
request.on("error", (err) => {
|
|
3591
|
+
file.close();
|
|
3592
|
+
unlink(dest, () => {
|
|
3593
|
+
});
|
|
3594
|
+
reject(err.message);
|
|
3595
|
+
});
|
|
3596
|
+
file.on("finish", () => {
|
|
3597
|
+
resolve(dest.toString());
|
|
3598
|
+
});
|
|
3599
|
+
file.on("error", (err) => {
|
|
3600
|
+
file.close();
|
|
3601
|
+
if (err.code === "EEXIST") {
|
|
3602
|
+
reject(new Error("File already exists"));
|
|
3603
|
+
} else {
|
|
3604
|
+
unlink(dest, () => {
|
|
3605
|
+
});
|
|
3606
|
+
reject(err.message);
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
// src/shell-command.ts
|
|
3613
|
+
import spawnAsync2 from "@expo/spawn-async";
|
|
3614
|
+
import { signalsByName as signalsByName2 } from "human-signals";
|
|
3615
|
+
var ShellCommand = class _ShellCommand extends Command {
|
|
3616
|
+
static fromString(command, env, cwd) {
|
|
3617
|
+
const { cmd, args } = this.parse(command, env);
|
|
3618
|
+
return new _ShellCommand({ cmd, args, cwd, env });
|
|
3619
|
+
}
|
|
3620
|
+
async run() {
|
|
3621
|
+
try {
|
|
3622
|
+
const resultPromise = spawnAsync2(this.cmd, this.args, {
|
|
3623
|
+
cwd: this.cwd,
|
|
3624
|
+
env: this.env
|
|
3625
|
+
// shell: true
|
|
3626
|
+
});
|
|
3627
|
+
let { pid, stdout, stderr, status, signal } = await resultPromise;
|
|
3628
|
+
const signalObj = signal && signalsByName2[signal] || void 0;
|
|
3629
|
+
const commandResult = new CommandResult(stdout || "", stderr || "", status || 0, signalObj);
|
|
3630
|
+
this.result = commandResult;
|
|
3631
|
+
} catch (e) {
|
|
3632
|
+
const error = e;
|
|
3633
|
+
if (error.message) console.error(error.message);
|
|
3634
|
+
if (error.stack) console.error(error.stack);
|
|
3635
|
+
let { pid, stdout, stderr, status, signal } = error;
|
|
3636
|
+
const signalObj = signal && signalsByName2[signal] || void 0;
|
|
3637
|
+
const commandResult = new CommandResult(stdout || "", stderr || "", status || 1, signalObj);
|
|
3638
|
+
this.result = commandResult;
|
|
3639
|
+
}
|
|
3640
|
+
return this.result;
|
|
3641
|
+
}
|
|
3642
|
+
};
|
|
3643
|
+
|
|
3644
|
+
// src/unarchive.ts
|
|
3645
|
+
import decompress from "decompress";
|
|
3646
|
+
import decompressTarGzPlugin from "decompress-targz";
|
|
3647
|
+
import decompressZipPlugin from "decompress-unzip";
|
|
3648
|
+
import { match as match2, P as P2 } from "ts-pattern";
|
|
3649
|
+
import decompressTarPlugin from "decompress-tar";
|
|
3650
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
3651
|
+
import { getStreamAsBuffer } from "get-stream";
|
|
3652
|
+
import xzdecompress from "xz-decompress";
|
|
3653
|
+
async function unarchive(inputPath, outputPath) {
|
|
3654
|
+
const filename = File.basename(inputPath);
|
|
3655
|
+
return await match2(filename).with(P2.string.regex(/.tar.xz$/), () => decompressTarXz(inputPath, outputPath)).with(P2.string.regex(/.tar.gz$/), () => decompressTarGz(inputPath, outputPath)).with(P2.string.regex(/.zip$/), () => decompressZip(inputPath, outputPath)).otherwise(() => {
|
|
3656
|
+
throw new Error(`unable to decompress unknown file type: ${inputPath}`);
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
async function decompressTarGz(inputPath, outputPath, dropRootDir = 1) {
|
|
3660
|
+
await decompress(inputPath, outputPath, {
|
|
3661
|
+
plugins: [decompressTarGzPlugin()],
|
|
3662
|
+
strip: dropRootDir
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
async function decompressTarXzPlugin(input) {
|
|
3666
|
+
const type = await fileTypeFromBuffer(input);
|
|
3667
|
+
if (!type || type.ext !== "xz") {
|
|
3668
|
+
return [];
|
|
3669
|
+
}
|
|
3670
|
+
const blob = new Blob([input]);
|
|
3671
|
+
const webReadableStream = blob.stream();
|
|
3672
|
+
const xzStream = new xzdecompress.XzReadableStream(webReadableStream);
|
|
3673
|
+
const decompressedBuffer = await getStreamAsBuffer(xzStream);
|
|
3674
|
+
return decompressTarPlugin()(decompressedBuffer);
|
|
3675
|
+
}
|
|
3676
|
+
async function decompressTarXz(inputPath, outputPath, dropRootDir = 1) {
|
|
3677
|
+
await decompress(inputPath, outputPath, {
|
|
3678
|
+
plugins: [decompressTarXzPlugin],
|
|
3679
|
+
strip: dropRootDir
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
async function decompressZip(inputPath, outputPath, dropRootDir = 1) {
|
|
3683
|
+
await decompress(inputPath, outputPath, {
|
|
3684
|
+
plugins: [decompressZipPlugin()],
|
|
3685
|
+
strip: dropRootDir
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// src/node-runtime.ts
|
|
3690
|
+
var NodeRuntime = class _NodeRuntime {
|
|
3691
|
+
constructor(tmpDir) {
|
|
3692
|
+
this.tmpDir = tmpDir;
|
|
3693
|
+
this.client = axios.create({
|
|
3694
|
+
baseURL: "https://nodejs.org/"
|
|
3695
|
+
});
|
|
3696
|
+
this.alreadyInstalled = false;
|
|
3697
|
+
this.localNode = this.tmpDir.join(_NodeRuntime.Name, "bin", "node");
|
|
3698
|
+
this.localNpm = this.tmpDir.join(_NodeRuntime.Name, "bin", "npm");
|
|
3699
|
+
}
|
|
3700
|
+
static Name = "nodejs";
|
|
3701
|
+
client;
|
|
3702
|
+
alreadyInstalled;
|
|
3703
|
+
localNode;
|
|
3704
|
+
localNpm;
|
|
3705
|
+
async isNodeInstalledGlobally() {
|
|
3706
|
+
const result = await ShellCommand.fromString(`node --version`).run();
|
|
3707
|
+
return result.success;
|
|
3708
|
+
}
|
|
3709
|
+
async isNodeInstalledLocally() {
|
|
3710
|
+
const result = await ShellCommand.fromString(`${this.localNode} --version`).run();
|
|
3711
|
+
return result.success;
|
|
3712
|
+
}
|
|
3713
|
+
async isNpmInstalledGlobally() {
|
|
3714
|
+
const result = await ShellCommand.fromString(`npm --version`).run();
|
|
3715
|
+
return result.success;
|
|
3716
|
+
}
|
|
3717
|
+
async isNpmInstalledLocally() {
|
|
3718
|
+
const result = await ShellCommand.fromString(`${this.localNpm} --version`).run();
|
|
3719
|
+
return result.success;
|
|
3720
|
+
}
|
|
3721
|
+
async nodeCmd() {
|
|
3722
|
+
if (await this.isNodeInstalledGlobally()) return "node";
|
|
3723
|
+
if (await this.isNodeInstalledLocally()) return this.localNode.toString();
|
|
3724
|
+
throw new Error("node not installed");
|
|
3725
|
+
}
|
|
3726
|
+
async nodePath() {
|
|
3727
|
+
if (await this.isNodeInstalledGlobally()) {
|
|
3728
|
+
return await which("node", { nothrow: true });
|
|
3729
|
+
}
|
|
3730
|
+
if (await this.isNodeInstalledLocally()) return this.localNode.toString();
|
|
3731
|
+
return null;
|
|
3732
|
+
}
|
|
3733
|
+
async npmCmd() {
|
|
3734
|
+
if (await this.isNpmInstalledGlobally()) return "npm";
|
|
3735
|
+
if (await this.isNpmInstalledLocally()) return this.localNpm.toString();
|
|
3736
|
+
throw new Error("npm not installed");
|
|
3737
|
+
}
|
|
3738
|
+
async npmPath() {
|
|
3739
|
+
if (await this.isNpmInstalledGlobally()) {
|
|
3740
|
+
return await which("npm", { nothrow: true });
|
|
3741
|
+
}
|
|
3742
|
+
if (await this.isNpmInstalledLocally()) return this.localNpm.toString();
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
3745
|
+
async installIfNeeded() {
|
|
3746
|
+
if (this.alreadyInstalled || await this.isNodeInstalledGlobally() || await this.isNodeInstalledLocally()) {
|
|
3747
|
+
this.alreadyInstalled = true;
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
return this.install();
|
|
3751
|
+
}
|
|
3752
|
+
async install() {
|
|
3753
|
+
const platform2 = os2.platform();
|
|
3754
|
+
const arch = os2.arch();
|
|
3755
|
+
const packagePath = await this.downloadNodePackage(platform2, arch);
|
|
3756
|
+
const unzipDir = await this.unzipPackage(packagePath);
|
|
3757
|
+
this.alreadyInstalled = true;
|
|
3758
|
+
return unzipDir;
|
|
3759
|
+
}
|
|
3760
|
+
async listPackages() {
|
|
3761
|
+
const response = await this.client.get(`/dist/latest/`);
|
|
3762
|
+
const doc = await cheerio.load(response.data);
|
|
3763
|
+
const allFileLinks = doc("a").map((i, el) => doc(el).attr("href")).toArray();
|
|
3764
|
+
const archiveFiles = A2(allFileLinks).select((filename) => filename.match(/.*\.(gz|zip|xz)/));
|
|
3765
|
+
const urls = A2(archiveFiles).map((filename) => `https://nodejs.org/dist/latest/${filename}`);
|
|
3766
|
+
return urls;
|
|
3767
|
+
}
|
|
3768
|
+
// returns the path to the downloaded zip file
|
|
3769
|
+
async downloadNodePackage(platform2, arch) {
|
|
3770
|
+
const platformInFilename = match3(platform2).with("linux", () => "linux").with("win32", () => "win").with("darwin", () => "darwin").otherwise(() => "unknown-platform");
|
|
3771
|
+
const archInFilename = match3(arch).with("x64", () => "x64").with("x86", () => "x86").with("arm", () => "armv7l").with("arm64", () => "arm64").otherwise(() => "unknown-arch");
|
|
3772
|
+
const packages = await this.listPackages();
|
|
3773
|
+
const url = A2(packages).find((url2) => url2.match(`node-v.*-${platformInFilename}-${archInFilename}`));
|
|
3774
|
+
if (V.isAbsent(url)) {
|
|
3775
|
+
throw new Error(`Unable to download node for ${os2}/${arch} OS/architecture`);
|
|
3776
|
+
}
|
|
3777
|
+
const filename = File.basename(url);
|
|
3778
|
+
const path3 = this.tmpDir.join(filename);
|
|
3779
|
+
if (path3.exists()) return path3.toString();
|
|
3780
|
+
return await downloadFile(url, path3.toString());
|
|
3781
|
+
}
|
|
3782
|
+
// returns the path to the unzipped package directory
|
|
3783
|
+
async unzipPackage(packagePath) {
|
|
3784
|
+
const dir = this.tmpDir.join(_NodeRuntime.Name);
|
|
3785
|
+
if (dir.exists()) return dir.toString();
|
|
3786
|
+
await unarchive(packagePath, dir.toString());
|
|
3787
|
+
return dir.toString();
|
|
3788
|
+
}
|
|
3789
|
+
async npmInstall(omitDev = true, cwd) {
|
|
3790
|
+
if (omitDev) {
|
|
3791
|
+
return this.npm("install --omit=dev", cwd);
|
|
3792
|
+
} else {
|
|
3793
|
+
return this.npm("install", cwd);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
async npm(npmArgs, cwd) {
|
|
3797
|
+
const npmCmd = await this.npmCmd();
|
|
3798
|
+
const env = O(process.env).select(([_, v]) => !!v);
|
|
3799
|
+
return ShellCommand.fromString(`${npmCmd} ${npmArgs}`.trim(), env, cwd).run();
|
|
3800
|
+
}
|
|
3801
|
+
async node(nodeArgs, cwd) {
|
|
3802
|
+
const nodeCmd = await this.nodeCmd();
|
|
3803
|
+
const env = O(process.env).select(([_, v]) => !!v);
|
|
3804
|
+
return ShellCommand.fromString(`${nodeCmd} ${nodeArgs}`.trim(), env, cwd).run();
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
|
|
3808
|
+
// src/zip.ts
|
|
3809
|
+
import AdmZip from "adm-zip";
|
|
3810
|
+
async function zipDirectory(sourceDir, outputFilePath) {
|
|
3811
|
+
const zip2 = new AdmZip();
|
|
3812
|
+
zip2.addLocalFolder(sourceDir);
|
|
3813
|
+
await zip2.writeZipPromise(outputFilePath);
|
|
3814
|
+
return outputFilePath;
|
|
3815
|
+
}
|
|
3816
|
+
async function unzipDirectory(inputFilePath, outputDirectory) {
|
|
3817
|
+
const zip2 = new AdmZip(inputFilePath);
|
|
3818
|
+
return new Promise((resolve, reject) => {
|
|
3819
|
+
zip2.extractAllToAsync(outputDirectory, true, false, (error) => {
|
|
3820
|
+
if (error) {
|
|
3821
|
+
reject(error);
|
|
3822
|
+
} else {
|
|
3823
|
+
resolve(outputDirectory);
|
|
3824
|
+
}
|
|
3825
|
+
});
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// src/hash.ts
|
|
3830
|
+
import { createHash } from "crypto";
|
|
3831
|
+
function sha256(str) {
|
|
3832
|
+
return createHash("sha256").update(str).digest("hex");
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// src/param-map.ts
|
|
3836
|
+
import { match as match4 } from "ts-pattern";
|
|
3837
|
+
var ParamMap = class _ParamMap {
|
|
3838
|
+
static parse(cliArguments) {
|
|
3839
|
+
const root = new _ParamMap();
|
|
3840
|
+
A2(cliArguments).each((arg) => {
|
|
3841
|
+
const idx = arg.indexOf(":");
|
|
3842
|
+
if (idx === -1) {
|
|
3843
|
+
root.set(arg, "true");
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
const key = arg.slice(0, idx);
|
|
3847
|
+
const value = arg.slice(idx + 1);
|
|
3848
|
+
root.set(key, value);
|
|
3849
|
+
});
|
|
3850
|
+
return root;
|
|
3851
|
+
}
|
|
3852
|
+
map;
|
|
3853
|
+
constructor() {
|
|
3854
|
+
this.map = /* @__PURE__ */ new Map();
|
|
3855
|
+
}
|
|
3856
|
+
get(keyPath) {
|
|
3857
|
+
try {
|
|
3858
|
+
return A2(keyPath.split(":")).reduce(
|
|
3859
|
+
(node, key) => node.get(key),
|
|
3860
|
+
this.map
|
|
3861
|
+
);
|
|
3862
|
+
} catch (e) {
|
|
3863
|
+
return void 0;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
set(keyPath, value) {
|
|
3867
|
+
const keyParts = keyPath.split(":");
|
|
3868
|
+
const paramMap = A2(keyParts).skipLast(1).reduce((node, key2) => {
|
|
3869
|
+
let nextNode = node.get(key2);
|
|
3870
|
+
if (nextNode) return nextNode;
|
|
3871
|
+
const newParamMap = new _ParamMap();
|
|
3872
|
+
node.set(key2, newParamMap);
|
|
3873
|
+
return newParamMap;
|
|
3874
|
+
}, this.map);
|
|
3875
|
+
const key = A2(keyParts).last(1)[0];
|
|
3876
|
+
paramMap.set(key, value);
|
|
3877
|
+
}
|
|
3878
|
+
toObject() {
|
|
3879
|
+
let obj = {};
|
|
3880
|
+
M(this.map).each(([k, v]) => {
|
|
3881
|
+
obj[k] = match4(v).when(V.isA(String), (str) => this.stringToJsonObj(str)).otherwise(() => {
|
|
3882
|
+
return v.toObject();
|
|
3883
|
+
});
|
|
3884
|
+
});
|
|
3885
|
+
return obj;
|
|
3886
|
+
}
|
|
3887
|
+
stringToJsonObj(str) {
|
|
3888
|
+
return match4(str).when(matches(/(.*,)+/g), (str2) => {
|
|
3889
|
+
return VP(str2.split(",")).map((s) => s.trim()).compact([""]).map((s) => this.stringToJsonObj(s)).value;
|
|
3890
|
+
}).when(isNumeric, (str2) => parseFloat(str2)).otherwise(() => str);
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
|
|
3894
|
+
// src/version.ts
|
|
3895
|
+
var version = "0.1.32";
|
|
3896
|
+
|
|
3897
|
+
// src/app.ts
|
|
3898
|
+
import { retryUntilDefined } from "ts-retry";
|
|
3899
|
+
import { Mutex as Mutex3 } from "async-mutex";
|
|
3900
|
+
import { match as match5 } from "ts-pattern";
|
|
3901
|
+
|
|
3902
|
+
// src/core/remote/runAllRemote.ts
|
|
3903
|
+
async function run(context) {
|
|
3904
|
+
const { params, ssh } = context;
|
|
3905
|
+
const { taskFn, params: taskParams } = params;
|
|
3906
|
+
return await ssh([], async (remoteContext) => {
|
|
3907
|
+
const hostIdentifier = remoteContext.host?.alias || remoteContext.host?.shortName || "unknown_remote_host";
|
|
3908
|
+
remoteContext.debug(`[${hostIdentifier}] Connected via SSH.`);
|
|
3909
|
+
try {
|
|
3910
|
+
const result = await remoteContext.run(taskFn(taskParams));
|
|
3911
|
+
remoteContext.debug(`[${hostIdentifier}] Remote task result:`, JSON.stringify(result));
|
|
3912
|
+
return result;
|
|
3913
|
+
} catch (e) {
|
|
3914
|
+
remoteContext.debug(`[${hostIdentifier}] Failed to run remote task:`, e.message);
|
|
3915
|
+
return e;
|
|
3916
|
+
}
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
var runAllRemote_default = task(run, {
|
|
3920
|
+
description: "run a task on all selected hosts"
|
|
3921
|
+
});
|
|
3922
|
+
|
|
3923
|
+
// src/app.ts
|
|
3924
|
+
var TaskTree = class {
|
|
3925
|
+
// private taskEventBus: Emittery<{ newTask: NewTaskEvent; taskComplete: TaskCompleteEvent }>;
|
|
3926
|
+
listr;
|
|
3927
|
+
subLists;
|
|
3928
|
+
nodes;
|
|
3929
|
+
mutex;
|
|
3930
|
+
constructor() {
|
|
3931
|
+
this.mutex = new Mutex3();
|
|
3932
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
3933
|
+
this.subLists = /* @__PURE__ */ new Map();
|
|
3934
|
+
this.listr = new Listr([], {
|
|
3935
|
+
// exitOnError: false,
|
|
3936
|
+
concurrent: true,
|
|
3937
|
+
rendererOptions: { collapseSubtasks: false }
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
clear() {
|
|
3941
|
+
this.nodes.clear();
|
|
3942
|
+
this.subLists.clear();
|
|
3943
|
+
this.listr = new Listr([], {
|
|
3944
|
+
// exitOnError: false,
|
|
3945
|
+
concurrent: true,
|
|
3946
|
+
rendererOptions: { collapseSubtasks: false }
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
async add(id, parentId, name, result) {
|
|
3950
|
+
return await this.mutex.runExclusive(async () => {
|
|
3951
|
+
const parent = this.get(parentId);
|
|
3952
|
+
const newNode = { id, parentId, parent, children: /* @__PURE__ */ new Set(), name, result };
|
|
3953
|
+
this.nodes.set(id, newNode);
|
|
3954
|
+
this.listr.add({
|
|
3955
|
+
title: newNode.name,
|
|
3956
|
+
task: async (ctx, task3) => {
|
|
3957
|
+
await result;
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
return newNode;
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
get(id) {
|
|
3964
|
+
return this.nodes.get(id);
|
|
3965
|
+
}
|
|
3966
|
+
async getWithTimeout(id) {
|
|
3967
|
+
try {
|
|
3968
|
+
return await retryUntilDefined(
|
|
3969
|
+
() => {
|
|
3970
|
+
return this.get(id);
|
|
3971
|
+
},
|
|
3972
|
+
{ delay: 50, maxTry: 5 }
|
|
3973
|
+
);
|
|
3974
|
+
} catch (err) {
|
|
3975
|
+
return void 0;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
// setResult(id: string, result: Promise<string | Error | undefined>): TaskTreeNode | undefined {
|
|
3979
|
+
// const node = this.get(id);
|
|
3980
|
+
// if (node) {
|
|
3981
|
+
// node.result = result;
|
|
3982
|
+
// }
|
|
3983
|
+
// return node;
|
|
3984
|
+
// }
|
|
3985
|
+
run(ctx) {
|
|
3986
|
+
return this.listr.run(ctx);
|
|
3987
|
+
}
|
|
3988
|
+
// emit(name: any, event: any) {
|
|
3989
|
+
// this.taskEventBus.emit(name, event);
|
|
3990
|
+
// }
|
|
3991
|
+
};
|
|
3992
|
+
var Verbosity = {
|
|
3993
|
+
ERROR: 0,
|
|
3994
|
+
WARN: 1,
|
|
3995
|
+
INFO: 2,
|
|
3996
|
+
DEBUG: 3,
|
|
3997
|
+
TRACE: 4
|
|
3998
|
+
};
|
|
3999
|
+
var App = class {
|
|
4000
|
+
configRef;
|
|
4001
|
+
_config;
|
|
4002
|
+
selectedTags;
|
|
4003
|
+
outputStyle;
|
|
4004
|
+
_tmpDir;
|
|
4005
|
+
tmpFileRegistry;
|
|
4006
|
+
taskTree;
|
|
4007
|
+
verbosity = Verbosity.ERROR;
|
|
4008
|
+
constructor() {
|
|
4009
|
+
this.taskTree = new TaskTree();
|
|
4010
|
+
this.selectedTags = /* @__PURE__ */ new Set([]);
|
|
4011
|
+
this.outputStyle = "plain";
|
|
4012
|
+
this.tmpFileRegistry = new TmpFileRegistry(this.hostctlTmpDir());
|
|
4013
|
+
this.configRef = void 0;
|
|
4014
|
+
process3.on("exit", (code) => this.appExitCallback());
|
|
4015
|
+
}
|
|
4016
|
+
appExitCallback() {
|
|
4017
|
+
}
|
|
4018
|
+
get config() {
|
|
4019
|
+
if (!this._config) {
|
|
4020
|
+
throw new Error("Configuration has not been loaded. Call loadConfig() first.");
|
|
4021
|
+
}
|
|
4022
|
+
return this._config;
|
|
4023
|
+
}
|
|
4024
|
+
get tmpDir() {
|
|
4025
|
+
if (!this._tmpDir) {
|
|
4026
|
+
if (!fs6.existsSync(this.hostctlDir().toString())) {
|
|
4027
|
+
fs6.mkdirSync(this.hostctlDir().toString(), { recursive: true });
|
|
4028
|
+
}
|
|
4029
|
+
this._tmpDir = this.createNamedTmpDir(version);
|
|
4030
|
+
}
|
|
4031
|
+
return this._tmpDir;
|
|
4032
|
+
}
|
|
4033
|
+
async loadConfig(configString) {
|
|
4034
|
+
this.configRef = configString;
|
|
4035
|
+
this._config = await load(configString);
|
|
4036
|
+
}
|
|
4037
|
+
log(level, ...args) {
|
|
4038
|
+
if (this.outputStyle === "plain" && this.verbosity >= level) {
|
|
4039
|
+
console.log(...args);
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
debug(...args) {
|
|
4043
|
+
this.log(Verbosity.DEBUG, ...args);
|
|
4044
|
+
}
|
|
4045
|
+
info(...args) {
|
|
4046
|
+
this.log(Verbosity.INFO, ...args);
|
|
4047
|
+
}
|
|
4048
|
+
warn(...args) {
|
|
4049
|
+
this.log(Verbosity.WARN, ...args);
|
|
4050
|
+
}
|
|
4051
|
+
error(...args) {
|
|
4052
|
+
this.log(Verbosity.ERROR, ...args);
|
|
4053
|
+
}
|
|
4054
|
+
osHomeDir() {
|
|
4055
|
+
return Path.new(homedir3()).absolute();
|
|
4056
|
+
}
|
|
4057
|
+
hostctlDir() {
|
|
4058
|
+
return this.osHomeDir().join(".hostctl");
|
|
4059
|
+
}
|
|
4060
|
+
hostctlTmpDir() {
|
|
4061
|
+
return this.hostctlDir().join("tmp");
|
|
4062
|
+
}
|
|
4063
|
+
randName() {
|
|
4064
|
+
return Math.random().toString(36).slice(-5) + Math.random().toString(36).slice(-5);
|
|
4065
|
+
}
|
|
4066
|
+
hostctlTmpPath(tmpName) {
|
|
4067
|
+
tmpName ??= this.randName();
|
|
4068
|
+
return this.hostctlTmpDir().join(tmpName);
|
|
4069
|
+
}
|
|
4070
|
+
// this directory will be automatically cleaned up at program exit
|
|
4071
|
+
createNamedTmpDir(subDirName) {
|
|
4072
|
+
return this.tmpFileRegistry.createNamedTmpDir(subDirName);
|
|
4073
|
+
}
|
|
4074
|
+
// this file will be automatically cleaned up at program exit
|
|
4075
|
+
writeTmpFile(fileContent) {
|
|
4076
|
+
return this.tmpFileRegistry.writeTmpFile(fileContent);
|
|
4077
|
+
}
|
|
4078
|
+
shouldRunRemote() {
|
|
4079
|
+
const selectedHosts = this.selectedInventory();
|
|
4080
|
+
if (selectedHosts.length === 0 && this.selectedTags.size === 0) return false;
|
|
4081
|
+
return selectedHosts.some((h) => !h.isLocal());
|
|
4082
|
+
}
|
|
4083
|
+
logHostCommandResult(host, command, cmdOrErr, isErrorResult = false) {
|
|
4084
|
+
const hostName = host.uri || (host.isLocal() ? "localhost" : "unknown-host");
|
|
4085
|
+
if (cmdOrErr instanceof Error || isErrorResult) {
|
|
4086
|
+
const error = cmdOrErr;
|
|
4087
|
+
this.debug(chalk4.red(`[${hostName}] Error executing command "${command}": ${error.message}`));
|
|
4088
|
+
if (error.stack && this.verbosity >= Verbosity.DEBUG) {
|
|
4089
|
+
this.debug(chalk4.red(error.stack));
|
|
4090
|
+
}
|
|
4091
|
+
} else {
|
|
4092
|
+
const cmdRes = cmdOrErr.result;
|
|
4093
|
+
this.debug(chalk4.green(`[${hostName}] Success executing "${command}" (exit code: ${cmdRes.exitCode})`));
|
|
4094
|
+
if (cmdRes.stdout) {
|
|
4095
|
+
this.debug(chalk4.cyan(`[${hostName}] STDOUT:
|
|
4096
|
+
${cmdRes.stdout.trim()}`));
|
|
4097
|
+
}
|
|
4098
|
+
if (cmdRes.stderr) {
|
|
4099
|
+
this.debug(chalk4.yellow(`[${hostName}] STDERR:
|
|
4100
|
+
${cmdRes.stderr.trim()}`));
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
defaultSshUser() {
|
|
4105
|
+
return void 0;
|
|
4106
|
+
}
|
|
4107
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/test-jill.priv tsx bin/ops.ts e whoami -t ubuntu
|
|
4108
|
+
// e.g. AGE_IDS=~/.secrets/age/david.priv tsx bin/ops.ts -c david.yaml exec "whoami" -t synology blog 4gb web1
|
|
4109
|
+
async execute(cmd) {
|
|
4110
|
+
const selectedHosts = this.selectedInventory();
|
|
4111
|
+
const cmdResults = A2(selectedHosts).map((host) => this.runRemote(host, cmd));
|
|
4112
|
+
const hostResultQuads = await VP(selectedHosts).a.zip(cmdResults).a.map(([host, cmdPromise]) => {
|
|
4113
|
+
return cmdPromise.then(
|
|
4114
|
+
(cmd2) => match5(cmd2).when(isError2, (err) => [host, false, "", chalk4.red(err.message)]).otherwise((cmd3) => [host, cmd3.result?.success ?? false, cmd3.result?.stdout, cmd3.result?.stderr])
|
|
4115
|
+
);
|
|
4116
|
+
}).waitAll().value;
|
|
4117
|
+
if (this.outputStyle === "plain") {
|
|
4118
|
+
this.warn(chalk4.yellow("=== Result ==="));
|
|
4119
|
+
A2(hostResultQuads).each(([host, success, stdout, stderr]) => {
|
|
4120
|
+
this.warn(
|
|
4121
|
+
`${success ? chalk4.green(CHECKMARK) : chalk4.red(XMARK)} ${chalk4.cyan(host.hostname)}${host.alias ? ` (${host.alias})` : ""}`
|
|
4122
|
+
);
|
|
4123
|
+
if (stdout) {
|
|
4124
|
+
this.warn(stdout);
|
|
4125
|
+
}
|
|
4126
|
+
if (stderr) {
|
|
4127
|
+
this.warn(chalk4.red(stderr));
|
|
4128
|
+
}
|
|
4129
|
+
this.warn(chalk4.yellow("--------------"));
|
|
4130
|
+
});
|
|
4131
|
+
} else if (this.outputStyle === "json") {
|
|
4132
|
+
const result = A2(hostResultQuads).reduce(
|
|
4133
|
+
(acc, [host, success, stdout, stderr]) => {
|
|
4134
|
+
acc[host.hostname] = {
|
|
4135
|
+
alias: host.alias || "",
|
|
4136
|
+
success,
|
|
4137
|
+
stdout,
|
|
4138
|
+
stderr
|
|
4139
|
+
};
|
|
4140
|
+
return acc;
|
|
4141
|
+
},
|
|
4142
|
+
{}
|
|
4143
|
+
);
|
|
4144
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4145
|
+
}
|
|
4146
|
+
return hostResultQuads.every(([_, success]) => success);
|
|
4147
|
+
}
|
|
4148
|
+
// used by execute to run a command on the remote host
|
|
4149
|
+
async runRemote(host, cmd) {
|
|
4150
|
+
try {
|
|
4151
|
+
const hostPassword = await host.decryptedPassword();
|
|
4152
|
+
const sshConnection = {
|
|
4153
|
+
host: host.uri,
|
|
4154
|
+
username: host.user,
|
|
4155
|
+
password: hostPassword,
|
|
4156
|
+
privateKeyPath: await host.plaintextSshKeyPath()
|
|
4157
|
+
};
|
|
4158
|
+
const interactionHandler = InteractionHandler.with(withSudo(hostPassword));
|
|
4159
|
+
const session = new SSHSession();
|
|
4160
|
+
await session.connect(sshConnection);
|
|
4161
|
+
const commandObj = await session.run(cmd, interactionHandler);
|
|
4162
|
+
session.disconnect();
|
|
4163
|
+
return commandObj;
|
|
4164
|
+
} catch (e) {
|
|
4165
|
+
return e;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
getSecretsForHost(hostId) {
|
|
4169
|
+
const secretsMap = /* @__PURE__ */ new Map();
|
|
4170
|
+
if (!this._config) {
|
|
4171
|
+
return secretsMap;
|
|
4172
|
+
}
|
|
4173
|
+
return secretsMap;
|
|
4174
|
+
}
|
|
4175
|
+
taskContextForRunFn(invocation, params, hostForContext) {
|
|
4176
|
+
const effectiveHost = hostForContext || invocation.host;
|
|
4177
|
+
return {
|
|
4178
|
+
// Properties from TaskContext
|
|
4179
|
+
params,
|
|
4180
|
+
id: invocation.id,
|
|
4181
|
+
host: effectiveHost,
|
|
4182
|
+
config: invocation.config,
|
|
4183
|
+
// Delegated from invocation
|
|
4184
|
+
// Methods from TaskContext, delegating to invocation
|
|
4185
|
+
log: (level, ...message) => {
|
|
4186
|
+
invocation.log(level, ...message);
|
|
4187
|
+
},
|
|
4188
|
+
info: (...message) => {
|
|
4189
|
+
invocation.info(...message);
|
|
4190
|
+
},
|
|
4191
|
+
debug: (...message) => {
|
|
4192
|
+
invocation.debug(...message);
|
|
4193
|
+
},
|
|
4194
|
+
warn: (...message) => {
|
|
4195
|
+
invocation.warn(...message);
|
|
4196
|
+
},
|
|
4197
|
+
error: (...message) => {
|
|
4198
|
+
invocation.error(...message);
|
|
4199
|
+
},
|
|
4200
|
+
exec: async (command, options) => {
|
|
4201
|
+
return await invocation.exec(command, options);
|
|
4202
|
+
},
|
|
4203
|
+
ssh: async (tags, remoteTaskFn) => {
|
|
4204
|
+
return await invocation.ssh(tags, remoteTaskFn);
|
|
4205
|
+
},
|
|
4206
|
+
run: async (taskPartialFn) => {
|
|
4207
|
+
return await invocation.run(taskPartialFn);
|
|
4208
|
+
},
|
|
4209
|
+
getPassword: async () => {
|
|
4210
|
+
return await invocation.getPassword();
|
|
4211
|
+
},
|
|
4212
|
+
getSecret: async (name) => {
|
|
4213
|
+
return await invocation.getSecret(name);
|
|
4214
|
+
},
|
|
4215
|
+
exit: (exitCode, message) => {
|
|
4216
|
+
invocation.exit(exitCode, message);
|
|
4217
|
+
},
|
|
4218
|
+
inventory: (tags) => {
|
|
4219
|
+
return invocation.inventory(tags);
|
|
4220
|
+
},
|
|
4221
|
+
file: invocation.file
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
setSelectedTags(selectedTags) {
|
|
4225
|
+
this.selectedTags = new Set(selectedTags);
|
|
4226
|
+
}
|
|
4227
|
+
setOutputStyle(outputStyle) {
|
|
4228
|
+
this.outputStyle = outputStyle;
|
|
4229
|
+
}
|
|
4230
|
+
setVerbosity(level) {
|
|
4231
|
+
this.verbosity = level;
|
|
4232
|
+
}
|
|
4233
|
+
outputJson() {
|
|
4234
|
+
return this.outputStyle == "json";
|
|
4235
|
+
}
|
|
4236
|
+
outputPlain() {
|
|
4237
|
+
return this.outputStyle == "plain";
|
|
4238
|
+
}
|
|
4239
|
+
querySelectedInventory(tags = []) {
|
|
4240
|
+
return this.selectInventory(this.selectedInventory(), new Set(tags));
|
|
4241
|
+
}
|
|
4242
|
+
selectedInventory() {
|
|
4243
|
+
return this.queryInventory(this.selectedTags);
|
|
4244
|
+
}
|
|
4245
|
+
// returns hosts that have *all* of the given tags
|
|
4246
|
+
// each tag is a string of the form:
|
|
4247
|
+
// - foo
|
|
4248
|
+
// - bar+baz
|
|
4249
|
+
// the tags are interpreted to be in disjunctive-normal-form: a disjunction of conjunctions
|
|
4250
|
+
queryInventory(tags = /* @__PURE__ */ new Set()) {
|
|
4251
|
+
if (!this._config) {
|
|
4252
|
+
this.warn("Warning: Configuration not loaded, cannot query inventory.");
|
|
4253
|
+
return [];
|
|
4254
|
+
}
|
|
4255
|
+
const allHosts = this._config.hosts();
|
|
4256
|
+
if (tags.size === 0) {
|
|
4257
|
+
return allHosts;
|
|
4258
|
+
}
|
|
4259
|
+
return this.selectInventory(allHosts, new Set(tags));
|
|
4260
|
+
}
|
|
4261
|
+
selectInventory(hosts, tags = /* @__PURE__ */ new Set()) {
|
|
4262
|
+
if (S(tags).isEmpty()) {
|
|
4263
|
+
return hosts;
|
|
4264
|
+
}
|
|
4265
|
+
return A2(hosts).select((host) => host.hasAnyTag(tags));
|
|
4266
|
+
}
|
|
4267
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/test-jill.priv tsx bin/hostctl.ts inventory -t ubuntu
|
|
4268
|
+
printInventoryReport() {
|
|
4269
|
+
const configFromGetter = this.config;
|
|
4270
|
+
const config = configFromGetter;
|
|
4271
|
+
const allHosts = config.hosts();
|
|
4272
|
+
const allSecrets = config.secrets();
|
|
4273
|
+
const allIds = config.ids();
|
|
4274
|
+
if (this.outputPlain()) {
|
|
4275
|
+
let output = "Config file: " + this.configRef + "\n\nHosts:\n";
|
|
4276
|
+
if (allHosts.length > 0) {
|
|
4277
|
+
allHosts.forEach((host) => {
|
|
4278
|
+
output += ` - ${host.uri}:
|
|
4279
|
+
`;
|
|
4280
|
+
output += ` alias: ${host.alias || ""}
|
|
4281
|
+
`;
|
|
4282
|
+
output += ` user: ${host.user || ""}
|
|
4283
|
+
`;
|
|
4284
|
+
output += ` port: ${host.port || ""}
|
|
4285
|
+
`;
|
|
4286
|
+
output += ` tags: ${host.tags.join(", ")}
|
|
4287
|
+
`;
|
|
4288
|
+
});
|
|
4289
|
+
} else {
|
|
4290
|
+
output += " {}\n";
|
|
4291
|
+
}
|
|
4292
|
+
output += "\nSecrets:\n";
|
|
4293
|
+
if (allSecrets.size > 0) {
|
|
4294
|
+
allSecrets.forEach((secret, name) => {
|
|
4295
|
+
output += ` - ${name}:
|
|
4296
|
+
`;
|
|
4297
|
+
output += ` encrypted: ${secret.value.isEncrypted()}
|
|
4298
|
+
`;
|
|
4299
|
+
});
|
|
4300
|
+
} else {
|
|
4301
|
+
output += " {}\n";
|
|
4302
|
+
}
|
|
4303
|
+
output += "\nIdentities (Public Keys):\n";
|
|
4304
|
+
if (allIds.size > 0) {
|
|
4305
|
+
allIds.forEach((idGroup, name) => {
|
|
4306
|
+
output += ` - ${name}:
|
|
4307
|
+
`;
|
|
4308
|
+
output += ` publicKeys: ${idGroup.publicKeys.join(", ")}
|
|
4309
|
+
`;
|
|
4310
|
+
});
|
|
4311
|
+
} else {
|
|
4312
|
+
output += " {}\n";
|
|
4313
|
+
}
|
|
4314
|
+
console.log(output.trim());
|
|
4315
|
+
} else if (this.outputJson()) {
|
|
4316
|
+
const jsonData = {
|
|
4317
|
+
hosts: allHosts.map((h) => h.toObject()),
|
|
4318
|
+
secrets: {},
|
|
4319
|
+
identities: {}
|
|
4320
|
+
};
|
|
4321
|
+
allSecrets.forEach((secret, name) => {
|
|
4322
|
+
jsonData.secrets[name] = {
|
|
4323
|
+
name: secret.name,
|
|
4324
|
+
encrypted: secret.value.isEncrypted()
|
|
4325
|
+
// Consider if ids/recipients should be exposed here
|
|
4326
|
+
};
|
|
4327
|
+
});
|
|
4328
|
+
allIds.forEach((idGroup, name) => {
|
|
4329
|
+
jsonData.identities[name] = {
|
|
4330
|
+
name: idGroup.name || name,
|
|
4331
|
+
// Assuming NamedRecipientGroup has a name property
|
|
4332
|
+
publicKeys: idGroup.publicKeys,
|
|
4333
|
+
idRefs: idGroup.idRefs
|
|
4334
|
+
};
|
|
4335
|
+
});
|
|
4336
|
+
console.log(JSON.stringify(jsonData, null, 2));
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/test-david.priv tsx bin/hostctl.ts inventory encrypt
|
|
4340
|
+
async encryptInventoryFile() {
|
|
4341
|
+
if (!this.configRef) {
|
|
4342
|
+
throw new Error("Cannot encrypt inventory: Configuration file reference is not set.");
|
|
4343
|
+
}
|
|
4344
|
+
const currentConfigRef = this.configRef;
|
|
4345
|
+
this.warn(`Encrypting inventory file: ${currentConfigRef}`);
|
|
4346
|
+
const configFile = new ConfigFile2(currentConfigRef);
|
|
4347
|
+
await configFile.load();
|
|
4348
|
+
await configFile.encryptAll();
|
|
4349
|
+
await configFile.save(currentConfigRef);
|
|
4350
|
+
}
|
|
4351
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts inventory decrypt
|
|
4352
|
+
async decryptInventoryFile() {
|
|
4353
|
+
if (!this.configRef) {
|
|
4354
|
+
throw new Error("Cannot decrypt inventory: Configuration file reference is not set.");
|
|
4355
|
+
}
|
|
4356
|
+
const currentConfigRef = this.configRef;
|
|
4357
|
+
const configFile = new ConfigFile2(currentConfigRef);
|
|
4358
|
+
await configFile.load();
|
|
4359
|
+
let hasEncryptedSecrets = false;
|
|
4360
|
+
for (const secret of configFile._secrets.values()) {
|
|
4361
|
+
if (secret.value.isEncrypted()) {
|
|
4362
|
+
hasEncryptedSecrets = true;
|
|
4363
|
+
break;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
if (!hasEncryptedSecrets) {
|
|
4367
|
+
this.info("No encrypted secrets found to decrypt. Inventory is already decrypted.");
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
this.warn(`Decrypting inventory file: ${currentConfigRef}`);
|
|
4371
|
+
try {
|
|
4372
|
+
await configFile.decryptAllIfPossible();
|
|
4373
|
+
const successfullyUsedIdentityPaths = configFile.loadPrivateKeys().filter((identity2) => {
|
|
4374
|
+
try {
|
|
4375
|
+
return fs6.existsSync(identity2.identityFilePath);
|
|
4376
|
+
} catch (e) {
|
|
4377
|
+
return false;
|
|
4378
|
+
}
|
|
4379
|
+
}).map((i) => i.identityFilePath).join("\n");
|
|
4380
|
+
if (this.verbosity >= Verbosity.INFO && successfullyUsedIdentityPaths.length > 0) {
|
|
4381
|
+
this.info(`Decrypted with one or more of the following private keys:
|
|
4382
|
+
${successfullyUsedIdentityPaths}`);
|
|
4383
|
+
} else if (this.verbosity >= Verbosity.INFO && hasEncryptedSecrets && successfullyUsedIdentityPaths.length === 0) {
|
|
4384
|
+
this.warn("Encrypted secrets found, but no specified AGE identities were successfully used or found.");
|
|
4385
|
+
}
|
|
4386
|
+
await configFile.save(currentConfigRef);
|
|
4387
|
+
} catch (error) {
|
|
4388
|
+
if (error.message?.includes("Unable to read identity file")) {
|
|
4389
|
+
throw new Error("Decryption failed: no identity matched or failed to decrypt due to key issue.");
|
|
4390
|
+
}
|
|
4391
|
+
if (error.message?.includes("No identity matched secret")) {
|
|
4392
|
+
throw new Error("Decryption failed: no identity matched the encrypted secrets.");
|
|
4393
|
+
}
|
|
4394
|
+
throw error;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts -t ubuntu -- script.ts
|
|
4398
|
+
// e.g. AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts -c david.yaml -t synology blog 4gb web1 -- script.ts
|
|
4399
|
+
async runScript(scriptRef, params) {
|
|
4400
|
+
const selectedHosts = this.selectedInventory();
|
|
4401
|
+
this.info(`Selected hosts: ${selectedHosts.length}`);
|
|
4402
|
+
const absoluteScriptRef = Path.new(scriptRef).absolute();
|
|
4403
|
+
this.debug(`absoluteScriptRef=${absoluteScriptRef}`);
|
|
4404
|
+
this.debug(`params=${util2.inspect(params)}`);
|
|
4405
|
+
const mod = await this.loadScriptAsModule(absoluteScriptRef.toString());
|
|
4406
|
+
this.debug(`mod=${util2.inspect(mod)}`);
|
|
4407
|
+
const packageFileDir = this.pathOfPackageJsonFile(absoluteScriptRef.toString());
|
|
4408
|
+
if (!packageFileDir) {
|
|
4409
|
+
console.error(
|
|
4410
|
+
chalk4.red(`Bundle failure. "${absoluteScriptRef}" nor any ancestor directory contains a package.json file.`)
|
|
4411
|
+
);
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const taskFn = mod.default;
|
|
4415
|
+
const interactionHandler = new InteractionHandler();
|
|
4416
|
+
const localRuntime = new LocalRuntime(this, absoluteScriptRef, interactionHandler);
|
|
4417
|
+
if (this.outputPlain()) {
|
|
4418
|
+
this.info(`run: ${chalk4.yellow(absoluteScriptRef.toString())} ${chalk4.cyan(util2.inspect(params))}`);
|
|
4419
|
+
}
|
|
4420
|
+
let invocation;
|
|
4421
|
+
let scriptResult;
|
|
4422
|
+
try {
|
|
4423
|
+
invocation = await localRuntime.invokeRootTask(taskFn, params, `Running ${scriptRef}`);
|
|
4424
|
+
if (!invocation) {
|
|
4425
|
+
this.error(`Failed to invoke task for script: ${scriptRef}`);
|
|
4426
|
+
scriptResult = new Error(`Failed to invoke task for script: ${scriptRef}`);
|
|
4427
|
+
} else {
|
|
4428
|
+
scriptResult = await invocation.result;
|
|
4429
|
+
this.reportScriptResult(invocation, scriptResult);
|
|
4430
|
+
}
|
|
4431
|
+
} catch (e) {
|
|
4432
|
+
scriptResult = e;
|
|
4433
|
+
if (this.outputPlain()) {
|
|
4434
|
+
this.error(`Error running script ${scriptRef}: ${scriptResult.message}`);
|
|
4435
|
+
if (this.verbosity >= Verbosity.DEBUG && scriptResult.stack) {
|
|
4436
|
+
this.error(scriptResult.stack);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
if (invocation) {
|
|
4440
|
+
this.reportScriptResult(invocation, scriptResult);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
if (scriptResult instanceof Error) {
|
|
4444
|
+
process3.exit(1);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
// entrypoint for the cli: AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts -t ubuntu -- script.ts
|
|
4448
|
+
// e.g. AGE_IDS=~/.secrets/age/david.priv tsx bin/hostctl.ts -c david.yaml -t synology blog 4gb web1 -- script.ts
|
|
4449
|
+
async runScriptRemote(scriptRef, params) {
|
|
4450
|
+
const selectedHosts = this.selectedInventory();
|
|
4451
|
+
this.info(`Selected hosts: ${selectedHosts.length}`);
|
|
4452
|
+
const absoluteScriptRef = Path.new(scriptRef).absolute();
|
|
4453
|
+
this.debug(`absoluteScriptRef=${absoluteScriptRef}`);
|
|
4454
|
+
this.debug(`params=${util2.inspect(params)}`);
|
|
4455
|
+
const mod = await this.loadScriptAsModule(absoluteScriptRef.toString());
|
|
4456
|
+
this.debug(`mod=${util2.inspect(mod)}`);
|
|
4457
|
+
const packageFileDir = this.pathOfPackageJsonFile(absoluteScriptRef.toString());
|
|
4458
|
+
if (!packageFileDir) {
|
|
4459
|
+
console.error(
|
|
4460
|
+
chalk4.red(`Bundle failure. "${absoluteScriptRef}" nor any ancestor directory contains a package.json file.`)
|
|
4461
|
+
);
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
const taskFn = mod.default;
|
|
4465
|
+
const interactionHandler = new InteractionHandler();
|
|
4466
|
+
const localRuntime = new LocalRuntime(this, absoluteScriptRef, interactionHandler);
|
|
4467
|
+
this.info(`run: ${chalk4.yellow(absoluteScriptRef.toString())} ${chalk4.cyan(util2.inspect(params))}`);
|
|
4468
|
+
let invocation;
|
|
4469
|
+
let scriptResult;
|
|
4470
|
+
try {
|
|
4471
|
+
const runAllRemoteParams = { taskFn, params };
|
|
4472
|
+
invocation = await localRuntime.invokeRootTask(
|
|
4473
|
+
runAllRemote_default,
|
|
4474
|
+
runAllRemoteParams,
|
|
4475
|
+
`Running ${scriptRef} on selected hosts`
|
|
4476
|
+
);
|
|
4477
|
+
if (!invocation) {
|
|
4478
|
+
this.error(`Failed to invoke task for script: ${scriptRef}`);
|
|
4479
|
+
scriptResult = new Error(`Failed to invoke task for script: ${scriptRef}`);
|
|
4480
|
+
} else {
|
|
4481
|
+
scriptResult = await invocation.result;
|
|
4482
|
+
this.reportScriptResult(invocation, scriptResult);
|
|
4483
|
+
}
|
|
4484
|
+
} catch (e) {
|
|
4485
|
+
scriptResult = e;
|
|
4486
|
+
if (this.outputPlain()) {
|
|
4487
|
+
this.error(`Error running script ${scriptRef}: ${scriptResult.message}`);
|
|
4488
|
+
if (this.verbosity >= Verbosity.DEBUG && scriptResult.stack) {
|
|
4489
|
+
this.error(scriptResult.stack);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
if (invocation) {
|
|
4493
|
+
this.reportScriptResult(invocation, scriptResult);
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
if (scriptResult instanceof Error) {
|
|
4497
|
+
process3.exit(1);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
async walkInvocationTreePreorder(invocation, visitFn, visited = /* @__PURE__ */ new Set(), depth = 0) {
|
|
4501
|
+
if (visited.has(invocation)) return;
|
|
4502
|
+
visited.add(invocation);
|
|
4503
|
+
await visitFn(invocation, depth);
|
|
4504
|
+
for (const childInvocation of invocation.getChildren()) {
|
|
4505
|
+
await this.walkInvocationTreePreorder(childInvocation, visitFn, visited, depth + 1);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
async reportScriptResult(invocation, scriptResult) {
|
|
4509
|
+
if (this.outputPlain()) {
|
|
4510
|
+
this.warn("=== Evaluation ===");
|
|
4511
|
+
await this.walkInvocationTreePreorder(invocation, async (invocation2, depth) => {
|
|
4512
|
+
const indent = " ".repeat(depth);
|
|
4513
|
+
if (invocation2.getDescription() || invocation2.id) {
|
|
4514
|
+
const err = await invocation2.resultError();
|
|
4515
|
+
if (err) {
|
|
4516
|
+
if (this.verbosity >= Verbosity.WARN) {
|
|
4517
|
+
this.warn(
|
|
4518
|
+
`${indent}${chalk4.red(XMARK)} ${invocation2.getDescription() || invocation2.id} ${chalk4.red(util2.inspect(err))}`
|
|
4519
|
+
);
|
|
4520
|
+
if (this.verbosity >= Verbosity.INFO) {
|
|
4521
|
+
this.info(`${indent} ${chalk4.red(err.message)}`);
|
|
4522
|
+
if (this.verbosity >= Verbosity.DEBUG) {
|
|
4523
|
+
invocation2.stdout && this.debug(`${indent} ${chalk4.bgGreen(invocation2.stdout.trimEnd())}`);
|
|
4524
|
+
invocation2.stderr && this.debug(`${indent} ${chalk4.bgRed(invocation2.stderr.trimEnd())}`);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
} else {
|
|
4529
|
+
if (this.verbosity >= Verbosity.WARN) {
|
|
4530
|
+
this.warn(`${indent}${chalk4.green(CHECKMARK)} ${invocation2.getDescription() || invocation2.id}`);
|
|
4531
|
+
if (this.verbosity >= Verbosity.INFO) {
|
|
4532
|
+
this.info(`${indent} ${chalk4.green(util2.inspect(await invocation2.getResultPromise()))}`);
|
|
4533
|
+
if (this.verbosity >= Verbosity.DEBUG) {
|
|
4534
|
+
invocation2.stdout && this.debug(`${indent} ${chalk4.bgGreen(invocation2.stdout.trimEnd())}`);
|
|
4535
|
+
invocation2.stderr && this.debug(`${indent} ${chalk4.bgRed(invocation2.stderr.trimEnd())}`);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4542
|
+
this.warn("=== Result ===");
|
|
4543
|
+
if (isError2(scriptResult)) {
|
|
4544
|
+
this.warn(chalk4.red(`${invocation.getDescription() || invocation.id} failed:`));
|
|
4545
|
+
this.warn(chalk4.red(scriptResult.message));
|
|
4546
|
+
if (scriptResult.stack) {
|
|
4547
|
+
this.warn(chalk4.red(scriptResult.stack));
|
|
4548
|
+
}
|
|
4549
|
+
} else if (scriptResult && typeof scriptResult === "object" && "exitCode" in scriptResult && typeof scriptResult.exitCode === "number") {
|
|
4550
|
+
const exitCode = scriptResult.exitCode;
|
|
4551
|
+
const message = scriptResult.message || "";
|
|
4552
|
+
this.warn(
|
|
4553
|
+
chalk4.yellow(
|
|
4554
|
+
`${invocation.getDescription() || invocation.id} requested exit with code ${exitCode}: ${message}`
|
|
4555
|
+
)
|
|
4556
|
+
);
|
|
4557
|
+
} else if (scriptResult === void 0) {
|
|
4558
|
+
this.warn(chalk4.green(`${invocation.getDescription() || invocation.id} completed successfully.`));
|
|
4559
|
+
} else {
|
|
4560
|
+
this.warn(
|
|
4561
|
+
chalk4.green(
|
|
4562
|
+
`${invocation.getDescription() || invocation.id} completed with result: ${util2.inspect(scriptResult, { depth: 3 })}`
|
|
4563
|
+
)
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4566
|
+
} else if (this.outputJson()) {
|
|
4567
|
+
if (!(scriptResult instanceof Error)) {
|
|
4568
|
+
console.log(JSON.stringify(scriptResult, null, 2));
|
|
4569
|
+
} else {
|
|
4570
|
+
const errorJson = {
|
|
4571
|
+
error: scriptResult.message,
|
|
4572
|
+
stack: scriptResult.stack
|
|
4573
|
+
};
|
|
4574
|
+
console.log(JSON.stringify(errorJson, null, 2));
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
async loadScriptAsModule(scriptRef) {
|
|
4579
|
+
const mod = await import(scriptRef);
|
|
4580
|
+
return mod;
|
|
4581
|
+
}
|
|
4582
|
+
parseParams(scriptArgs) {
|
|
4583
|
+
return ParamMap.parse(scriptArgs).toObject();
|
|
4584
|
+
}
|
|
4585
|
+
// this function creates a bundle of a given directory
|
|
4586
|
+
async bundleProject(entrypointPath) {
|
|
4587
|
+
const nodeRuntime = new NodeRuntime(this.tmpDir);
|
|
4588
|
+
const entrypointDir = this.pathOfPackageJsonFile(entrypointPath);
|
|
4589
|
+
if (!entrypointDir) {
|
|
4590
|
+
console.error(
|
|
4591
|
+
chalk4.red(`Bundle failure. "${entrypointPath}" nor any ancestor directory contains a package.json file.`)
|
|
4592
|
+
);
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
const tasks = new Listr([], {
|
|
4596
|
+
exitOnError: false,
|
|
4597
|
+
concurrent: true
|
|
4598
|
+
});
|
|
4599
|
+
tasks.add([
|
|
4600
|
+
{
|
|
4601
|
+
title: `Bundling ${entrypointDir.toString()}`,
|
|
4602
|
+
task: async (ctx) => {
|
|
4603
|
+
await this.generateBundle(nodeRuntime, entrypointDir);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
]);
|
|
4607
|
+
try {
|
|
4608
|
+
await tasks.run();
|
|
4609
|
+
} catch (e) {
|
|
4610
|
+
console.error(e);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
async generateBundle(nodeRuntime, packageFileDir) {
|
|
4614
|
+
await nodeRuntime.installIfNeeded();
|
|
4615
|
+
const result = await nodeRuntime.npmInstall(true, packageFileDir.toString());
|
|
4616
|
+
if (result.failure) throw new Error(result.err);
|
|
4617
|
+
const absoluteDirPath = packageFileDir.toString();
|
|
4618
|
+
const dirNameHash = sha256(absoluteDirPath).slice(0, 10);
|
|
4619
|
+
const bundleZipFile = this.tmpDir.join(`bundle${dirNameHash}.zip`);
|
|
4620
|
+
const zipPath = await zipDirectory(packageFileDir.toString(), bundleZipFile.toString());
|
|
4621
|
+
return Path.new(zipPath);
|
|
4622
|
+
}
|
|
4623
|
+
// walks the directory tree that contains the given path from leaf to root searching for the deepest directory
|
|
4624
|
+
// containing a package.json file and returns the absolute path of that directory
|
|
4625
|
+
pathOfPackageJsonFile(path3) {
|
|
4626
|
+
let p = Path.new(path3);
|
|
4627
|
+
while (true) {
|
|
4628
|
+
if (p.dirContains("package.json")) {
|
|
4629
|
+
return p.absolute();
|
|
4630
|
+
}
|
|
4631
|
+
if (p.isRoot()) {
|
|
4632
|
+
return null;
|
|
4633
|
+
}
|
|
4634
|
+
p = p.parent();
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
async printRuntimeReport() {
|
|
4638
|
+
const nodeRuntime = new NodeRuntime(this.tmpDir);
|
|
4639
|
+
const reportObj = {
|
|
4640
|
+
nodePath: await nodeRuntime.nodePath(),
|
|
4641
|
+
npmPath: await nodeRuntime.npmPath()
|
|
4642
|
+
// summary: report?.getReport(),
|
|
4643
|
+
};
|
|
4644
|
+
this.info(reportObj);
|
|
4645
|
+
}
|
|
4646
|
+
async promptPassword(message = "Enter your password") {
|
|
4647
|
+
return await promptPassword({ message });
|
|
4648
|
+
}
|
|
4649
|
+
async installRuntime() {
|
|
4650
|
+
this.info(`creating node runtime with ${this.tmpDir.toString()}`);
|
|
4651
|
+
const nodeRuntime = new NodeRuntime(this.tmpDir);
|
|
4652
|
+
await nodeRuntime.installIfNeeded();
|
|
4653
|
+
}
|
|
4654
|
+
async bootstrap() {
|
|
4655
|
+
}
|
|
4656
|
+
};
|
|
4657
|
+
|
|
4658
|
+
// src/cli.ts
|
|
4659
|
+
import JSON5 from "json5";
|
|
4660
|
+
|
|
4661
|
+
// src/cli/run-resolver.ts
|
|
4662
|
+
import { simpleGit } from "simple-git";
|
|
4663
|
+
import filenamify from "filenamify";
|
|
4664
|
+
import { match as match6 } from "ts-pattern";
|
|
4665
|
+
import { dirname } from "path";
|
|
4666
|
+
import { fileURLToPath } from "url";
|
|
4667
|
+
async function resolveScriptPath(app, packageOrBundle, scriptRef, scriptArgs) {
|
|
4668
|
+
if (packageOrBundle && packageOrBundle.startsWith("core.") || !packageOrBundle && scriptRef && scriptRef.startsWith("core.")) {
|
|
4669
|
+
const name = packageOrBundle?.startsWith("core.") ? packageOrBundle : scriptRef;
|
|
4670
|
+
const relPath = name.replace(/^core\./, "").split(".").join(Path.sep());
|
|
4671
|
+
const moduleDir = Path.new(dirname(fileURLToPath(import.meta.url)));
|
|
4672
|
+
const coreRoot = moduleDir.parent().join("core");
|
|
4673
|
+
const candidate = coreRoot.join(`${relPath}.ts`);
|
|
4674
|
+
if (!candidate.isFile()) {
|
|
4675
|
+
throw new Error(`Core task script not found: ${candidate}`);
|
|
4676
|
+
}
|
|
4677
|
+
if (packageOrBundle?.startsWith("core.") && scriptRef) {
|
|
4678
|
+
scriptArgs = [scriptRef, ...scriptArgs];
|
|
4679
|
+
}
|
|
4680
|
+
return { scriptRef: candidate.toString(), scriptArgs };
|
|
4681
|
+
}
|
|
4682
|
+
if (!packageOrBundle) {
|
|
4683
|
+
throw new Error("No bundle or package specified.");
|
|
4684
|
+
}
|
|
4685
|
+
const currentPackageOrBundle = packageOrBundle;
|
|
4686
|
+
let resolvedScriptRef = scriptRef ?? "";
|
|
4687
|
+
await match6(currentPackageOrBundle).when(
|
|
4688
|
+
(path3) => Path.new(path3).isFile() && Path.new(path3).ext() === ".zip",
|
|
4689
|
+
async () => {
|
|
4690
|
+
const tmpName = filenamify(currentPackageOrBundle);
|
|
4691
|
+
const destPath = app.tmpDir.join(tmpName);
|
|
4692
|
+
await unzipDirectory(currentPackageOrBundle, destPath.toString());
|
|
4693
|
+
resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
|
|
4694
|
+
}
|
|
4695
|
+
).when(
|
|
4696
|
+
(path3) => Path.new(path3).isDirectory(),
|
|
4697
|
+
async () => {
|
|
4698
|
+
const tmpName = filenamify(currentPackageOrBundle);
|
|
4699
|
+
const destPath = app.tmpDir.join(tmpName);
|
|
4700
|
+
const pkgPath = Path.new(currentPackageOrBundle);
|
|
4701
|
+
await pkgPath.copy(destPath.toString());
|
|
4702
|
+
resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
|
|
4703
|
+
}
|
|
4704
|
+
).when(isValidUrl, async () => {
|
|
4705
|
+
const tmpName = filenamify(currentPackageOrBundle);
|
|
4706
|
+
const destPath = app.tmpDir.join(tmpName);
|
|
4707
|
+
const git = simpleGit();
|
|
4708
|
+
await git.clone(currentPackageOrBundle, destPath.toString());
|
|
4709
|
+
resolvedScriptRef = destPath.resolve(resolvedScriptRef).toString();
|
|
4710
|
+
}).otherwise(async () => {
|
|
4711
|
+
if (resolvedScriptRef) {
|
|
4712
|
+
A2(scriptArgs).prepend(resolvedScriptRef);
|
|
4713
|
+
}
|
|
4714
|
+
resolvedScriptRef = currentPackageOrBundle;
|
|
4715
|
+
});
|
|
4716
|
+
return { scriptRef: resolvedScriptRef, scriptArgs };
|
|
4717
|
+
}
|
|
4718
|
+
function isValidUrl(url) {
|
|
4719
|
+
try {
|
|
4720
|
+
new URL(url);
|
|
4721
|
+
return true;
|
|
4722
|
+
} catch {
|
|
4723
|
+
return false;
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
// src/cli.ts
|
|
4728
|
+
var isError2 = (value) => !!value && typeof value === "object" && "message" in value && typeof value.message === "string" && "stack" in value && typeof value.stack === "string";
|
|
4729
|
+
var logError = (message, error) => {
|
|
4730
|
+
console.error(`Error: ${message}`);
|
|
4731
|
+
if (isError2(error)) {
|
|
4732
|
+
console.error(error.stack);
|
|
4733
|
+
} else {
|
|
4734
|
+
try {
|
|
4735
|
+
const unknownError = new Error(
|
|
4736
|
+
`Unexpected value thrown: ${typeof error === "object" ? JSON.stringify(error) : String(error)}`
|
|
4737
|
+
);
|
|
4738
|
+
console.error(unknownError.stack);
|
|
4739
|
+
} catch {
|
|
4740
|
+
console.error(new Error(`Unexpected value thrown: non-stringifiable object`).stack);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
};
|
|
4744
|
+
function isValidUrl2(url) {
|
|
4745
|
+
try {
|
|
4746
|
+
new URL(url);
|
|
4747
|
+
return true;
|
|
4748
|
+
} catch (_) {
|
|
4749
|
+
return false;
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
function increaseVerbosity(dummyValue, previous) {
|
|
4753
|
+
return previous + 1;
|
|
4754
|
+
}
|
|
4755
|
+
function decreaseVerbosity(dummyValue, previous) {
|
|
4756
|
+
return previous - 1;
|
|
4757
|
+
}
|
|
4758
|
+
var Cli = class {
|
|
4759
|
+
app;
|
|
4760
|
+
program;
|
|
4761
|
+
constructor() {
|
|
4762
|
+
this.app = new App();
|
|
4763
|
+
this.program = new cmdr.Command();
|
|
4764
|
+
this.program.configureHelp({ showGlobalOptions: true }).name("hostctl").version(version).option("-q, --quiet", "quiet; be less verbose; may be specified multiple times", decreaseVerbosity, 0).option(
|
|
4765
|
+
"-v, --verbose",
|
|
4766
|
+
"verbose; may be specified multiple times for increased verbosity",
|
|
4767
|
+
increaseVerbosity,
|
|
4768
|
+
Verbosity.WARN
|
|
4769
|
+
).option("-c, --config <file path>", "config file path or http endpoint").option("--json", "output should be json formatted").option("-p, --password", "should prompt for sudo password?", false).option("-t, --tag <tags...>", "specify a tag (repeat for multiple tags)");
|
|
4770
|
+
this.program.command("bundle").alias("b").argument("[path]", `the path to bundle (e.g. hostctl bundle .)`, ".").description("bundle the given path as an npm project").action(this.handleBundle.bind(this));
|
|
4771
|
+
this.program.command("exec").alias("e").argument(
|
|
4772
|
+
"<command...>",
|
|
4773
|
+
`the command string to run, with optional arguments (e.g. hostctl exec sudo sh -c 'echo "$(whoami)"')`
|
|
4774
|
+
).description("execute a command on the selected hosts (default)").action(this.handleExec.bind(this));
|
|
4775
|
+
const inventoryCmd = this.program.command("inventory").alias("i").description("manage inventory and view reports");
|
|
4776
|
+
inventoryCmd.command("report", { isDefault: true }).description("print out an inventory report (default)").action(this.handleInventory.bind(this));
|
|
4777
|
+
inventoryCmd.command("encrypt").alias("e").description("encrypt the inventory file").action(this.handleInventoryEncrypt.bind(this));
|
|
4778
|
+
inventoryCmd.command("decrypt").alias("d").description("decrypt the inventory file").action(this.handleInventoryDecrypt.bind(this));
|
|
4779
|
+
inventoryCmd.command("list").alias("ls").description("list the hosts in the inventory file").action(this.handleInventoryList.bind(this));
|
|
4780
|
+
this.program.command("run").alias("r").argument(
|
|
4781
|
+
"[package_or_bundle]",
|
|
4782
|
+
`the package or bundle to run the specified <script> from.
|
|
4783
|
+
The package or bundle may be either:
|
|
4784
|
+
- a directory in the local filesystem, e.g. /my/package/foo
|
|
4785
|
+
- a git repository, e.g. https://github.com/hostctl/core
|
|
4786
|
+
- a zipped package (a bundle), e.g. packagebundle123.zip
|
|
4787
|
+
`
|
|
4788
|
+
).argument(
|
|
4789
|
+
"[script]",
|
|
4790
|
+
`the hostctl script to run (e.g. hostctl run myscript OR hostctl run mypackage myscript).
|
|
4791
|
+
The script may be specified as either:
|
|
4792
|
+
- a package followed by the script, e.g. hostctl run github.com/hostctl/core echo args:hello,world
|
|
4793
|
+
- a script, e.g. hostctl run main.ts
|
|
4794
|
+
- a zipped package (a bundle), e.g. hostctl run packagebundle.zip echo args:hello,world`
|
|
4795
|
+
).argument("[script arguments...]", "the runtime arguments to the script.").description("run a hostctl script (default)").option(
|
|
4796
|
+
"-f, --file <path>",
|
|
4797
|
+
"runtime parameters should be read from file at <path> containing json object representing params to supply to the script"
|
|
4798
|
+
).option(
|
|
4799
|
+
'-p, --params "<json object>"',
|
|
4800
|
+
"runtime parameters supplied as a string to be parsed as a json object representing params to supply to the script"
|
|
4801
|
+
).option("-r, --remote", "run the script on remote hosts specified by tags via SSH orchestration").action(this.handleRun.bind(this));
|
|
4802
|
+
const runtimeCmd = this.program.command("runtime").alias("rt").description("print out a report of the runtime environment").action(this.handleRuntime.bind(this));
|
|
4803
|
+
runtimeCmd.command("install").alias("i").description("install a temporary runtime environment on the local host if needed").action(this.handleRuntimeInstall.bind(this));
|
|
4804
|
+
}
|
|
4805
|
+
async handleBundle(path3, options, cmd) {
|
|
4806
|
+
const opts = cmd.optsWithGlobals();
|
|
4807
|
+
await this.loadApp(opts);
|
|
4808
|
+
this.app.bundleProject(path3);
|
|
4809
|
+
}
|
|
4810
|
+
async handleInventory(options, cmd) {
|
|
4811
|
+
const opts = cmd.optsWithGlobals();
|
|
4812
|
+
await this.loadApp(opts);
|
|
4813
|
+
this.app.printInventoryReport();
|
|
4814
|
+
}
|
|
4815
|
+
async handleInventoryEncrypt(options, cmd) {
|
|
4816
|
+
const opts = cmd.optsWithGlobals();
|
|
4817
|
+
await this.loadApp(opts);
|
|
4818
|
+
await this.app.encryptInventoryFile();
|
|
4819
|
+
}
|
|
4820
|
+
async handleInventoryDecrypt(options, cmd) {
|
|
4821
|
+
const opts = cmd.optsWithGlobals();
|
|
4822
|
+
await this.loadApp(opts);
|
|
4823
|
+
await this.app.decryptInventoryFile();
|
|
4824
|
+
}
|
|
4825
|
+
async handleInventoryList(options, cmd) {
|
|
4826
|
+
const opts = cmd.optsWithGlobals();
|
|
4827
|
+
await this.loadApp(opts);
|
|
4828
|
+
if (!this.app.config) {
|
|
4829
|
+
console.log("Configuration could not be loaded. Cannot list hosts.");
|
|
4830
|
+
if (!this.app.configRef) {
|
|
4831
|
+
console.log("(Also, no configuration file path was found by the app instance)");
|
|
4832
|
+
}
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
console.log("Config file: " + this.app.configRef);
|
|
4836
|
+
const hosts = this.app.selectedInventory();
|
|
4837
|
+
if (hosts.length > 0) {
|
|
4838
|
+
console.log("\nHosts in inventory:");
|
|
4839
|
+
hosts.forEach((host) => {
|
|
4840
|
+
let hostIdentifier = host.shortName || "(Unnamed host)";
|
|
4841
|
+
console.log(` - ${hostIdentifier}`);
|
|
4842
|
+
if (host.tags && host.tags.length > 0) {
|
|
4843
|
+
console.log(` Tags: ${host.tags.join(", ")}`);
|
|
4844
|
+
}
|
|
4845
|
+
});
|
|
4846
|
+
} else {
|
|
4847
|
+
const selectedTagsSet = this.app.selectedTags;
|
|
4848
|
+
const selectedTagsArray = Array.from(selectedTagsSet);
|
|
4849
|
+
if (selectedTagsArray.length > 0) {
|
|
4850
|
+
console.log(`No hosts found matching tags: ${selectedTagsArray.join(", ")}`);
|
|
4851
|
+
} else {
|
|
4852
|
+
console.log("No hosts found in the inventory.");
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
async handleExec(commandParts, options, cmd) {
|
|
4857
|
+
const opts = cmd.optsWithGlobals();
|
|
4858
|
+
await this.loadApp(opts);
|
|
4859
|
+
this.app.warn(`Executing command: ${commandParts}`);
|
|
4860
|
+
const success = await this.app.execute(commandParts);
|
|
4861
|
+
if (!success) {
|
|
4862
|
+
process4.exitCode = 1;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
async handleRun(packageOrBundle, scriptRef, scriptArgs, options, cmd) {
|
|
4866
|
+
const opts = cmd.optsWithGlobals();
|
|
4867
|
+
await this.loadApp(opts);
|
|
4868
|
+
this.app.debug(`tags: ${opts.tag}, remote: ${opts.remote}`);
|
|
4869
|
+
const resolved = await resolveScriptPath(this.app, packageOrBundle, scriptRef, scriptArgs);
|
|
4870
|
+
scriptRef = resolved.scriptRef;
|
|
4871
|
+
scriptArgs = resolved.scriptArgs;
|
|
4872
|
+
let params = {};
|
|
4873
|
+
try {
|
|
4874
|
+
if (options.params) {
|
|
4875
|
+
params = JSON5.parse(options.params);
|
|
4876
|
+
} else if (options.file) {
|
|
4877
|
+
const contents = File.readSync(options.file);
|
|
4878
|
+
params = JSON5.parse(contents);
|
|
4879
|
+
} else {
|
|
4880
|
+
params = this.app.parseParams(scriptArgs);
|
|
4881
|
+
}
|
|
4882
|
+
} catch (err) {
|
|
4883
|
+
this.app.error(
|
|
4884
|
+
`Failed to parse params as JSON5: ${err.message}`,
|
|
4885
|
+
`params: ${JSON5.stringify(options.params)}`
|
|
4886
|
+
);
|
|
4887
|
+
throw new Error(`Failed to parse params as JSON5: ${err.message}`);
|
|
4888
|
+
}
|
|
4889
|
+
if (!scriptRef) {
|
|
4890
|
+
this.program.help();
|
|
4891
|
+
}
|
|
4892
|
+
this.app.debug(`Resolved script: ${scriptRef}`);
|
|
4893
|
+
this.app.debug(`Script params: ${JSON.stringify(params)}`);
|
|
4894
|
+
if (opts.remote) {
|
|
4895
|
+
await this.app.runScriptRemote(scriptRef, params);
|
|
4896
|
+
} else {
|
|
4897
|
+
await this.app.runScript(scriptRef, params);
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
deriveConfigRef(suppliedConfigRef) {
|
|
4901
|
+
if (suppliedConfigRef && Path.new(suppliedConfigRef).isFile()) {
|
|
4902
|
+
return Path.new(suppliedConfigRef).toString();
|
|
4903
|
+
}
|
|
4904
|
+
const foundPath = Path.cwd().selfAndAncestors().find((p) => p.join("hostctl.yaml").isFile());
|
|
4905
|
+
if (foundPath) {
|
|
4906
|
+
return foundPath.join("hostctl.yaml").toString();
|
|
4907
|
+
}
|
|
4908
|
+
const homeConfigPath = Path.homeDir().join(".hostctl", "hostctl.yaml");
|
|
4909
|
+
if (homeConfigPath.isFile()) {
|
|
4910
|
+
return homeConfigPath.toString();
|
|
4911
|
+
}
|
|
4912
|
+
if (suppliedConfigRef && isValidUrl2(suppliedConfigRef)) {
|
|
4913
|
+
return suppliedConfigRef;
|
|
4914
|
+
}
|
|
4915
|
+
throw new Error(
|
|
4916
|
+
"Could not find a hostctl configuration file. Searched for 'hostctl.yaml' in the current directory, ancestor directories, and '~/.hostctl/hostctl.yaml'. If you intended to use a URL or a specific file path, please ensure it's correct and accessible via the -c option."
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
async loadApp(opts) {
|
|
4920
|
+
const derivedConfigRef = this.deriveConfigRef(opts.config);
|
|
4921
|
+
await this.app.loadConfig(derivedConfigRef);
|
|
4922
|
+
const verbosity = opts.quiet + opts.verbose;
|
|
4923
|
+
this.app.setVerbosity(verbosity);
|
|
4924
|
+
if (opts.json) {
|
|
4925
|
+
this.app.setOutputStyle("json");
|
|
4926
|
+
} else {
|
|
4927
|
+
this.app.setOutputStyle("plain");
|
|
4928
|
+
}
|
|
4929
|
+
let tagsToSet = [];
|
|
4930
|
+
if (typeof opts.tag === "string") {
|
|
4931
|
+
tagsToSet = [opts.tag];
|
|
4932
|
+
} else if (Array.isArray(opts.tag)) {
|
|
4933
|
+
tagsToSet = opts.tag;
|
|
4934
|
+
}
|
|
4935
|
+
this.app.setSelectedTags(tagsToSet);
|
|
4936
|
+
}
|
|
4937
|
+
async run() {
|
|
4938
|
+
try {
|
|
4939
|
+
await this.program.parseAsync(process4.argv);
|
|
4940
|
+
} catch (e) {
|
|
4941
|
+
logError("hostctl error:", e);
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
async handleRuntime(options, cmd) {
|
|
4945
|
+
const opts = cmd.optsWithGlobals();
|
|
4946
|
+
await this.loadApp(opts);
|
|
4947
|
+
await this.app.printRuntimeReport();
|
|
4948
|
+
}
|
|
4949
|
+
async handleRuntimeInstall(options, cmd) {
|
|
4950
|
+
const opts = cmd.optsWithGlobals();
|
|
4951
|
+
await this.loadApp(opts);
|
|
4952
|
+
await this.app.installRuntime();
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
|
|
4956
|
+
// src/bin/hostctl.ts
|
|
4957
|
+
var cli = new Cli();
|
|
4958
|
+
cli.program.parseAsync(process.argv).catch((error) => {
|
|
4959
|
+
console.error("Unhandled error in hostctl binary:", error);
|
|
4960
|
+
process.exit(1);
|
|
4961
|
+
});
|
|
4962
|
+
//# sourceMappingURL=hostctl.js.map
|