@foresthubai/workflow-cli 0.4.3 → 0.4.5
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 +661 -661
- package/NOTICE +23 -23
- package/README.md +137 -137
- package/dist/assets/index-C4Hm2Pud.js +75 -0
- package/dist/assets/index-CiLOHdeR.css +1 -0
- package/dist/index.html +14 -14
- package/dist-cli/cli.js +1325 -503
- package/dist-cli/deployment.yaml +105 -0
- package/dist-cli/llmproxy.yaml +303 -300
- package/dist-cli/workflow.yaml +913 -895
- package/package.json +2 -3
- package/dist/assets/index-CnM-MQ9w.css +0 -1
- package/dist/assets/index-LJpK2FES.js +0 -75
package/dist-cli/cli.js
CHANGED
|
@@ -3232,8 +3232,8 @@ var require_utils = __commonJS({
|
|
|
3232
3232
|
}
|
|
3233
3233
|
return ind;
|
|
3234
3234
|
}
|
|
3235
|
-
function removeDotSegments(
|
|
3236
|
-
let input =
|
|
3235
|
+
function removeDotSegments(path13) {
|
|
3236
|
+
let input = path13;
|
|
3237
3237
|
const output = [];
|
|
3238
3238
|
let nextSlash = -1;
|
|
3239
3239
|
let len = 0;
|
|
@@ -3485,8 +3485,8 @@ var require_schemes = __commonJS({
|
|
|
3485
3485
|
wsComponent.secure = void 0;
|
|
3486
3486
|
}
|
|
3487
3487
|
if (wsComponent.resourceName) {
|
|
3488
|
-
const [
|
|
3489
|
-
wsComponent.path =
|
|
3488
|
+
const [path13, query] = wsComponent.resourceName.split("?");
|
|
3489
|
+
wsComponent.path = path13 && path13 !== "/" ? path13 : void 0;
|
|
3490
3490
|
wsComponent.query = query;
|
|
3491
3491
|
wsComponent.resourceName = void 0;
|
|
3492
3492
|
}
|
|
@@ -3641,7 +3641,7 @@ var require_fast_uri = __commonJS({
|
|
|
3641
3641
|
normalizeString(uri, options);
|
|
3642
3642
|
} else if (typeof uri === "object") {
|
|
3643
3643
|
uri = /** @type {T} */
|
|
3644
|
-
parse3(
|
|
3644
|
+
parse3(serialize8(uri, options), options);
|
|
3645
3645
|
}
|
|
3646
3646
|
return uri;
|
|
3647
3647
|
}
|
|
@@ -3649,13 +3649,13 @@ var require_fast_uri = __commonJS({
|
|
|
3649
3649
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3650
3650
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3651
3651
|
schemelessOptions.skipEscape = true;
|
|
3652
|
-
return
|
|
3652
|
+
return serialize8(resolved, schemelessOptions);
|
|
3653
3653
|
}
|
|
3654
3654
|
function resolveComponent(base, relative, options, skipNormalization) {
|
|
3655
3655
|
const target = {};
|
|
3656
3656
|
if (!skipNormalization) {
|
|
3657
|
-
base = parse3(
|
|
3658
|
-
relative = parse3(
|
|
3657
|
+
base = parse3(serialize8(base, options), options);
|
|
3658
|
+
relative = parse3(serialize8(relative, options), options);
|
|
3659
3659
|
}
|
|
3660
3660
|
options = options || {};
|
|
3661
3661
|
if (!options.tolerant && relative.scheme) {
|
|
@@ -3709,7 +3709,7 @@ var require_fast_uri = __commonJS({
|
|
|
3709
3709
|
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3710
3710
|
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3711
3711
|
}
|
|
3712
|
-
function
|
|
3712
|
+
function serialize8(cmpts, opts) {
|
|
3713
3713
|
const component = {
|
|
3714
3714
|
host: cmpts.host,
|
|
3715
3715
|
scheme: cmpts.scheme,
|
|
@@ -3887,7 +3887,7 @@ var require_fast_uri = __commonJS({
|
|
|
3887
3887
|
function normalizeStringWithStatus(uri, opts) {
|
|
3888
3888
|
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3889
3889
|
return {
|
|
3890
|
-
normalized: malformedAuthorityOrPort ? uri :
|
|
3890
|
+
normalized: malformedAuthorityOrPort ? uri : serialize8(parsed, opts),
|
|
3891
3891
|
malformedAuthorityOrPort
|
|
3892
3892
|
};
|
|
3893
3893
|
}
|
|
@@ -3897,7 +3897,7 @@ var require_fast_uri = __commonJS({
|
|
|
3897
3897
|
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3898
3898
|
}
|
|
3899
3899
|
if (typeof uri === "object") {
|
|
3900
|
-
return
|
|
3900
|
+
return serialize8(uri, opts);
|
|
3901
3901
|
}
|
|
3902
3902
|
}
|
|
3903
3903
|
var fastUri = {
|
|
@@ -3906,7 +3906,7 @@ var require_fast_uri = __commonJS({
|
|
|
3906
3906
|
resolve,
|
|
3907
3907
|
resolveComponent,
|
|
3908
3908
|
equal,
|
|
3909
|
-
serialize:
|
|
3909
|
+
serialize: serialize8,
|
|
3910
3910
|
parse: parse3
|
|
3911
3911
|
};
|
|
3912
3912
|
module.exports = fastUri;
|
|
@@ -4034,7 +4034,7 @@ var require_core = __commonJS({
|
|
|
4034
4034
|
uriResolver
|
|
4035
4035
|
};
|
|
4036
4036
|
}
|
|
4037
|
-
var
|
|
4037
|
+
var Ajv3 = class {
|
|
4038
4038
|
constructor(opts = {}) {
|
|
4039
4039
|
this.schemas = {};
|
|
4040
4040
|
this.refs = {};
|
|
@@ -4404,9 +4404,9 @@ var require_core = __commonJS({
|
|
|
4404
4404
|
}
|
|
4405
4405
|
}
|
|
4406
4406
|
};
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
exports.default =
|
|
4407
|
+
Ajv3.ValidationError = validation_error_1.default;
|
|
4408
|
+
Ajv3.MissingRefError = ref_error_1.default;
|
|
4409
|
+
exports.default = Ajv3;
|
|
4410
4410
|
function checkOptions(checkOpts, options, msg, log = "error") {
|
|
4411
4411
|
for (const key in checkOpts) {
|
|
4412
4412
|
const opt = key;
|
|
@@ -6517,7 +6517,7 @@ var require_ajv = __commonJS({
|
|
|
6517
6517
|
var draft7MetaSchema = require_json_schema_draft_07();
|
|
6518
6518
|
var META_SUPPORT_DATA = ["/properties"];
|
|
6519
6519
|
var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema";
|
|
6520
|
-
var
|
|
6520
|
+
var Ajv3 = class extends core_1.default {
|
|
6521
6521
|
_addVocabularies() {
|
|
6522
6522
|
super._addVocabularies();
|
|
6523
6523
|
draft7_1.default.forEach((v) => this.addVocabulary(v));
|
|
@@ -6536,11 +6536,11 @@ var require_ajv = __commonJS({
|
|
|
6536
6536
|
return this.opts.defaultMeta = super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : void 0);
|
|
6537
6537
|
}
|
|
6538
6538
|
};
|
|
6539
|
-
exports.Ajv =
|
|
6540
|
-
module.exports = exports =
|
|
6541
|
-
module.exports.Ajv =
|
|
6539
|
+
exports.Ajv = Ajv3;
|
|
6540
|
+
module.exports = exports = Ajv3;
|
|
6541
|
+
module.exports.Ajv = Ajv3;
|
|
6542
6542
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6543
|
-
exports.default =
|
|
6543
|
+
exports.default = Ajv3;
|
|
6544
6544
|
var validate_1 = require_validate();
|
|
6545
6545
|
Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() {
|
|
6546
6546
|
return validate_1.KeywordCxt;
|
|
@@ -9932,7 +9932,7 @@ var require_core3 = __commonJS({
|
|
|
9932
9932
|
uriResolver
|
|
9933
9933
|
};
|
|
9934
9934
|
}
|
|
9935
|
-
var
|
|
9935
|
+
var Ajv3 = class {
|
|
9936
9936
|
constructor(opts = {}) {
|
|
9937
9937
|
this.schemas = {};
|
|
9938
9938
|
this.refs = {};
|
|
@@ -10302,9 +10302,9 @@ var require_core3 = __commonJS({
|
|
|
10302
10302
|
}
|
|
10303
10303
|
}
|
|
10304
10304
|
};
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
exports.default =
|
|
10305
|
+
Ajv3.ValidationError = validation_error_1.default;
|
|
10306
|
+
Ajv3.MissingRefError = ref_error_1.default;
|
|
10307
|
+
exports.default = Ajv3;
|
|
10308
10308
|
function checkOptions(checkOpts, options, msg, log = "error") {
|
|
10309
10309
|
for (const key in checkOpts) {
|
|
10310
10310
|
const opt = key;
|
|
@@ -12415,7 +12415,7 @@ var require_ajv2 = __commonJS({
|
|
|
12415
12415
|
var draft7MetaSchema = require_json_schema_draft_072();
|
|
12416
12416
|
var META_SUPPORT_DATA = ["/properties"];
|
|
12417
12417
|
var META_SCHEMA_ID = "http://json-schema.org/draft-07/schema";
|
|
12418
|
-
var
|
|
12418
|
+
var Ajv3 = class extends core_1.default {
|
|
12419
12419
|
_addVocabularies() {
|
|
12420
12420
|
super._addVocabularies();
|
|
12421
12421
|
draft7_1.default.forEach((v) => this.addVocabulary(v));
|
|
@@ -12434,11 +12434,11 @@ var require_ajv2 = __commonJS({
|
|
|
12434
12434
|
return this.opts.defaultMeta = super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : void 0);
|
|
12435
12435
|
}
|
|
12436
12436
|
};
|
|
12437
|
-
exports.Ajv =
|
|
12438
|
-
module.exports = exports =
|
|
12439
|
-
module.exports.Ajv =
|
|
12437
|
+
exports.Ajv = Ajv3;
|
|
12438
|
+
module.exports = exports = Ajv3;
|
|
12439
|
+
module.exports.Ajv = Ajv3;
|
|
12440
12440
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12441
|
-
exports.default =
|
|
12441
|
+
exports.default = Ajv3;
|
|
12442
12442
|
var validate_1 = require_validate2();
|
|
12443
12443
|
Object.defineProperty(exports, "KeywordCxt", { enumerable: true, get: function() {
|
|
12444
12444
|
return validate_1.KeywordCxt;
|
|
@@ -12557,12 +12557,12 @@ var require_dist = __commonJS({
|
|
|
12557
12557
|
var fastName = new codegen_1.Name("fastFormats");
|
|
12558
12558
|
var formatsPlugin = (ajv, opts = { keywords: true }) => {
|
|
12559
12559
|
if (Array.isArray(opts)) {
|
|
12560
|
-
|
|
12560
|
+
addFormats3(ajv, opts, formats_1.fullFormats, fullName);
|
|
12561
12561
|
return ajv;
|
|
12562
12562
|
}
|
|
12563
12563
|
const [formats, exportName] = opts.mode === "fast" ? [formats_1.fastFormats, fastName] : [formats_1.fullFormats, fullName];
|
|
12564
12564
|
const list = opts.formats || formats_1.formatNames;
|
|
12565
|
-
|
|
12565
|
+
addFormats3(ajv, list, formats, exportName);
|
|
12566
12566
|
if (opts.keywords)
|
|
12567
12567
|
(0, limit_1.default)(ajv);
|
|
12568
12568
|
return ajv;
|
|
@@ -12574,12 +12574,12 @@ var require_dist = __commonJS({
|
|
|
12574
12574
|
throw new Error(`Unknown format "${name}"`);
|
|
12575
12575
|
return f;
|
|
12576
12576
|
};
|
|
12577
|
-
function
|
|
12577
|
+
function addFormats3(ajv, list, fs10, exportName) {
|
|
12578
12578
|
var _a3;
|
|
12579
12579
|
var _b;
|
|
12580
12580
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
12581
12581
|
for (const f of list)
|
|
12582
|
-
ajv.addFormat(f,
|
|
12582
|
+
ajv.addFormat(f, fs10[f]);
|
|
12583
12583
|
}
|
|
12584
12584
|
module.exports = exports = formatsPlugin;
|
|
12585
12585
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -12782,9 +12782,29 @@ function isAllowed(target, roots) {
|
|
|
12782
12782
|
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
12783
12783
|
});
|
|
12784
12784
|
}
|
|
12785
|
+
function isLoopbackHost(host) {
|
|
12786
|
+
if (!host) return false;
|
|
12787
|
+
const name = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.replace(/:\d+$/, "");
|
|
12788
|
+
return name === "localhost" || name === "127.0.0.1" || name === "[::1]";
|
|
12789
|
+
}
|
|
12790
|
+
function isTrustedRequest(req) {
|
|
12791
|
+
if (!isLoopbackHost(req.headers.host)) return false;
|
|
12792
|
+
const origin = req.headers.origin;
|
|
12793
|
+
if (origin === void 0) return true;
|
|
12794
|
+
try {
|
|
12795
|
+
return isLoopbackHost(new URL(origin).host);
|
|
12796
|
+
} catch {
|
|
12797
|
+
return false;
|
|
12798
|
+
}
|
|
12799
|
+
}
|
|
12785
12800
|
async function handleFileRequest(req, res, allowedRoots) {
|
|
12786
12801
|
const url2 = new URL(req.url ?? "", "http://localhost");
|
|
12787
12802
|
if (url2.pathname !== "/api/file") return false;
|
|
12803
|
+
if (!isTrustedRequest(req)) {
|
|
12804
|
+
res.statusCode = 403;
|
|
12805
|
+
res.end("forbidden: non-local request");
|
|
12806
|
+
return true;
|
|
12807
|
+
}
|
|
12788
12808
|
try {
|
|
12789
12809
|
const filePath = url2.searchParams.get("path");
|
|
12790
12810
|
if (!filePath) {
|
|
@@ -12810,7 +12830,7 @@ async function handleFileRequest(req, res, allowedRoots) {
|
|
|
12810
12830
|
}
|
|
12811
12831
|
return true;
|
|
12812
12832
|
}
|
|
12813
|
-
if (req.method === "PUT"
|
|
12833
|
+
if (req.method === "PUT") {
|
|
12814
12834
|
const chunks = [];
|
|
12815
12835
|
for await (const chunk of req) chunks.push(chunk);
|
|
12816
12836
|
const body = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -13121,6 +13141,11 @@ function toFunctionInfo(fn) {
|
|
|
13121
13141
|
}
|
|
13122
13142
|
|
|
13123
13143
|
// ../workflow-core/src/function/serialization.ts
|
|
13144
|
+
function serialize(fn) {
|
|
13145
|
+
const outputAssignments = {};
|
|
13146
|
+
for (const o of fn.outputs) outputAssignments[o.uid] = o.expression;
|
|
13147
|
+
return { functionInfo: toFunctionInfo(fn), outputAssignments };
|
|
13148
|
+
}
|
|
13124
13149
|
function deserialize(functionInfo, outputAssignments) {
|
|
13125
13150
|
return {
|
|
13126
13151
|
id: functionInfo.id,
|
|
@@ -13182,6 +13207,51 @@ function isToolFlow(type) {
|
|
|
13182
13207
|
}
|
|
13183
13208
|
|
|
13184
13209
|
// ../workflow-core/src/edge/serialization.ts
|
|
13210
|
+
function serialize2(edge) {
|
|
13211
|
+
const sourceHandle = edge.sourceHandle || "";
|
|
13212
|
+
const targetHandle = edge.targetHandle || "";
|
|
13213
|
+
const edgeType = edge.type;
|
|
13214
|
+
const from = { nodeId: edge.source, port: sourceHandle };
|
|
13215
|
+
const to = { nodeId: edge.target, port: targetHandle };
|
|
13216
|
+
switch (edgeType) {
|
|
13217
|
+
case "agentTask":
|
|
13218
|
+
return {
|
|
13219
|
+
id: edge.id,
|
|
13220
|
+
type: "agentTask",
|
|
13221
|
+
from,
|
|
13222
|
+
to,
|
|
13223
|
+
prompt: edge.data?.prompt ?? { expression: "", references: [], dataType: "string" }
|
|
13224
|
+
};
|
|
13225
|
+
case "agentChoice":
|
|
13226
|
+
return {
|
|
13227
|
+
id: edge.id,
|
|
13228
|
+
type: "agentChoice",
|
|
13229
|
+
from,
|
|
13230
|
+
to,
|
|
13231
|
+
...edge.data?.description ? { description: edge.data.description } : {}
|
|
13232
|
+
};
|
|
13233
|
+
case "agentDelegate":
|
|
13234
|
+
return {
|
|
13235
|
+
id: edge.id,
|
|
13236
|
+
type: "agentDelegate",
|
|
13237
|
+
from,
|
|
13238
|
+
to,
|
|
13239
|
+
...edge.data?.prompt ? { prompt: edge.data.prompt } : {},
|
|
13240
|
+
...edge.data?.description ? { description: edge.data.description } : {}
|
|
13241
|
+
};
|
|
13242
|
+
case "control":
|
|
13243
|
+
return { id: edge.id, type: "control", from, to };
|
|
13244
|
+
case "tool":
|
|
13245
|
+
return { id: edge.id, type: "tool", from, to };
|
|
13246
|
+
default:
|
|
13247
|
+
return {
|
|
13248
|
+
id: edge.id,
|
|
13249
|
+
type: sourceHandle.startsWith("ctrl") || targetHandle.startsWith("ctrl") ? "control" : "tool",
|
|
13250
|
+
from,
|
|
13251
|
+
to
|
|
13252
|
+
};
|
|
13253
|
+
}
|
|
13254
|
+
}
|
|
13185
13255
|
function deserialize2(apiEdge) {
|
|
13186
13256
|
let data;
|
|
13187
13257
|
if ((apiEdge.type === "agentTask" || apiEdge.type === "agentDelegate") && apiEdge.prompt) {
|
|
@@ -13271,6 +13341,14 @@ function isParameterActive(param, parameterValues, isToolInput) {
|
|
|
13271
13341
|
function isEmpty(value) {
|
|
13272
13342
|
return value === void 0 || value === null || value === "";
|
|
13273
13343
|
}
|
|
13344
|
+
function pruneArguments(args2, parameters, isToolInput = false) {
|
|
13345
|
+
for (const param of parameters) {
|
|
13346
|
+
const inactive = !!param.activationRules?.length && !isParameterActive(param, args2, isToolInput);
|
|
13347
|
+
if (inactive || isEmpty(args2[param.id])) {
|
|
13348
|
+
delete args2[param.id];
|
|
13349
|
+
}
|
|
13350
|
+
}
|
|
13351
|
+
}
|
|
13274
13352
|
function resolveExpressionType(param, args2, variables) {
|
|
13275
13353
|
if (param.fromReference) {
|
|
13276
13354
|
const ref = args2[param.fromReference];
|
|
@@ -13724,9 +13802,9 @@ var SerialWriteNodeDefinition = {
|
|
|
13724
13802
|
{
|
|
13725
13803
|
id: "portReference",
|
|
13726
13804
|
label: "Port",
|
|
13727
|
-
description: "Serial port to write to",
|
|
13805
|
+
description: "Serial port or log channel to write to",
|
|
13728
13806
|
type: "channelSelect",
|
|
13729
|
-
channelType: ["UART"]
|
|
13807
|
+
channelType: ["UART", "LOG"]
|
|
13730
13808
|
},
|
|
13731
13809
|
{
|
|
13732
13810
|
id: "value",
|
|
@@ -14105,7 +14183,7 @@ function isNodeUsedAsTool(nodeId, nodeData, edges) {
|
|
|
14105
14183
|
}
|
|
14106
14184
|
|
|
14107
14185
|
// ../workflow-core/src/channel/Channel.ts
|
|
14108
|
-
var ALL_CHANNEL_TYPES = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT"];
|
|
14186
|
+
var ALL_CHANNEL_TYPES = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT", "LOG"];
|
|
14109
14187
|
|
|
14110
14188
|
// ../workflow-core/src/channel/ChannelDefinition.ts
|
|
14111
14189
|
var CHANNEL_DEFINITION = {
|
|
@@ -14153,11 +14231,62 @@ var CHANNEL_DEFINITION = {
|
|
|
14153
14231
|
description: "MQTT topic this channel publishes to / subscribes on (e.g. sensors/temperature)",
|
|
14154
14232
|
type: "string",
|
|
14155
14233
|
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["MQTT"] }]
|
|
14234
|
+
},
|
|
14235
|
+
{
|
|
14236
|
+
id: "level",
|
|
14237
|
+
label: "Level",
|
|
14238
|
+
description: "Severity the engine records messages written to this channel at",
|
|
14239
|
+
type: "selection",
|
|
14240
|
+
default: "info",
|
|
14241
|
+
options: [
|
|
14242
|
+
{ value: "debug", label: "Debug" },
|
|
14243
|
+
{ value: "info", label: "Info" },
|
|
14244
|
+
{ value: "warn", label: "Warn" },
|
|
14245
|
+
{ value: "error", label: "Error" }
|
|
14246
|
+
],
|
|
14247
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["LOG"] }]
|
|
14248
|
+
},
|
|
14249
|
+
{
|
|
14250
|
+
id: "tag",
|
|
14251
|
+
label: "Tag",
|
|
14252
|
+
description: "Optional category stamped on each line so the backend can group workflow-emitted logs",
|
|
14253
|
+
type: "string",
|
|
14254
|
+
optional: true,
|
|
14255
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["LOG"] }]
|
|
14156
14256
|
}
|
|
14157
14257
|
]
|
|
14158
14258
|
};
|
|
14159
14259
|
|
|
14160
14260
|
// ../workflow-core/src/channel/serialization.ts
|
|
14261
|
+
function serialize3(ch) {
|
|
14262
|
+
const { id, label, type } = ch;
|
|
14263
|
+
const args2 = { ...ch.arguments, type };
|
|
14264
|
+
pruneArguments(args2, CHANNEL_DEFINITION.parameters);
|
|
14265
|
+
switch (type) {
|
|
14266
|
+
case "GPIOIN":
|
|
14267
|
+
return {
|
|
14268
|
+
type,
|
|
14269
|
+
id,
|
|
14270
|
+
label,
|
|
14271
|
+
bias: args2.bias,
|
|
14272
|
+
debounceMs: args2.debounceMs
|
|
14273
|
+
};
|
|
14274
|
+
case "GPIOOUT":
|
|
14275
|
+
return { type, id, label };
|
|
14276
|
+
case "ADC":
|
|
14277
|
+
return { type, id, label };
|
|
14278
|
+
case "PWM":
|
|
14279
|
+
return { type, id, label, frequency: args2.frequency };
|
|
14280
|
+
case "DAC":
|
|
14281
|
+
return { type, id, label };
|
|
14282
|
+
case "UART":
|
|
14283
|
+
return { type, id, label };
|
|
14284
|
+
case "MQTT":
|
|
14285
|
+
return { type, id, label, topic: args2.topic };
|
|
14286
|
+
case "LOG":
|
|
14287
|
+
return { type, id, label, level: args2.level, tag: args2.tag };
|
|
14288
|
+
}
|
|
14289
|
+
}
|
|
14161
14290
|
function deserialize3(api) {
|
|
14162
14291
|
const { id, label, type } = api;
|
|
14163
14292
|
const args2 = {};
|
|
@@ -14172,6 +14301,10 @@ function deserialize3(api) {
|
|
|
14172
14301
|
case "MQTT":
|
|
14173
14302
|
args2.topic = api.topic;
|
|
14174
14303
|
break;
|
|
14304
|
+
case "LOG":
|
|
14305
|
+
args2.level = api.level;
|
|
14306
|
+
args2.tag = api.tag;
|
|
14307
|
+
break;
|
|
14175
14308
|
case "GPIOOUT":
|
|
14176
14309
|
case "ADC":
|
|
14177
14310
|
case "DAC":
|
|
@@ -14254,6 +14387,27 @@ var MemoryRegistry = new MemoryDefinitionRegistry();
|
|
|
14254
14387
|
MemoryRegistry.initialize();
|
|
14255
14388
|
|
|
14256
14389
|
// ../workflow-core/src/memory/serialization.ts
|
|
14390
|
+
function serialize4(mem) {
|
|
14391
|
+
const { id, label, type, arguments: args2 } = mem;
|
|
14392
|
+
switch (type) {
|
|
14393
|
+
case "MemoryFile":
|
|
14394
|
+
return {
|
|
14395
|
+
type,
|
|
14396
|
+
id,
|
|
14397
|
+
label,
|
|
14398
|
+
description: args2.description ?? "",
|
|
14399
|
+
content: args2.content ?? "",
|
|
14400
|
+
...args2.maxSizeBytes != null ? { maxSizeBytes: args2.maxSizeBytes } : {}
|
|
14401
|
+
};
|
|
14402
|
+
case "VectorDatabase":
|
|
14403
|
+
return {
|
|
14404
|
+
type,
|
|
14405
|
+
id,
|
|
14406
|
+
label,
|
|
14407
|
+
...args2.description != null ? { description: args2.description } : {}
|
|
14408
|
+
};
|
|
14409
|
+
}
|
|
14410
|
+
}
|
|
14257
14411
|
function deserialize4(api) {
|
|
14258
14412
|
const { id, label, type } = api;
|
|
14259
14413
|
const args2 = {};
|
|
@@ -14301,6 +14455,18 @@ var ModelRegistry = new ModelDefinitionRegistry();
|
|
|
14301
14455
|
ModelRegistry.initialize();
|
|
14302
14456
|
|
|
14303
14457
|
// ../workflow-core/src/model/serialization.ts
|
|
14458
|
+
function serialize5(model) {
|
|
14459
|
+
const { id, label, type, arguments: args2 } = model;
|
|
14460
|
+
switch (type) {
|
|
14461
|
+
case "LLMModel":
|
|
14462
|
+
return {
|
|
14463
|
+
type,
|
|
14464
|
+
id,
|
|
14465
|
+
label,
|
|
14466
|
+
capabilities: args2.capabilities ?? ["chat"]
|
|
14467
|
+
};
|
|
14468
|
+
}
|
|
14469
|
+
}
|
|
14304
14470
|
function deserialize5(api) {
|
|
14305
14471
|
const { id, label, type } = api;
|
|
14306
14472
|
const args2 = {};
|
|
@@ -14349,6 +14515,269 @@ function buildFunctionNodeDef(fn, t = (key) => key) {
|
|
|
14349
14515
|
}
|
|
14350
14516
|
|
|
14351
14517
|
// ../workflow-core/src/node/serialization.ts
|
|
14518
|
+
function draft(value) {
|
|
14519
|
+
return value;
|
|
14520
|
+
}
|
|
14521
|
+
function serialize6(node, isToolInput) {
|
|
14522
|
+
const result = serializeNodeData(node, node.position, isToolInput);
|
|
14523
|
+
if (node.label) {
|
|
14524
|
+
result.label = node.label;
|
|
14525
|
+
}
|
|
14526
|
+
if ("arguments" in result && result.arguments) {
|
|
14527
|
+
const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : void 0;
|
|
14528
|
+
if (def) {
|
|
14529
|
+
pruneArguments(result.arguments, def.parameters, isToolInput);
|
|
14530
|
+
}
|
|
14531
|
+
const args2 = result.arguments;
|
|
14532
|
+
for (const key of Object.keys(args2)) {
|
|
14533
|
+
if (args2[key] === void 0) delete args2[key];
|
|
14534
|
+
}
|
|
14535
|
+
}
|
|
14536
|
+
return result;
|
|
14537
|
+
}
|
|
14538
|
+
function serializeNodeData(data, position, isToolInput) {
|
|
14539
|
+
switch (data.type) {
|
|
14540
|
+
case "ReadPin":
|
|
14541
|
+
return {
|
|
14542
|
+
id: data.id,
|
|
14543
|
+
type: data.type,
|
|
14544
|
+
position,
|
|
14545
|
+
arguments: {
|
|
14546
|
+
pinReference: draft(data.arguments.pinReference),
|
|
14547
|
+
signalType: data.arguments.signalType,
|
|
14548
|
+
output: data.arguments.output,
|
|
14549
|
+
toolDescription: data.arguments.toolDescription
|
|
14550
|
+
}
|
|
14551
|
+
};
|
|
14552
|
+
case "SerialRead":
|
|
14553
|
+
return {
|
|
14554
|
+
id: data.id,
|
|
14555
|
+
type: data.type,
|
|
14556
|
+
position,
|
|
14557
|
+
arguments: {
|
|
14558
|
+
portReference: draft(data.arguments.portReference),
|
|
14559
|
+
...data.arguments.prompt !== void 0 ? { prompt: data.arguments.prompt } : {},
|
|
14560
|
+
output: data.arguments.output
|
|
14561
|
+
}
|
|
14562
|
+
};
|
|
14563
|
+
case "WritePin":
|
|
14564
|
+
return {
|
|
14565
|
+
id: data.id,
|
|
14566
|
+
type: data.type,
|
|
14567
|
+
position,
|
|
14568
|
+
arguments: {
|
|
14569
|
+
pinReference: draft(data.arguments.pinReference),
|
|
14570
|
+
signalType: data.arguments.signalType,
|
|
14571
|
+
value: data.arguments.value
|
|
14572
|
+
}
|
|
14573
|
+
};
|
|
14574
|
+
case "SerialWrite":
|
|
14575
|
+
return {
|
|
14576
|
+
id: data.id,
|
|
14577
|
+
type: data.type,
|
|
14578
|
+
position,
|
|
14579
|
+
arguments: {
|
|
14580
|
+
portReference: draft(data.arguments.portReference),
|
|
14581
|
+
value: data.arguments.value
|
|
14582
|
+
}
|
|
14583
|
+
};
|
|
14584
|
+
case "Agent": {
|
|
14585
|
+
return {
|
|
14586
|
+
id: data.id,
|
|
14587
|
+
type: data.type,
|
|
14588
|
+
position,
|
|
14589
|
+
arguments: {
|
|
14590
|
+
name: data.arguments.name,
|
|
14591
|
+
model: data.arguments.model,
|
|
14592
|
+
instructions: data.arguments.instructions,
|
|
14593
|
+
maxTurns: data.arguments.maxTurns,
|
|
14594
|
+
outputDeclarations: data.arguments.outputDeclarations,
|
|
14595
|
+
memoryRefs: data.arguments.memoryRefs ?? [],
|
|
14596
|
+
answer: data.arguments.answer,
|
|
14597
|
+
toolDescription: data.arguments.toolDescription
|
|
14598
|
+
}
|
|
14599
|
+
};
|
|
14600
|
+
}
|
|
14601
|
+
case "If":
|
|
14602
|
+
return {
|
|
14603
|
+
id: data.id,
|
|
14604
|
+
type: data.type,
|
|
14605
|
+
position,
|
|
14606
|
+
arguments: {
|
|
14607
|
+
condition: data.arguments.condition
|
|
14608
|
+
}
|
|
14609
|
+
};
|
|
14610
|
+
case "OnFunctionCall":
|
|
14611
|
+
return {
|
|
14612
|
+
id: data.id,
|
|
14613
|
+
type: data.type,
|
|
14614
|
+
position
|
|
14615
|
+
};
|
|
14616
|
+
case "OnStartup":
|
|
14617
|
+
return {
|
|
14618
|
+
id: data.id,
|
|
14619
|
+
type: data.type,
|
|
14620
|
+
position
|
|
14621
|
+
};
|
|
14622
|
+
case "OnPinEdge":
|
|
14623
|
+
return {
|
|
14624
|
+
id: data.id,
|
|
14625
|
+
type: data.type,
|
|
14626
|
+
position,
|
|
14627
|
+
arguments: {
|
|
14628
|
+
pinReference: draft(data.arguments.pinReference),
|
|
14629
|
+
edge: data.arguments.edge
|
|
14630
|
+
}
|
|
14631
|
+
};
|
|
14632
|
+
case "OnSerialReceive":
|
|
14633
|
+
return {
|
|
14634
|
+
id: data.id,
|
|
14635
|
+
type: data.type,
|
|
14636
|
+
position,
|
|
14637
|
+
arguments: {
|
|
14638
|
+
portReference: draft(data.arguments.portReference),
|
|
14639
|
+
output: data.arguments.output
|
|
14640
|
+
}
|
|
14641
|
+
};
|
|
14642
|
+
case "OnThreshold":
|
|
14643
|
+
return {
|
|
14644
|
+
id: data.id,
|
|
14645
|
+
type: data.type,
|
|
14646
|
+
position,
|
|
14647
|
+
arguments: {
|
|
14648
|
+
variable: draft(data.arguments.variable),
|
|
14649
|
+
threshold: draft(data.arguments.threshold),
|
|
14650
|
+
direction: data.arguments.direction,
|
|
14651
|
+
deadband: data.arguments.deadband,
|
|
14652
|
+
output: data.arguments.output
|
|
14653
|
+
}
|
|
14654
|
+
};
|
|
14655
|
+
case "Delay":
|
|
14656
|
+
return {
|
|
14657
|
+
id: data.id,
|
|
14658
|
+
type: data.type,
|
|
14659
|
+
position,
|
|
14660
|
+
arguments: {
|
|
14661
|
+
delayMs: draft(data.arguments.delayMs)
|
|
14662
|
+
}
|
|
14663
|
+
};
|
|
14664
|
+
case "Ticker":
|
|
14665
|
+
return {
|
|
14666
|
+
id: data.id,
|
|
14667
|
+
type: data.type,
|
|
14668
|
+
position,
|
|
14669
|
+
arguments: {
|
|
14670
|
+
intervalValue: draft(data.arguments.intervalValue),
|
|
14671
|
+
intervalUnit: data.arguments.intervalUnit
|
|
14672
|
+
}
|
|
14673
|
+
};
|
|
14674
|
+
case "Alarm":
|
|
14675
|
+
return {
|
|
14676
|
+
id: data.id,
|
|
14677
|
+
type: data.type,
|
|
14678
|
+
position,
|
|
14679
|
+
arguments: {
|
|
14680
|
+
time: data.arguments.time,
|
|
14681
|
+
days: data.arguments.days
|
|
14682
|
+
}
|
|
14683
|
+
};
|
|
14684
|
+
case "WebSearchTool":
|
|
14685
|
+
return {
|
|
14686
|
+
id: data.id,
|
|
14687
|
+
type: data.type,
|
|
14688
|
+
position,
|
|
14689
|
+
arguments: {
|
|
14690
|
+
maxResults: data.arguments.maxResults
|
|
14691
|
+
}
|
|
14692
|
+
};
|
|
14693
|
+
case "Retriever":
|
|
14694
|
+
return {
|
|
14695
|
+
id: data.id,
|
|
14696
|
+
type: data.type,
|
|
14697
|
+
position,
|
|
14698
|
+
arguments: {
|
|
14699
|
+
memoryReference: data.arguments.memoryReference,
|
|
14700
|
+
topK: draft(data.arguments.topK),
|
|
14701
|
+
query: data.arguments.query,
|
|
14702
|
+
output: data.arguments.output,
|
|
14703
|
+
toolDescription: data.arguments.toolDescription
|
|
14704
|
+
}
|
|
14705
|
+
};
|
|
14706
|
+
case "WebFetch":
|
|
14707
|
+
return {
|
|
14708
|
+
id: data.id,
|
|
14709
|
+
type: data.type,
|
|
14710
|
+
position,
|
|
14711
|
+
arguments: {
|
|
14712
|
+
url: data.arguments.url,
|
|
14713
|
+
maxChars: data.arguments.maxChars,
|
|
14714
|
+
output: data.arguments.output
|
|
14715
|
+
}
|
|
14716
|
+
};
|
|
14717
|
+
case "FunctionCall": {
|
|
14718
|
+
const inputBindings = {};
|
|
14719
|
+
const outputBindings = {};
|
|
14720
|
+
const args2 = data.arguments;
|
|
14721
|
+
for (const arg of data.functionInfo.arguments) {
|
|
14722
|
+
const key = arg.uid ?? arg.name;
|
|
14723
|
+
const v = args2[key];
|
|
14724
|
+
if (v !== void 0) inputBindings[key] = v;
|
|
14725
|
+
}
|
|
14726
|
+
for (const ret of data.functionInfo.returns) {
|
|
14727
|
+
const key = ret.uid ?? ret.name;
|
|
14728
|
+
const v = args2[key];
|
|
14729
|
+
if (v !== void 0) outputBindings[key] = v;
|
|
14730
|
+
}
|
|
14731
|
+
const toolDescription = args2.toolDescription;
|
|
14732
|
+
return {
|
|
14733
|
+
id: data.id,
|
|
14734
|
+
type: data.type,
|
|
14735
|
+
functionId: data.functionInfo.id,
|
|
14736
|
+
position,
|
|
14737
|
+
arguments: {
|
|
14738
|
+
inputBindings,
|
|
14739
|
+
outputBindings,
|
|
14740
|
+
...isToolInput && toolDescription !== void 0 ? { toolDescription } : {}
|
|
14741
|
+
}
|
|
14742
|
+
};
|
|
14743
|
+
}
|
|
14744
|
+
case "SetVariable":
|
|
14745
|
+
return {
|
|
14746
|
+
id: data.id,
|
|
14747
|
+
type: data.type,
|
|
14748
|
+
position,
|
|
14749
|
+
arguments: {
|
|
14750
|
+
variable: draft(data.arguments.variable),
|
|
14751
|
+
value: data.arguments.value
|
|
14752
|
+
}
|
|
14753
|
+
};
|
|
14754
|
+
case "MqttPublish":
|
|
14755
|
+
return {
|
|
14756
|
+
id: data.id,
|
|
14757
|
+
type: data.type,
|
|
14758
|
+
position,
|
|
14759
|
+
arguments: {
|
|
14760
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
14761
|
+
dataType: data.arguments.dataType,
|
|
14762
|
+
value: data.arguments.value,
|
|
14763
|
+
// Unset stays unset — Number(undefined) is NaN, which stringifies to null.
|
|
14764
|
+
qos: draft(data.arguments.qos === void 0 ? void 0 : Number(data.arguments.qos)),
|
|
14765
|
+
retain: data.arguments.retain
|
|
14766
|
+
}
|
|
14767
|
+
};
|
|
14768
|
+
case "OnMqttMessage":
|
|
14769
|
+
return {
|
|
14770
|
+
id: data.id,
|
|
14771
|
+
type: data.type,
|
|
14772
|
+
position,
|
|
14773
|
+
arguments: {
|
|
14774
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
14775
|
+
dataType: data.arguments.dataType,
|
|
14776
|
+
output: data.arguments.output
|
|
14777
|
+
}
|
|
14778
|
+
};
|
|
14779
|
+
}
|
|
14780
|
+
}
|
|
14352
14781
|
function deserialize6(apiNode, resolveFunctionInfo) {
|
|
14353
14782
|
return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
|
|
14354
14783
|
}
|
|
@@ -14587,6 +15016,43 @@ var MAIN_CANVAS_ID = "main";
|
|
|
14587
15016
|
|
|
14588
15017
|
// ../workflow-core/src/workflow/serialization.ts
|
|
14589
15018
|
var KNOWN_CHANNEL_TYPES = new Set(ALL_CHANNEL_TYPES);
|
|
15019
|
+
function serialize7(state) {
|
|
15020
|
+
const mainCanvas = state.canvases[MAIN_CANVAS_ID];
|
|
15021
|
+
if (!mainCanvas) {
|
|
15022
|
+
throw new Error("Main canvas missing");
|
|
15023
|
+
}
|
|
15024
|
+
const mainNodes = mainCanvas.nodes.map((n) => serialize6(n, isNodeUsedAsTool(n.id, n, mainCanvas.edges)));
|
|
15025
|
+
const mainEdges = mainCanvas.edges.map(serialize2);
|
|
15026
|
+
const mainDeclared = extractDeclaredVariables(mainCanvas.variables);
|
|
15027
|
+
const functions = [];
|
|
15028
|
+
for (const [id, decl] of Object.entries(state.functions)) {
|
|
15029
|
+
const body = state.canvases[id];
|
|
15030
|
+
if (!body) {
|
|
15031
|
+
throw new Error(`[workflow-core] function ${id} has no body canvas \u2014 cannot serialize`);
|
|
15032
|
+
}
|
|
15033
|
+
const { functionInfo, outputAssignments } = serialize(decl);
|
|
15034
|
+
functions.push({
|
|
15035
|
+
functionInfo,
|
|
15036
|
+
outputAssignments,
|
|
15037
|
+
nodes: body.nodes.map((n) => serialize6(n, isNodeUsedAsTool(n.id, n, body.edges))),
|
|
15038
|
+
edges: body.edges.map(serialize2),
|
|
15039
|
+
declaredVariables: extractDeclaredVariables(body.variables)
|
|
15040
|
+
});
|
|
15041
|
+
}
|
|
15042
|
+
const channels = Object.values(state.channels).map(serialize3);
|
|
15043
|
+
const memory = Object.values(state.memory).map(serialize4);
|
|
15044
|
+
const models = Object.values(state.models).map(serialize5);
|
|
15045
|
+
return {
|
|
15046
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
15047
|
+
nodes: mainNodes,
|
|
15048
|
+
edges: mainEdges,
|
|
15049
|
+
functions,
|
|
15050
|
+
declaredVariables: mainDeclared,
|
|
15051
|
+
channels,
|
|
15052
|
+
memory,
|
|
15053
|
+
models
|
|
15054
|
+
};
|
|
15055
|
+
}
|
|
14590
15056
|
function deserialize7(workflow) {
|
|
14591
15057
|
const canvases = {};
|
|
14592
15058
|
const functions = {};
|
|
@@ -14679,6 +15145,20 @@ function buildCanvasVariables(nodes, fnArgs, declaredVariables) {
|
|
|
14679
15145
|
}
|
|
14680
15146
|
return variables;
|
|
14681
15147
|
}
|
|
15148
|
+
function extractDeclaredVariables(variables) {
|
|
15149
|
+
const result = [];
|
|
15150
|
+
for (const v of Object.values(variables)) {
|
|
15151
|
+
if (v.kind === "declared") {
|
|
15152
|
+
result.push({
|
|
15153
|
+
uid: v.uid,
|
|
15154
|
+
name: v.name,
|
|
15155
|
+
dataType: v.dataType,
|
|
15156
|
+
...v.initialValue !== void 0 ? { initialValue: v.initialValue } : {}
|
|
15157
|
+
});
|
|
15158
|
+
}
|
|
15159
|
+
}
|
|
15160
|
+
return result;
|
|
15161
|
+
}
|
|
14682
15162
|
|
|
14683
15163
|
// ../workflow-core/src/expression/types.ts
|
|
14684
15164
|
function resolveExpression(apiExpr, availableVars) {
|
|
@@ -16575,12 +17055,21 @@ function printReport(file2, result) {
|
|
|
16575
17055
|
`);
|
|
16576
17056
|
for (const d of result.memoryDiagnostics) printDiagnostic(d, " ");
|
|
16577
17057
|
}
|
|
17058
|
+
if (result.modelDiagnostics.length > 0) {
|
|
17059
|
+
out.write(`
|
|
17060
|
+
models:
|
|
17061
|
+
`);
|
|
17062
|
+
for (const d of result.modelDiagnostics) printDiagnostic(d, " ");
|
|
17063
|
+
}
|
|
16578
17064
|
}
|
|
16579
17065
|
function printDiagnostic(d, indent) {
|
|
16580
17066
|
const where = [];
|
|
16581
17067
|
if (d.nodeId) where.push(`node ${d.nodeId}`);
|
|
16582
17068
|
if (d.edgeId) where.push(`edge ${d.edgeId}`);
|
|
16583
17069
|
if (d.channelId) where.push(`channel ${d.channelId}`);
|
|
17070
|
+
if (d.memoryId) where.push(`memory ${d.memoryId}`);
|
|
17071
|
+
if (d.modelId) where.push(`model ${d.modelId}`);
|
|
17072
|
+
if (d.functionId) where.push(`function ${d.functionId}`);
|
|
16584
17073
|
if (d.paramId) where.push(`param ${d.paramId}`);
|
|
16585
17074
|
if (d.outputId) where.push(`output ${d.outputId}`);
|
|
16586
17075
|
const location = where.length > 0 ? ` (${where.join(", ")})` : "";
|
|
@@ -16595,7 +17084,11 @@ function pluralize(n) {
|
|
|
16595
17084
|
// cli/check-schema.ts
|
|
16596
17085
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
16597
17086
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
16598
|
-
import {
|
|
17087
|
+
import { promises as fs5 } from "node:fs";
|
|
17088
|
+
import path7 from "node:path";
|
|
17089
|
+
|
|
17090
|
+
// cli/contract.ts
|
|
17091
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16599
17092
|
import path6 from "node:path";
|
|
16600
17093
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16601
17094
|
|
|
@@ -18961,7 +19454,7 @@ var import_js_yaml = /* @__PURE__ */ __toESM2((/* @__PURE__ */ __commonJSMin(((e
|
|
|
18961
19454
|
var { Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, load, loadAll, dump, YAMLException, types, safeLoad, safeLoadAll, safeDump } = import_js_yaml.default;
|
|
18962
19455
|
var index_vite_proxy_tmp_default = import_js_yaml.default;
|
|
18963
19456
|
|
|
18964
|
-
// cli/
|
|
19457
|
+
// cli/contract.ts
|
|
18965
19458
|
var here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
18966
19459
|
var CONTRACT_DIR = existsSync(path6.join(here, "workflow.yaml")) ? here : path6.resolve(here, "../../../contract");
|
|
18967
19460
|
function loadContractDocument(fileName) {
|
|
@@ -18987,6 +19480,8 @@ function openApiToJsonSchema(value) {
|
|
|
18987
19480
|
}
|
|
18988
19481
|
return out;
|
|
18989
19482
|
}
|
|
19483
|
+
|
|
19484
|
+
// cli/check-schema.ts
|
|
18990
19485
|
var cachedValidator;
|
|
18991
19486
|
function compileWorkflowValidator() {
|
|
18992
19487
|
if (cachedValidator) return cachedValidator;
|
|
@@ -19011,7 +19506,7 @@ async function checkSchemaCommand(filePath) {
|
|
|
19011
19506
|
process.stderr.write("Usage: fh-workflow check-schema <file.json>\n");
|
|
19012
19507
|
process.exit(1);
|
|
19013
19508
|
}
|
|
19014
|
-
const abs =
|
|
19509
|
+
const abs = path7.resolve(process.cwd(), filePath);
|
|
19015
19510
|
let raw;
|
|
19016
19511
|
try {
|
|
19017
19512
|
raw = await fs5.readFile(abs, "utf-8");
|
|
@@ -19060,35 +19555,35 @@ function describeParams(e) {
|
|
|
19060
19555
|
return "";
|
|
19061
19556
|
}
|
|
19062
19557
|
|
|
19063
|
-
//
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19075
|
-
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
if (!modelId) continue;
|
|
19082
|
-
if (!customModelIds.has(modelId)) hasProviderModel = true;
|
|
19083
|
-
} else if (node.type === "Retriever") {
|
|
19084
|
-
hasRetriever = true;
|
|
19085
|
-
} else if (node.type === "WebSearchTool") {
|
|
19086
|
-
hasWebSearch = true;
|
|
19558
|
+
// ../workflow-core/src/deploy/requirements.ts
|
|
19559
|
+
function getReferencedCatalogModelIds(workflow) {
|
|
19560
|
+
const declaredModel = new Set(Object.keys(workflow.models));
|
|
19561
|
+
const catalogIds = /* @__PURE__ */ new Set();
|
|
19562
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
19563
|
+
for (const node of canvas.nodes) {
|
|
19564
|
+
const def = NodeRegistry.getByType(node.type);
|
|
19565
|
+
if (!def) continue;
|
|
19566
|
+
const args2 = node.arguments;
|
|
19567
|
+
const isToolInput = isNodeUsedAsTool(node.id, node, canvas.edges);
|
|
19568
|
+
for (const param of def.parameters) {
|
|
19569
|
+
if (param.type !== "modelSelect") continue;
|
|
19570
|
+
if (!isParameterActive(param, args2, isToolInput)) continue;
|
|
19571
|
+
const id = args2[param.id];
|
|
19572
|
+
if (typeof id === "string" && id !== "" && !declaredModel.has(id)) {
|
|
19573
|
+
catalogIds.add(id);
|
|
19574
|
+
}
|
|
19575
|
+
}
|
|
19087
19576
|
}
|
|
19088
19577
|
}
|
|
19578
|
+
return [...catalogIds];
|
|
19579
|
+
}
|
|
19580
|
+
function assertNeverChannel(t) {
|
|
19581
|
+
throw new Error(`unhandled channel type: ${String(t)}`);
|
|
19582
|
+
}
|
|
19583
|
+
function deriveRequirements(workflow) {
|
|
19089
19584
|
const hardwareChannels = [];
|
|
19090
19585
|
const mqttChannels = [];
|
|
19091
|
-
for (const channel of workflow.channels) {
|
|
19586
|
+
for (const channel of Object.values(workflow.channels)) {
|
|
19092
19587
|
switch (channel.type) {
|
|
19093
19588
|
case "GPIOIN":
|
|
19094
19589
|
case "GPIOOUT":
|
|
@@ -19109,14 +19604,277 @@ function inspect(workflow) {
|
|
|
19109
19604
|
case "MQTT":
|
|
19110
19605
|
mqttChannels.push({ id: channel.id, label: channel.label });
|
|
19111
19606
|
break;
|
|
19607
|
+
case "LOG":
|
|
19608
|
+
break;
|
|
19609
|
+
default:
|
|
19610
|
+
return assertNeverChannel(channel.type);
|
|
19611
|
+
}
|
|
19612
|
+
}
|
|
19613
|
+
let hasRetriever = false;
|
|
19614
|
+
let hasWebSearch = false;
|
|
19615
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
19616
|
+
for (const node of canvas.nodes) {
|
|
19617
|
+
if (node.type === "Retriever") hasRetriever = true;
|
|
19618
|
+
else if (node.type === "WebSearchTool") hasWebSearch = true;
|
|
19619
|
+
}
|
|
19620
|
+
}
|
|
19621
|
+
const customModels = Object.values(workflow.models).map((m) => ({ id: m.id, label: m.label }));
|
|
19622
|
+
return {
|
|
19623
|
+
hasProviderModel: getReferencedCatalogModelIds(workflow).length > 0,
|
|
19624
|
+
hasRetriever,
|
|
19625
|
+
hasWebSearch,
|
|
19626
|
+
hardwareChannels,
|
|
19627
|
+
mqttChannels,
|
|
19628
|
+
customModels
|
|
19629
|
+
};
|
|
19630
|
+
}
|
|
19631
|
+
|
|
19632
|
+
// ../workflow-core/src/deploy/spec.ts
|
|
19633
|
+
var COMPONENT_WORKSPACE_PATH = "/var/lib/foresthub/workspace";
|
|
19634
|
+
var STATE_ROOT = ".";
|
|
19635
|
+
function workspaceDir(container) {
|
|
19636
|
+
return `${STATE_ROOT}/workspaces/${container}`;
|
|
19637
|
+
}
|
|
19638
|
+
var RefAllocator = class {
|
|
19639
|
+
byKey = /* @__PURE__ */ new Map();
|
|
19640
|
+
used = /* @__PURE__ */ new Set();
|
|
19641
|
+
alloc(key, hint) {
|
|
19642
|
+
const existing = this.byKey.get(key);
|
|
19643
|
+
if (existing) return existing;
|
|
19644
|
+
let ref = hint;
|
|
19645
|
+
for (let n = 2; this.used.has(ref); n++) ref = `${hint}-${n}`;
|
|
19646
|
+
this.used.add(ref);
|
|
19647
|
+
this.byKey.set(key, ref);
|
|
19648
|
+
return ref;
|
|
19649
|
+
}
|
|
19650
|
+
};
|
|
19651
|
+
function basename(p) {
|
|
19652
|
+
const tail = p.replace(/\/+$/, "").split("/").pop() ?? p;
|
|
19653
|
+
return tail.replace(/[^A-Za-z0-9._:-]/g, "-") || "res";
|
|
19654
|
+
}
|
|
19655
|
+
function brokerHost(url2) {
|
|
19656
|
+
try {
|
|
19657
|
+
return new URL(url2).hostname || "mqtt";
|
|
19658
|
+
} catch {
|
|
19659
|
+
return "mqtt";
|
|
19660
|
+
}
|
|
19661
|
+
}
|
|
19662
|
+
function sidecarServiceName(modelId) {
|
|
19663
|
+
const slug = modelId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
19664
|
+
return `llama-${slug || "model"}`;
|
|
19665
|
+
}
|
|
19666
|
+
function ggufNameError(name) {
|
|
19667
|
+
const t = (name ?? "").trim();
|
|
19668
|
+
if (!t) return "a model filename is required";
|
|
19669
|
+
if (!t.toLowerCase().endsWith(".gguf")) return "must be a .gguf file (llama-server only loads GGUF)";
|
|
19670
|
+
if (t.includes("/")) return "just the filename, not a path \u2014 the file goes in the model's workspace dir (./workspaces/llama-\u2026/)";
|
|
19671
|
+
return null;
|
|
19672
|
+
}
|
|
19673
|
+
function hardwareAddressKey(family, chipOrDevice, index) {
|
|
19674
|
+
const dev = chipOrDevice.trim();
|
|
19675
|
+
return family === "serial" ? `serial:${dev}` : `${family}:${dev}:${index}`;
|
|
19676
|
+
}
|
|
19677
|
+
function hardwareAddressLabel(family, chipOrDevice, index) {
|
|
19678
|
+
const dev = chipOrDevice.trim();
|
|
19679
|
+
if (family === "serial") return dev;
|
|
19680
|
+
return `${dev} ${family === "gpio" ? "line" : "channel"} ${index}`;
|
|
19681
|
+
}
|
|
19682
|
+
function hardwareConflicts(channels, bindings) {
|
|
19683
|
+
const conflicts = [];
|
|
19684
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
19685
|
+
for (const ch of channels) {
|
|
19686
|
+
const b = bindings[ch.id];
|
|
19687
|
+
if (!b?.chipOrDevice || ch.addressable && b.index === void 0) continue;
|
|
19688
|
+
const key = hardwareAddressKey(ch.family, b.chipOrDevice, b.index);
|
|
19689
|
+
const holder = claimed.get(key);
|
|
19690
|
+
if (holder) {
|
|
19691
|
+
conflicts.push(`hardware "${ch.id}": ${hardwareAddressLabel(ch.family, b.chipOrDevice, b.index)} is already used by "${holder}"`);
|
|
19692
|
+
} else {
|
|
19693
|
+
claimed.set(key, ch.id);
|
|
19694
|
+
}
|
|
19695
|
+
}
|
|
19696
|
+
return conflicts;
|
|
19697
|
+
}
|
|
19698
|
+
function familyMismatches(channels, bindings) {
|
|
19699
|
+
const mismatches = [];
|
|
19700
|
+
for (const ch of channels) {
|
|
19701
|
+
const b = bindings[ch.id];
|
|
19702
|
+
if (!b) continue;
|
|
19703
|
+
if (ch.family !== "serial" && b.baud !== void 0) {
|
|
19704
|
+
mismatches.push(`hardware "${ch.id}": "baud" only applies to serial channels (this is a ${ch.family} channel)`);
|
|
19705
|
+
}
|
|
19706
|
+
if (ch.family === "serial" && b.index !== void 0) {
|
|
19707
|
+
mismatches.push(`hardware "${ch.id}": "index" does not apply to serial channels (the device path is the full address)`);
|
|
19708
|
+
}
|
|
19709
|
+
}
|
|
19710
|
+
return mismatches;
|
|
19711
|
+
}
|
|
19712
|
+
function assertNeverFamily(f) {
|
|
19713
|
+
throw new Error(`unhandled hardware family: ${String(f)}`);
|
|
19714
|
+
}
|
|
19715
|
+
function assertDeployable(req, inputs) {
|
|
19716
|
+
const missing = [];
|
|
19717
|
+
for (const ch of req.hardwareChannels) {
|
|
19718
|
+
const b = inputs.hardware[ch.id];
|
|
19719
|
+
if (!b?.chipOrDevice) missing.push(`hardware "${ch.id}": device path`);
|
|
19720
|
+
else if (ch.addressable && b.index === void 0) missing.push(`hardware "${ch.id}": index`);
|
|
19721
|
+
}
|
|
19722
|
+
for (const ch of req.mqttChannels) {
|
|
19723
|
+
if (!inputs.mqtt[ch.id]?.brokerUrl) missing.push(`mqtt "${ch.id}": broker URL`);
|
|
19724
|
+
}
|
|
19725
|
+
for (const m of req.customModels) {
|
|
19726
|
+
const b = inputs.models[m.id];
|
|
19727
|
+
if (b?.location === "device") {
|
|
19728
|
+
const err = ggufNameError(b.modelFile);
|
|
19729
|
+
if (err) missing.push(`model "${m.id}": ${err}`);
|
|
19730
|
+
} else if (!b?.url) {
|
|
19731
|
+
missing.push(`model "${m.id}": endpoint URL`);
|
|
19732
|
+
}
|
|
19733
|
+
}
|
|
19734
|
+
missing.push(...hardwareConflicts(req.hardwareChannels, inputs.hardware));
|
|
19735
|
+
missing.push(...familyMismatches(req.hardwareChannels, inputs.hardware));
|
|
19736
|
+
if (missing.length > 0) {
|
|
19737
|
+
throw new Error(`invalid deploy config:
|
|
19738
|
+
- ${missing.join("\n - ")}`);
|
|
19739
|
+
}
|
|
19740
|
+
}
|
|
19741
|
+
function assertNoNameCollisions(components) {
|
|
19742
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19743
|
+
for (const c of components) {
|
|
19744
|
+
if (seen.has(c.name)) throw new Error(`duplicate component name "${c.name}"`);
|
|
19745
|
+
seen.add(c.name);
|
|
19746
|
+
}
|
|
19747
|
+
}
|
|
19748
|
+
function buildDeploymentSpec(workflow, inputs, meta3, customComponents = []) {
|
|
19749
|
+
const req = deriveRequirements(workflow);
|
|
19750
|
+
assertDeployable(req, inputs);
|
|
19751
|
+
const refs = new RefAllocator();
|
|
19752
|
+
const resourceSecrets = {};
|
|
19753
|
+
const gpios = {};
|
|
19754
|
+
const adcs = {};
|
|
19755
|
+
const dacs = {};
|
|
19756
|
+
const pwms = {};
|
|
19757
|
+
const serials = {};
|
|
19758
|
+
const externalResources = {};
|
|
19759
|
+
const mapping = {};
|
|
19760
|
+
const cdev = /* @__PURE__ */ new Set();
|
|
19761
|
+
let privileged = false;
|
|
19762
|
+
for (const ch of req.hardwareChannels) {
|
|
19763
|
+
const b = inputs.hardware[ch.id];
|
|
19764
|
+
if (!b) throw new Error(`unbound hardware channel ${ch.id}`);
|
|
19765
|
+
const dev = b.chipOrDevice;
|
|
19766
|
+
const ref = refs.alloc(`hw:${ch.family}:${dev}`, basename(dev));
|
|
19767
|
+
switch (ch.family) {
|
|
19768
|
+
case "gpio":
|
|
19769
|
+
gpios[ref] = { chip: dev };
|
|
19770
|
+
cdev.add(dev);
|
|
19771
|
+
break;
|
|
19772
|
+
case "serial":
|
|
19773
|
+
serials[ref] = b.baud ? { device: dev, baud: b.baud } : { device: dev };
|
|
19774
|
+
cdev.add(dev);
|
|
19775
|
+
break;
|
|
19776
|
+
case "pwm":
|
|
19777
|
+
pwms[ref] = { chip: dev };
|
|
19778
|
+
privileged = true;
|
|
19779
|
+
break;
|
|
19780
|
+
case "adc":
|
|
19781
|
+
adcs[ref] = { device: dev };
|
|
19782
|
+
privileged = true;
|
|
19783
|
+
break;
|
|
19784
|
+
case "dac":
|
|
19785
|
+
dacs[ref] = { device: dev };
|
|
19786
|
+
privileged = true;
|
|
19787
|
+
break;
|
|
19112
19788
|
default:
|
|
19113
|
-
return
|
|
19789
|
+
return assertNeverFamily(ch.family);
|
|
19114
19790
|
}
|
|
19791
|
+
if (ch.addressable && b.index !== void 0) mapping[ch.id] = { ref, index: b.index };
|
|
19792
|
+
else mapping[ch.id] = { ref };
|
|
19793
|
+
}
|
|
19794
|
+
for (const ch of req.mqttChannels) {
|
|
19795
|
+
const b = inputs.mqtt[ch.id];
|
|
19796
|
+
if (!b) throw new Error(`unbound mqtt channel ${ch.id}`);
|
|
19797
|
+
const conn = { type: "mqtt", brokerUrl: b.brokerUrl };
|
|
19798
|
+
if (b.username) conn.username = b.username;
|
|
19799
|
+
const ref = refs.alloc(`mqtt:${JSON.stringify(conn)}:${b.password ?? ""}`, `mqtt-${brokerHost(b.brokerUrl)}`);
|
|
19800
|
+
externalResources[ref] = conn;
|
|
19801
|
+
mapping[ch.id] = { ref };
|
|
19802
|
+
if (b.password) resourceSecrets[ref] = b.password;
|
|
19115
19803
|
}
|
|
19116
|
-
const
|
|
19117
|
-
|
|
19804
|
+
const llamaComponents = [];
|
|
19805
|
+
for (const m of req.customModels) {
|
|
19806
|
+
const b = inputs.models[m.id];
|
|
19807
|
+
if (!b) throw new Error(`unbound model ${m.id}`);
|
|
19808
|
+
const ref = refs.alloc(`model:${m.id}`, basename(m.id));
|
|
19809
|
+
mapping[m.id] = { ref };
|
|
19810
|
+
if (b.location === "device") {
|
|
19811
|
+
const port = b.port ?? 8080;
|
|
19812
|
+
const service = sidecarServiceName(m.id);
|
|
19813
|
+
externalResources[ref] = { type: "selfhosted", url: `http://${service}:${port}` };
|
|
19814
|
+
llamaComponents.push({
|
|
19815
|
+
name: service,
|
|
19816
|
+
image: meta3.llamaServerImage,
|
|
19817
|
+
command: [
|
|
19818
|
+
"--model",
|
|
19819
|
+
`${COMPONENT_WORKSPACE_PATH}/${b.modelFile}`,
|
|
19820
|
+
"--host",
|
|
19821
|
+
"0.0.0.0",
|
|
19822
|
+
"--port",
|
|
19823
|
+
String(port),
|
|
19824
|
+
"--ctx-size",
|
|
19825
|
+
String(b.ctxSize ?? 4096)
|
|
19826
|
+
],
|
|
19827
|
+
// The model lives in this sidecar's own workspace; read-only, so no chown
|
|
19828
|
+
// is needed (docs §5). The operator drops the GGUF in workspaceDir(service).
|
|
19829
|
+
volumes: [`${workspaceDir(service)}:${COMPONENT_WORKSPACE_PATH}:ro`]
|
|
19830
|
+
});
|
|
19831
|
+
} else {
|
|
19832
|
+
externalResources[ref] = { type: "selfhosted", url: b.url };
|
|
19833
|
+
if (b.apiKey) resourceSecrets[ref] = b.apiKey;
|
|
19834
|
+
}
|
|
19835
|
+
}
|
|
19836
|
+
const manifest = {};
|
|
19837
|
+
if (Object.keys(gpios).length) manifest.gpios = gpios;
|
|
19838
|
+
if (Object.keys(adcs).length) manifest.adcs = adcs;
|
|
19839
|
+
if (Object.keys(dacs).length) manifest.dacs = dacs;
|
|
19840
|
+
if (Object.keys(pwms).length) manifest.pwms = pwms;
|
|
19841
|
+
if (Object.keys(serials).length) manifest.serials = serials;
|
|
19842
|
+
const config2 = { workflow: serialize7(workflow) };
|
|
19843
|
+
if (Object.keys(mapping).length) config2.mapping = mapping;
|
|
19844
|
+
if (Object.keys(externalResources).length) config2.externalResources = externalResources;
|
|
19845
|
+
if (Object.keys(manifest).length) config2.manifest = manifest;
|
|
19846
|
+
const engine = {
|
|
19847
|
+
name: "engine",
|
|
19848
|
+
image: meta3.engineImage,
|
|
19849
|
+
pull: "never",
|
|
19850
|
+
// built locally before deploy, in no registry
|
|
19851
|
+
config: config2,
|
|
19852
|
+
// Durable memory: a host bind mount under the state root, read-write, at the
|
|
19853
|
+
// in-container workspace path (docs/device-filesystem.md §10).
|
|
19854
|
+
volumes: [`${workspaceDir("engine")}:${COMPONENT_WORKSPACE_PATH}`],
|
|
19855
|
+
// Run as root: writes the rw workspace bind mount without a host chown step
|
|
19856
|
+
// (the OSS default, §5), and also opens root-owned device nodes when hardware
|
|
19857
|
+
// (cdev passthrough / sysfs-privileged) is mapped in below.
|
|
19858
|
+
user: "0:0"
|
|
19859
|
+
};
|
|
19860
|
+
if (cdev.size > 0) engine.devices = [...cdev];
|
|
19861
|
+
if (privileged) engine.privileged = true;
|
|
19862
|
+
const components = [engine, ...llamaComponents, ...customComponents];
|
|
19863
|
+
assertNoNameCollisions(components);
|
|
19864
|
+
const spec = {
|
|
19865
|
+
schemaVersion: 1,
|
|
19866
|
+
id: meta3.id,
|
|
19867
|
+
components
|
|
19868
|
+
};
|
|
19869
|
+
if (meta3.createdAt) spec.createdAt = meta3.createdAt;
|
|
19870
|
+
return { spec, resourceSecrets };
|
|
19118
19871
|
}
|
|
19119
19872
|
|
|
19873
|
+
// cli/deploy/index.ts
|
|
19874
|
+
import { promises as fs9 } from "node:fs";
|
|
19875
|
+
import path12 from "node:path";
|
|
19876
|
+
import { parseArgs } from "node:util";
|
|
19877
|
+
|
|
19120
19878
|
// ../node_modules/@inquirer/core/dist/lib/key.js
|
|
19121
19879
|
var keybindings = ["emacs", "vim"];
|
|
19122
19880
|
var keybindingLookup = new Set(keybindings);
|
|
@@ -20520,7 +21278,7 @@ var PromisePolyfill = class extends Promise {
|
|
|
20520
21278
|
};
|
|
20521
21279
|
|
|
20522
21280
|
// ../node_modules/@inquirer/core/dist/lib/create-prompt.js
|
|
20523
|
-
import
|
|
21281
|
+
import path8 from "node:path";
|
|
20524
21282
|
var nativeSetImmediate = globalThis.setImmediate;
|
|
20525
21283
|
function getCallSites() {
|
|
20526
21284
|
const savedPrepareStackTrace = Error.prepareStackTrace;
|
|
@@ -20591,7 +21349,7 @@ function createPrompt(view) {
|
|
|
20591
21349
|
if (nextView === void 0) {
|
|
20592
21350
|
let callerFilename = callSites[1]?.getFileName();
|
|
20593
21351
|
if (callerFilename && !callerFilename.startsWith("file://")) {
|
|
20594
|
-
callerFilename =
|
|
21352
|
+
callerFilename = path8.resolve(callerFilename);
|
|
20595
21353
|
}
|
|
20596
21354
|
throw new Error(`Prompt functions must return a string.
|
|
20597
21355
|
at ${callerFilename}`);
|
|
@@ -20836,11 +21594,57 @@ var dist_default4 = createPrompt((config2, done) => {
|
|
|
20836
21594
|
return `${lines}${cursorHide}`;
|
|
20837
21595
|
});
|
|
20838
21596
|
|
|
21597
|
+
// ../node_modules/@inquirer/confirm/dist/index.js
|
|
21598
|
+
function getBooleanValue(value, defaultValue) {
|
|
21599
|
+
let answer = defaultValue !== false;
|
|
21600
|
+
if (/^(y|yes)/i.test(value))
|
|
21601
|
+
answer = true;
|
|
21602
|
+
else if (/^(n|no)/i.test(value))
|
|
21603
|
+
answer = false;
|
|
21604
|
+
return answer;
|
|
21605
|
+
}
|
|
21606
|
+
function boolToString(value) {
|
|
21607
|
+
return value ? "Yes" : "No";
|
|
21608
|
+
}
|
|
21609
|
+
var dist_default5 = createPrompt((config2, done) => {
|
|
21610
|
+
const { transformer = boolToString } = config2;
|
|
21611
|
+
const [status, setStatus] = useState("idle");
|
|
21612
|
+
const [value, setValue] = useState("");
|
|
21613
|
+
const theme = makeTheme(config2.theme);
|
|
21614
|
+
const prefix = usePrefix({ status, theme });
|
|
21615
|
+
useKeypress((key, rl) => {
|
|
21616
|
+
if (status !== "idle")
|
|
21617
|
+
return;
|
|
21618
|
+
if (isEnterKey(key)) {
|
|
21619
|
+
const answer = getBooleanValue(value, config2.default);
|
|
21620
|
+
setValue(transformer(answer));
|
|
21621
|
+
setStatus("done");
|
|
21622
|
+
done(answer);
|
|
21623
|
+
} else if (isTabKey(key)) {
|
|
21624
|
+
const answer = boolToString(!getBooleanValue(value, config2.default));
|
|
21625
|
+
rl.clearLine(0);
|
|
21626
|
+
rl.write(answer);
|
|
21627
|
+
setValue(answer);
|
|
21628
|
+
} else {
|
|
21629
|
+
setValue(rl.line);
|
|
21630
|
+
}
|
|
21631
|
+
});
|
|
21632
|
+
let formattedValue = value;
|
|
21633
|
+
let defaultValue = "";
|
|
21634
|
+
if (status === "done") {
|
|
21635
|
+
formattedValue = theme.style.answer(value);
|
|
21636
|
+
} else {
|
|
21637
|
+
defaultValue = ` ${theme.style.defaultAnswer(config2.default === false ? "y/N" : "Y/n")}`;
|
|
21638
|
+
}
|
|
21639
|
+
const message = theme.style.message(config2.message, status);
|
|
21640
|
+
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
|
|
21641
|
+
});
|
|
21642
|
+
|
|
20839
21643
|
// ../node_modules/@inquirer/input/dist/index.js
|
|
20840
21644
|
var inputTheme = {
|
|
20841
21645
|
validationFailureMode: "keep"
|
|
20842
21646
|
};
|
|
20843
|
-
var
|
|
21647
|
+
var dist_default6 = createPrompt((config2, done) => {
|
|
20844
21648
|
const { prefill = "tab" } = config2;
|
|
20845
21649
|
const theme = makeTheme(inputTheme, config2.theme);
|
|
20846
21650
|
const [status, setStatus] = useState("idle");
|
|
@@ -20927,7 +21731,7 @@ var passwordTheme = {
|
|
|
20927
21731
|
maskedText: "[input is masked]"
|
|
20928
21732
|
}
|
|
20929
21733
|
};
|
|
20930
|
-
var
|
|
21734
|
+
var dist_default7 = createPrompt((config2, done) => {
|
|
20931
21735
|
const { validate = () => true } = config2;
|
|
20932
21736
|
const theme = makeTheme(passwordTheme, config2.theme);
|
|
20933
21737
|
const [status, setStatus] = useState("idle");
|
|
@@ -21019,7 +21823,7 @@ function normalizeChoices2(choices) {
|
|
|
21019
21823
|
return normalizedChoice;
|
|
21020
21824
|
});
|
|
21021
21825
|
}
|
|
21022
|
-
var
|
|
21826
|
+
var dist_default8 = createPrompt((config2, done) => {
|
|
21023
21827
|
const { loop = true, pageSize = 7 } = config2;
|
|
21024
21828
|
const theme = makeTheme(selectTheme, config2.theme);
|
|
21025
21829
|
const { keybindings: keybindings2 } = theme;
|
|
@@ -21148,8 +21952,145 @@ var dist_default7 = createPrompt((config2, done) => {
|
|
|
21148
21952
|
});
|
|
21149
21953
|
|
|
21150
21954
|
// cli/deploy/prompts.ts
|
|
21955
|
+
import { existsSync as existsSync3, promises as fs7 } from "node:fs";
|
|
21956
|
+
import path10 from "node:path";
|
|
21957
|
+
|
|
21958
|
+
// cli/deploy/components.ts
|
|
21151
21959
|
import { existsSync as existsSync2, promises as fs6 } from "node:fs";
|
|
21152
|
-
import
|
|
21960
|
+
import path9 from "node:path";
|
|
21961
|
+
var import_ajv2 = __toESM(require_ajv(), 1);
|
|
21962
|
+
var import_ajv_formats2 = __toESM(require_dist(), 1);
|
|
21963
|
+
async function readComponentJson(dir) {
|
|
21964
|
+
const file2 = path9.join(dir, "component.json");
|
|
21965
|
+
let raw;
|
|
21966
|
+
try {
|
|
21967
|
+
raw = await fs6.readFile(file2, "utf-8");
|
|
21968
|
+
} catch (err) {
|
|
21969
|
+
if (err.code === "ENOENT") {
|
|
21970
|
+
throw new Error(`custom component: no component.json in ${dir}`, { cause: err });
|
|
21971
|
+
}
|
|
21972
|
+
throw err;
|
|
21973
|
+
}
|
|
21974
|
+
try {
|
|
21975
|
+
return JSON.parse(raw);
|
|
21976
|
+
} catch (err) {
|
|
21977
|
+
throw new Error(`${file2}: invalid JSON: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
21978
|
+
}
|
|
21979
|
+
}
|
|
21980
|
+
var cachedValidator2;
|
|
21981
|
+
function compileDeployComponentValidator() {
|
|
21982
|
+
if (cachedValidator2) return cachedValidator2;
|
|
21983
|
+
const deployment = openApiToJsonSchema(loadContractDocument("deployment.yaml"));
|
|
21984
|
+
deployment.$id = "deployment.yaml";
|
|
21985
|
+
const schemas = deployment.components.schemas;
|
|
21986
|
+
schemas.DeployComponent.additionalProperties = false;
|
|
21987
|
+
const ajv = new import_ajv2.default({ strict: false, allErrors: true });
|
|
21988
|
+
(0, import_ajv_formats2.default)(ajv);
|
|
21989
|
+
ajv.addSchema(deployment);
|
|
21990
|
+
cachedValidator2 = ajv.compile({ $ref: "deployment.yaml#/components/schemas/DeployComponent" });
|
|
21991
|
+
return cachedValidator2;
|
|
21992
|
+
}
|
|
21993
|
+
function validateComponent(data) {
|
|
21994
|
+
const validate = compileDeployComponentValidator();
|
|
21995
|
+
if (validate(data)) return [];
|
|
21996
|
+
return (validate.errors ?? []).map((e) => {
|
|
21997
|
+
const p = e.params;
|
|
21998
|
+
const which = p.additionalProperty ?? p.missingProperty;
|
|
21999
|
+
const detail = typeof which === "string" ? ` "${which}"` : "";
|
|
22000
|
+
return `${e.instancePath || "/"} ${e.message ?? "invalid"}${detail}`;
|
|
22001
|
+
});
|
|
22002
|
+
}
|
|
22003
|
+
function parseDeployComponents(raw) {
|
|
22004
|
+
const errors = [];
|
|
22005
|
+
for (const { source, data } of raw) {
|
|
22006
|
+
for (const message of validateComponent(data)) errors.push(`${source}: ${message}`);
|
|
22007
|
+
}
|
|
22008
|
+
if (errors.length > 0) {
|
|
22009
|
+
throw new Error(`invalid custom component(s):
|
|
22010
|
+
- ${errors.join("\n - ")}`);
|
|
22011
|
+
}
|
|
22012
|
+
return raw.map((r) => r.data);
|
|
22013
|
+
}
|
|
22014
|
+
function parseEnvExample(text) {
|
|
22015
|
+
return text.split("\n").map((line) => {
|
|
22016
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
22017
|
+
return m ? { kind: "kv", key: m[1] ?? "", value: m[2] ?? "" } : { kind: "comment", text: line };
|
|
22018
|
+
});
|
|
22019
|
+
}
|
|
22020
|
+
var SECRET_HINT = /password|secret|key|token/i;
|
|
22021
|
+
async function resolveComponentEnv(dir, name, opts) {
|
|
22022
|
+
const file2 = path9.join(dir, `${name}.env.example`);
|
|
22023
|
+
let raw;
|
|
22024
|
+
try {
|
|
22025
|
+
raw = await fs6.readFile(file2, "utf-8");
|
|
22026
|
+
} catch (err) {
|
|
22027
|
+
if (err.code === "ENOENT") return null;
|
|
22028
|
+
throw err;
|
|
22029
|
+
}
|
|
22030
|
+
const lines = [];
|
|
22031
|
+
for (const e of parseEnvExample(raw)) {
|
|
22032
|
+
if (e.kind === "comment") {
|
|
22033
|
+
lines.push(e.text);
|
|
22034
|
+
} else if (e.value !== "" || !opts.interactive) {
|
|
22035
|
+
lines.push(`${e.key}=${e.value}`);
|
|
22036
|
+
} else {
|
|
22037
|
+
const value = SECRET_HINT.test(e.key) ? await dist_default7({ message: `${name}: ${e.key}`, mask: "*" }) : await dist_default6({ message: `${name}: ${e.key}` });
|
|
22038
|
+
lines.push(`${e.key}=${value}`);
|
|
22039
|
+
}
|
|
22040
|
+
}
|
|
22041
|
+
const header = `# Auto-generated by \`fh-workflow deploy\` from ${name}.env.example.
|
|
22042
|
+
# Loaded into the ${name} container via compose env_file; chmod 600 if it holds
|
|
22043
|
+
# secrets, and fill any empty values before \`up\`.
|
|
22044
|
+
`;
|
|
22045
|
+
return `${header}
|
|
22046
|
+
${lines.join("\n")}
|
|
22047
|
+
`;
|
|
22048
|
+
}
|
|
22049
|
+
async function promptCustomComponents(preloaded) {
|
|
22050
|
+
const components = [];
|
|
22051
|
+
const env = {};
|
|
22052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22053
|
+
const add = async (loaded) => {
|
|
22054
|
+
seen.add(loaded.component.name);
|
|
22055
|
+
components.push(loaded.component);
|
|
22056
|
+
const text = await resolveComponentEnv(loaded.dir, loaded.component.name, { interactive: true });
|
|
22057
|
+
if (text !== null) env[loaded.component.name] = text;
|
|
22058
|
+
};
|
|
22059
|
+
if (preloaded.length > 0) {
|
|
22060
|
+
process.stdout.write(` Already added: ${preloaded.map((c) => c.component.name).join(", ")}
|
|
22061
|
+
`);
|
|
22062
|
+
}
|
|
22063
|
+
for (const loaded of preloaded) await add(loaded);
|
|
22064
|
+
while (await dist_default5({
|
|
22065
|
+
message: components.length === 0 ? "Add a custom component?" : "Add another custom component?",
|
|
22066
|
+
default: false
|
|
22067
|
+
})) {
|
|
22068
|
+
const dir = (await dist_default6({
|
|
22069
|
+
message: "Custom component folder",
|
|
22070
|
+
// Full inline validation: path, component.json presence, contract shape,
|
|
22071
|
+
// and a name not already taken — the same checks the batch path runs, so
|
|
22072
|
+
// a bad folder is rejected here instead of failing late in the build.
|
|
22073
|
+
validate: async (v) => {
|
|
22074
|
+
const t = v.trim();
|
|
22075
|
+
if (!t) return "enter a folder path";
|
|
22076
|
+
if (!existsSync2(t)) return `folder not found: ${t}`;
|
|
22077
|
+
if (!existsSync2(path9.join(t, "component.json"))) return `no component.json in ${t}`;
|
|
22078
|
+
let data;
|
|
22079
|
+
try {
|
|
22080
|
+
data = await readComponentJson(t);
|
|
22081
|
+
} catch (err) {
|
|
22082
|
+
return err instanceof Error ? err.message : String(err);
|
|
22083
|
+
}
|
|
22084
|
+
const errors = validateComponent(data);
|
|
22085
|
+
if (errors.length > 0) return errors.join("; ");
|
|
22086
|
+
const name = data.name;
|
|
22087
|
+
return seen.has(name) ? `"${name}" already added` : true;
|
|
22088
|
+
}
|
|
22089
|
+
})).trim();
|
|
22090
|
+
await add({ component: await readComponentJson(dir), dir });
|
|
22091
|
+
}
|
|
22092
|
+
return { components, env };
|
|
22093
|
+
}
|
|
21153
22094
|
|
|
21154
22095
|
// ../node_modules/zod/v4/classic/external.js
|
|
21155
22096
|
var external_exports = {};
|
|
@@ -21761,7 +22702,7 @@ __export(util_exports, {
|
|
|
21761
22702
|
assert: () => assert,
|
|
21762
22703
|
assertEqual: () => assertEqual,
|
|
21763
22704
|
assertIs: () => assertIs,
|
|
21764
|
-
assertNever: () =>
|
|
22705
|
+
assertNever: () => assertNever,
|
|
21765
22706
|
assertNotEqual: () => assertNotEqual,
|
|
21766
22707
|
assignProp: () => assignProp,
|
|
21767
22708
|
base64ToUint8Array: () => base64ToUint8Array,
|
|
@@ -21825,7 +22766,7 @@ function assertNotEqual(val) {
|
|
|
21825
22766
|
}
|
|
21826
22767
|
function assertIs(_arg) {
|
|
21827
22768
|
}
|
|
21828
|
-
function
|
|
22769
|
+
function assertNever(_x) {
|
|
21829
22770
|
throw new Error("Unexpected value in exhaustive check");
|
|
21830
22771
|
}
|
|
21831
22772
|
function assert(_) {
|
|
@@ -21917,10 +22858,10 @@ function mergeDefs(...defs) {
|
|
|
21917
22858
|
function cloneDef(schema) {
|
|
21918
22859
|
return mergeDefs(schema._zod.def);
|
|
21919
22860
|
}
|
|
21920
|
-
function getElementAtPath(obj,
|
|
21921
|
-
if (!
|
|
22861
|
+
function getElementAtPath(obj, path13) {
|
|
22862
|
+
if (!path13)
|
|
21922
22863
|
return obj;
|
|
21923
|
-
return
|
|
22864
|
+
return path13.reduce((acc, key) => acc?.[key], obj);
|
|
21924
22865
|
}
|
|
21925
22866
|
function promiseAllObject(promisesObj) {
|
|
21926
22867
|
const keys = Object.keys(promisesObj);
|
|
@@ -22329,11 +23270,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
22329
23270
|
}
|
|
22330
23271
|
return false;
|
|
22331
23272
|
}
|
|
22332
|
-
function prefixIssues(
|
|
23273
|
+
function prefixIssues(path13, issues) {
|
|
22333
23274
|
return issues.map((iss) => {
|
|
22334
23275
|
var _a3;
|
|
22335
23276
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
22336
|
-
iss.path.unshift(
|
|
23277
|
+
iss.path.unshift(path13);
|
|
22337
23278
|
return iss;
|
|
22338
23279
|
});
|
|
22339
23280
|
}
|
|
@@ -22480,16 +23421,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22480
23421
|
}
|
|
22481
23422
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
22482
23423
|
const fieldErrors = { _errors: [] };
|
|
22483
|
-
const processError = (error52,
|
|
23424
|
+
const processError = (error52, path13 = []) => {
|
|
22484
23425
|
for (const issue2 of error52.issues) {
|
|
22485
23426
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
22486
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
23427
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
22487
23428
|
} else if (issue2.code === "invalid_key") {
|
|
22488
|
-
processError({ issues: issue2.issues }, [...
|
|
23429
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22489
23430
|
} else if (issue2.code === "invalid_element") {
|
|
22490
|
-
processError({ issues: issue2.issues }, [...
|
|
23431
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22491
23432
|
} else {
|
|
22492
|
-
const fullpath = [...
|
|
23433
|
+
const fullpath = [...path13, ...issue2.path];
|
|
22493
23434
|
if (fullpath.length === 0) {
|
|
22494
23435
|
fieldErrors._errors.push(mapper(issue2));
|
|
22495
23436
|
} else {
|
|
@@ -22516,17 +23457,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22516
23457
|
}
|
|
22517
23458
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
22518
23459
|
const result = { errors: [] };
|
|
22519
|
-
const processError = (error52,
|
|
23460
|
+
const processError = (error52, path13 = []) => {
|
|
22520
23461
|
var _a3, _b;
|
|
22521
23462
|
for (const issue2 of error52.issues) {
|
|
22522
23463
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
22523
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
23464
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
22524
23465
|
} else if (issue2.code === "invalid_key") {
|
|
22525
|
-
processError({ issues: issue2.issues }, [...
|
|
23466
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22526
23467
|
} else if (issue2.code === "invalid_element") {
|
|
22527
|
-
processError({ issues: issue2.issues }, [...
|
|
23468
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22528
23469
|
} else {
|
|
22529
|
-
const fullpath = [...
|
|
23470
|
+
const fullpath = [...path13, ...issue2.path];
|
|
22530
23471
|
if (fullpath.length === 0) {
|
|
22531
23472
|
result.errors.push(mapper(issue2));
|
|
22532
23473
|
continue;
|
|
@@ -22558,8 +23499,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22558
23499
|
}
|
|
22559
23500
|
function toDotPath(_path) {
|
|
22560
23501
|
const segs = [];
|
|
22561
|
-
const
|
|
22562
|
-
for (const seg of
|
|
23502
|
+
const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
23503
|
+
for (const seg of path13) {
|
|
22563
23504
|
if (typeof seg === "number")
|
|
22564
23505
|
segs.push(`[${seg}]`);
|
|
22565
23506
|
else if (typeof seg === "symbol")
|
|
@@ -35251,13 +36192,13 @@ function resolveRef(ref, ctx) {
|
|
|
35251
36192
|
if (!ref.startsWith("#")) {
|
|
35252
36193
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
35253
36194
|
}
|
|
35254
|
-
const
|
|
35255
|
-
if (
|
|
36195
|
+
const path13 = ref.slice(1).split("/").filter(Boolean);
|
|
36196
|
+
if (path13.length === 0) {
|
|
35256
36197
|
return ctx.rootSchema;
|
|
35257
36198
|
}
|
|
35258
36199
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
35259
|
-
if (
|
|
35260
|
-
const key =
|
|
36200
|
+
if (path13[0] === defsKey) {
|
|
36201
|
+
const key = path13[1];
|
|
35261
36202
|
if (!key || !ctx.defs[key]) {
|
|
35262
36203
|
throw new Error(`Reference not found: ${ref}`);
|
|
35263
36204
|
}
|
|
@@ -35679,55 +36620,18 @@ var mqttBindingSchema = external_exports.strictObject({
|
|
|
35679
36620
|
password: external_exports.string().optional()
|
|
35680
36621
|
});
|
|
35681
36622
|
var modelBindingSchema = external_exports.discriminatedUnion("location", [
|
|
35682
|
-
external_exports.strictObject({
|
|
36623
|
+
external_exports.strictObject({
|
|
36624
|
+
location: external_exports.literal("device"),
|
|
36625
|
+
modelFile: external_exports.string(),
|
|
36626
|
+
port: external_exports.number().int().positive().optional(),
|
|
36627
|
+
ctxSize: external_exports.number().int().positive().optional()
|
|
36628
|
+
}),
|
|
35683
36629
|
external_exports.strictObject({ location: external_exports.literal("network"), url: external_exports.string(), apiKey: external_exports.string().optional() })
|
|
35684
36630
|
]);
|
|
35685
|
-
|
|
35686
|
-
|
|
35687
|
-
|
|
35688
|
-
|
|
35689
|
-
if (t.includes("/")) return "just the filename, not a path \u2014 the file goes in ./models/";
|
|
35690
|
-
return null;
|
|
35691
|
-
}
|
|
35692
|
-
function hardwareAddressKey(family, chipOrDevice, index) {
|
|
35693
|
-
const dev = chipOrDevice.trim();
|
|
35694
|
-
return family === "serial" ? `serial:${dev}` : `${family}:${dev}:${index}`;
|
|
35695
|
-
}
|
|
35696
|
-
function hardwareAddressLabel(family, chipOrDevice, index) {
|
|
35697
|
-
const dev = chipOrDevice.trim();
|
|
35698
|
-
if (family === "serial") return dev;
|
|
35699
|
-
return `${dev} ${family === "gpio" ? "line" : "channel"} ${index}`;
|
|
35700
|
-
}
|
|
35701
|
-
function hardwareConflicts(channels, bindings) {
|
|
35702
|
-
const conflicts = [];
|
|
35703
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
35704
|
-
for (const ch of channels) {
|
|
35705
|
-
const b = bindings[ch.id];
|
|
35706
|
-
if (!b?.chipOrDevice || ch.addressable && b.index === void 0) continue;
|
|
35707
|
-
const key = hardwareAddressKey(ch.family, b.chipOrDevice, b.index);
|
|
35708
|
-
const holder = claimed.get(key);
|
|
35709
|
-
if (holder) {
|
|
35710
|
-
conflicts.push(`hardware "${ch.id}": ${hardwareAddressLabel(ch.family, b.chipOrDevice, b.index)} is already used by "${holder}"`);
|
|
35711
|
-
} else {
|
|
35712
|
-
claimed.set(key, ch.id);
|
|
35713
|
-
}
|
|
35714
|
-
}
|
|
35715
|
-
return conflicts;
|
|
35716
|
-
}
|
|
35717
|
-
function familyMismatches(channels, bindings) {
|
|
35718
|
-
const mismatches = [];
|
|
35719
|
-
for (const ch of channels) {
|
|
35720
|
-
const b = bindings[ch.id];
|
|
35721
|
-
if (!b) continue;
|
|
35722
|
-
if (ch.family !== "serial" && b.baud !== void 0) {
|
|
35723
|
-
mismatches.push(`hardware "${ch.id}": "baud" only applies to serial channels (this is a ${ch.family} channel)`);
|
|
35724
|
-
}
|
|
35725
|
-
if (ch.family === "serial" && b.index !== void 0) {
|
|
35726
|
-
mismatches.push(`hardware "${ch.id}": "index" does not apply to serial channels (the device path is the full address)`);
|
|
35727
|
-
}
|
|
35728
|
-
}
|
|
35729
|
-
return mismatches;
|
|
35730
|
-
}
|
|
36631
|
+
var webSearchBindingSchema = external_exports.strictObject({
|
|
36632
|
+
provider: external_exports.string(),
|
|
36633
|
+
apiKey: external_exports.string()
|
|
36634
|
+
});
|
|
35731
36635
|
function unknownIds(req, p) {
|
|
35732
36636
|
const unknown2 = [];
|
|
35733
36637
|
const check3 = (kind, ids, bindings) => {
|
|
@@ -35741,10 +36645,6 @@ function unknownIds(req, p) {
|
|
|
35741
36645
|
check3("model", req.customModels.map((m) => m.id), p.models);
|
|
35742
36646
|
return unknown2;
|
|
35743
36647
|
}
|
|
35744
|
-
var webSearchBindingSchema = external_exports.strictObject({
|
|
35745
|
-
provider: external_exports.string(),
|
|
35746
|
-
apiKey: external_exports.string()
|
|
35747
|
-
});
|
|
35748
36648
|
var deployConfigSchema = external_exports.strictObject({
|
|
35749
36649
|
llmKeys: external_exports.partialRecord(external_exports.enum(ALL_PROVIDERS), external_exports.string()),
|
|
35750
36650
|
outputDir: external_exports.string(),
|
|
@@ -35780,7 +36680,7 @@ async function promptHardware(channels, seed) {
|
|
|
35780
36680
|
}
|
|
35781
36681
|
for (const ch of channels) {
|
|
35782
36682
|
if (result[ch.id]) continue;
|
|
35783
|
-
const chipOrDevice = await
|
|
36683
|
+
const chipOrDevice = await dist_default6({
|
|
35784
36684
|
message: `${ch.label} (${ch.family}): device path`,
|
|
35785
36685
|
default: HARDWARE_EXAMPLE[ch.family],
|
|
35786
36686
|
// Only serial claims the whole device; gpio/adc/dac/pwm share the path
|
|
@@ -35794,7 +36694,7 @@ async function promptHardware(channels, seed) {
|
|
|
35794
36694
|
const dev = chipOrDevice.trim();
|
|
35795
36695
|
const binding = { chipOrDevice: dev };
|
|
35796
36696
|
if (ch.addressable) {
|
|
35797
|
-
const idx = await
|
|
36697
|
+
const idx = await dist_default6({
|
|
35798
36698
|
message: `${ch.label}: channel index / GPIO line`,
|
|
35799
36699
|
validate: (v) => {
|
|
35800
36700
|
const uint = isUint(v);
|
|
@@ -35806,7 +36706,7 @@ async function promptHardware(channels, seed) {
|
|
|
35806
36706
|
binding.index = Number(idx.trim());
|
|
35807
36707
|
}
|
|
35808
36708
|
if (ch.family === "serial") {
|
|
35809
|
-
const baud = await
|
|
36709
|
+
const baud = await dist_default6({ message: `${ch.label}: baud rate`, default: "115200", validate: isUint });
|
|
35810
36710
|
binding.baud = Number(baud.trim());
|
|
35811
36711
|
}
|
|
35812
36712
|
result[ch.id] = binding;
|
|
@@ -35821,7 +36721,7 @@ async function promptMqtt(channels, seed) {
|
|
|
35821
36721
|
const brokers = /* @__PURE__ */ new Map();
|
|
35822
36722
|
for (const b of Object.values(result)) brokers.set(b.brokerUrl, b);
|
|
35823
36723
|
if (brokers.size > 0) {
|
|
35824
|
-
const choice = await
|
|
36724
|
+
const choice = await dist_default8({
|
|
35825
36725
|
message: `${ch.label}: MQTT broker`,
|
|
35826
36726
|
choices: [
|
|
35827
36727
|
...[...brokers.keys()].map((url2) => ({ value: url2, name: `reuse ${url2}` })),
|
|
@@ -35834,13 +36734,13 @@ async function promptMqtt(channels, seed) {
|
|
|
35834
36734
|
continue;
|
|
35835
36735
|
}
|
|
35836
36736
|
}
|
|
35837
|
-
const brokerUrl = await
|
|
36737
|
+
const brokerUrl = await dist_default6({
|
|
35838
36738
|
message: `${ch.label}: broker URL`,
|
|
35839
36739
|
default: "tcp://localhost:1883",
|
|
35840
36740
|
validate: (v) => v.trim().length > 0 || "broker URL is required"
|
|
35841
36741
|
});
|
|
35842
|
-
const username = (await
|
|
35843
|
-
const pass = await
|
|
36742
|
+
const username = (await dist_default6({ message: `${ch.label}: username (optional)` })).trim();
|
|
36743
|
+
const pass = await dist_default7({ message: `${ch.label}: password (optional)`, mask: "*" });
|
|
35844
36744
|
const binding = { brokerUrl: brokerUrl.trim() };
|
|
35845
36745
|
if (username) binding.username = username;
|
|
35846
36746
|
if (pass) binding.password = pass;
|
|
@@ -35852,7 +36752,7 @@ async function promptModels(models, seed) {
|
|
|
35852
36752
|
const result = { ...seed };
|
|
35853
36753
|
for (const m of models) {
|
|
35854
36754
|
if (result[m.id]) continue;
|
|
35855
|
-
const location = await
|
|
36755
|
+
const location = await dist_default8({
|
|
35856
36756
|
message: `${m.label}: where does this model run?`,
|
|
35857
36757
|
choices: [
|
|
35858
36758
|
{ value: "device", name: "on this device (generate a llama-server container)" },
|
|
@@ -35860,19 +36760,26 @@ async function promptModels(models, seed) {
|
|
|
35860
36760
|
]
|
|
35861
36761
|
});
|
|
35862
36762
|
if (location === "device") {
|
|
35863
|
-
const modelFile = await
|
|
35864
|
-
message: `${m.label}: model filename in
|
|
36763
|
+
const modelFile = await dist_default6({
|
|
36764
|
+
message: `${m.label}: model filename, dropped in its workspace dir (e.g. model.gguf)`,
|
|
35865
36765
|
validate: (v) => ggufNameError(v) ?? true
|
|
35866
36766
|
});
|
|
35867
|
-
|
|
36767
|
+
const ctxSize = await dist_default6({ message: `${m.label}: context window in tokens`, default: "4096", validate: isUint });
|
|
36768
|
+
const port = await dist_default6({ message: `${m.label}: sidecar port`, default: "8080", validate: isUint });
|
|
36769
|
+
result[m.id] = {
|
|
36770
|
+
location: "device",
|
|
36771
|
+
modelFile: modelFile.trim(),
|
|
36772
|
+
ctxSize: Number(ctxSize.trim()),
|
|
36773
|
+
port: Number(port.trim())
|
|
36774
|
+
};
|
|
35868
36775
|
continue;
|
|
35869
36776
|
}
|
|
35870
|
-
const url2 = await
|
|
36777
|
+
const url2 = await dist_default6({
|
|
35871
36778
|
message: `${m.label}: inference endpoint URL (a server you run \u2014 llama-server/vLLM/Ollama)`,
|
|
35872
36779
|
default: "http://localhost:8080",
|
|
35873
36780
|
validate: (v) => v.trim().length > 0 || "endpoint URL is required"
|
|
35874
36781
|
});
|
|
35875
|
-
const apiKey = await
|
|
36782
|
+
const apiKey = await dist_default7({ message: `${m.label}: API key (optional)`, mask: "*" });
|
|
35876
36783
|
const binding = { location: "network", url: url2.trim() };
|
|
35877
36784
|
if (apiKey) binding.apiKey = apiKey;
|
|
35878
36785
|
result[m.id] = binding;
|
|
@@ -35881,11 +36788,11 @@ async function promptModels(models, seed) {
|
|
|
35881
36788
|
}
|
|
35882
36789
|
async function promptWebSearch(seed) {
|
|
35883
36790
|
if (seed) return seed;
|
|
35884
|
-
const provider = (await
|
|
35885
|
-
const apiKey = await
|
|
36791
|
+
const provider = (await dist_default6({ message: "Web search provider", default: "brave" })).trim() || "brave";
|
|
36792
|
+
const apiKey = await dist_default7({ message: "Web search API key", mask: "*" });
|
|
35886
36793
|
return { provider, apiKey };
|
|
35887
36794
|
}
|
|
35888
|
-
async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
36795
|
+
async function promptMissing(partial2, outputDirDefault, req, workflowName, preloadedComponents) {
|
|
35889
36796
|
const hwTodo = req.hardwareChannels.filter((ch) => !partial2.hardware?.[ch.id]);
|
|
35890
36797
|
const mqttTodo = req.mqttChannels.filter((ch) => !partial2.mqtt?.[ch.id]);
|
|
35891
36798
|
const modelsTodo = req.customModels.filter((m) => !partial2.models?.[m.id]);
|
|
@@ -35893,13 +36800,11 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35893
36800
|
const askWeb = req.hasWebSearch && !partial2.webSearch;
|
|
35894
36801
|
let askOut = true;
|
|
35895
36802
|
if (partial2.outputDir) {
|
|
35896
|
-
const resolved =
|
|
35897
|
-
const nonEmpty =
|
|
36803
|
+
const resolved = path10.resolve(process.cwd(), partial2.outputDir);
|
|
36804
|
+
const nonEmpty = existsSync3(resolved) && (await fs7.readdir(resolved)).length > 0;
|
|
35898
36805
|
askOut = nonEmpty && !(partial2.force ?? false);
|
|
35899
36806
|
}
|
|
35900
|
-
const total = [askKeys, hwTodo.length > 0, mqttTodo.length > 0, modelsTodo.length > 0, askWeb, askOut].filter(
|
|
35901
|
-
Boolean
|
|
35902
|
-
).length;
|
|
36807
|
+
const total = [askKeys, hwTodo.length > 0, mqttTodo.length > 0, modelsTodo.length > 0, askWeb, askOut].filter(Boolean).length + 1;
|
|
35903
36808
|
let step = 0;
|
|
35904
36809
|
const section = (label, count = 0) => {
|
|
35905
36810
|
step += 1;
|
|
@@ -35925,7 +36830,7 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35925
36830
|
}
|
|
35926
36831
|
for (const provider of selectedProviders) {
|
|
35927
36832
|
if (llmKeys[provider]) continue;
|
|
35928
|
-
const key = await
|
|
36833
|
+
const key = await dist_default7({ message: `${provider} API key`, mask: "*" });
|
|
35929
36834
|
if (key) llmKeys[provider] = key;
|
|
35930
36835
|
}
|
|
35931
36836
|
}
|
|
@@ -35937,6 +36842,8 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35937
36842
|
const models = await promptModels(req.customModels, partial2.models ?? {});
|
|
35938
36843
|
if (askWeb) section("Web search");
|
|
35939
36844
|
const webSearch = req.hasWebSearch ? await promptWebSearch(partial2.webSearch) : void 0;
|
|
36845
|
+
section("Custom components");
|
|
36846
|
+
const { components: customComponents, env: componentEnv } = await promptCustomComponents(preloadedComponents);
|
|
35940
36847
|
if (askOut) section("Output");
|
|
35941
36848
|
let outputDir;
|
|
35942
36849
|
let force = partial2.force ?? false;
|
|
@@ -35944,16 +36851,16 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35944
36851
|
let candidate = partial2.outputDir;
|
|
35945
36852
|
while (true) {
|
|
35946
36853
|
if (!candidate) {
|
|
35947
|
-
candidate = await
|
|
36854
|
+
candidate = await dist_default6({ message: "Output directory", default: outputDirDefault });
|
|
35948
36855
|
}
|
|
35949
|
-
const resolved =
|
|
35950
|
-
const exists =
|
|
35951
|
-
const nonEmpty = exists && (await
|
|
36856
|
+
const resolved = path10.resolve(process.cwd(), candidate);
|
|
36857
|
+
const exists = existsSync3(resolved);
|
|
36858
|
+
const nonEmpty = exists && (await fs7.readdir(resolved)).length > 0;
|
|
35952
36859
|
if (!nonEmpty || force) {
|
|
35953
36860
|
outputDir = candidate;
|
|
35954
36861
|
break;
|
|
35955
36862
|
}
|
|
35956
|
-
const action = await
|
|
36863
|
+
const action = await dist_default8({
|
|
35957
36864
|
message: `${resolved} is not empty.`,
|
|
35958
36865
|
choices: [
|
|
35959
36866
|
{ value: "overwrite", name: "overwrite the existing files" },
|
|
@@ -35970,7 +36877,7 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35970
36877
|
candidate = void 0;
|
|
35971
36878
|
}
|
|
35972
36879
|
}
|
|
35973
|
-
|
|
36880
|
+
const config2 = {
|
|
35974
36881
|
llmKeys,
|
|
35975
36882
|
outputDir,
|
|
35976
36883
|
force,
|
|
@@ -35980,229 +36887,86 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35980
36887
|
models,
|
|
35981
36888
|
webSearch
|
|
35982
36889
|
};
|
|
36890
|
+
return { config: config2, customComponents, componentEnv };
|
|
35983
36891
|
}
|
|
35984
36892
|
|
|
35985
36893
|
// cli/deploy/write.ts
|
|
35986
|
-
import { existsSync as
|
|
35987
|
-
import
|
|
36894
|
+
import { existsSync as existsSync4, promises as fs8 } from "node:fs";
|
|
36895
|
+
import path11 from "node:path";
|
|
35988
36896
|
|
|
35989
|
-
// cli/deploy/
|
|
35990
|
-
|
|
35991
|
-
|
|
35992
|
-
|
|
35993
|
-
|
|
35994
|
-
|
|
35995
|
-
|
|
35996
|
-
|
|
35997
|
-
|
|
35998
|
-
|
|
35999
|
-
|
|
36000
|
-
|
|
36001
|
-
|
|
36002
|
-
}
|
|
36003
|
-
function
|
|
36004
|
-
const
|
|
36005
|
-
|
|
36006
|
-
|
|
36007
|
-
|
|
36008
|
-
|
|
36009
|
-
|
|
36010
|
-
}
|
|
36011
|
-
|
|
36012
|
-
|
|
36013
|
-
}
|
|
36014
|
-
|
|
36015
|
-
|
|
36016
|
-
|
|
36017
|
-
|
|
36018
|
-
|
|
36019
|
-
|
|
36020
|
-
}
|
|
36021
|
-
|
|
36022
|
-
const
|
|
36023
|
-
|
|
36024
|
-
|
|
36025
|
-
|
|
36026
|
-
|
|
36027
|
-
|
|
36028
|
-
|
|
36029
|
-
|
|
36030
|
-
}
|
|
36031
|
-
|
|
36032
|
-
|
|
36033
|
-
|
|
36034
|
-
|
|
36035
|
-
|
|
36036
|
-
|
|
36037
|
-
|
|
36038
|
-
|
|
36039
|
-
|
|
36040
|
-
|
|
36041
|
-
|
|
36042
|
-
|
|
36043
|
-
|
|
36044
|
-
|
|
36045
|
-
- ${missing.join("\n - ")}`);
|
|
36046
|
-
}
|
|
36047
|
-
}
|
|
36048
|
-
function buildDeployArtifacts(req, cfg) {
|
|
36049
|
-
assertDeployable(req, cfg);
|
|
36050
|
-
const refs = new RefAllocator();
|
|
36051
|
-
const gpios = {};
|
|
36052
|
-
const adcs = {};
|
|
36053
|
-
const dacs = {};
|
|
36054
|
-
const pwms = {};
|
|
36055
|
-
const serials = {};
|
|
36056
|
-
const externalResources = {};
|
|
36057
|
-
const deploymentMapping = {};
|
|
36058
|
-
for (const ch of req.hardwareChannels) {
|
|
36059
|
-
const b = cfg.hardware[ch.id];
|
|
36060
|
-
if (!b) throw new Error(`unbound hardware channel ${ch.id}`);
|
|
36061
|
-
const dev = b.chipOrDevice;
|
|
36062
|
-
const ref = refs.alloc(`hw:${ch.family}:${dev}`, basename(dev));
|
|
36063
|
-
switch (ch.family) {
|
|
36064
|
-
case "gpio":
|
|
36065
|
-
gpios[ref] = { chip: dev };
|
|
36066
|
-
break;
|
|
36067
|
-
case "pwm":
|
|
36068
|
-
pwms[ref] = { chip: dev };
|
|
36069
|
-
break;
|
|
36070
|
-
case "adc":
|
|
36071
|
-
adcs[ref] = { device: dev };
|
|
36072
|
-
break;
|
|
36073
|
-
case "dac":
|
|
36074
|
-
dacs[ref] = { device: dev };
|
|
36075
|
-
break;
|
|
36076
|
-
case "serial":
|
|
36077
|
-
serials[ref] = b.baud ? { device: dev, baud: b.baud } : { device: dev };
|
|
36078
|
-
break;
|
|
36079
|
-
default:
|
|
36080
|
-
return assertNeverFamily(ch.family);
|
|
36081
|
-
}
|
|
36082
|
-
deploymentMapping[ch.id] = ch.addressable ? { ref, index: b.index } : { ref };
|
|
36083
|
-
}
|
|
36084
|
-
for (const ch of req.mqttChannels) {
|
|
36085
|
-
const b = cfg.mqtt[ch.id];
|
|
36086
|
-
if (!b) throw new Error(`unbound mqtt channel ${ch.id}`);
|
|
36087
|
-
const conn = { type: "mqtt", brokerUrl: b.brokerUrl };
|
|
36088
|
-
if (b.username) conn.username = b.username;
|
|
36089
|
-
if (b.password) conn.password = b.password;
|
|
36090
|
-
const ref = refs.alloc(`mqtt:${JSON.stringify(conn)}`, `mqtt-${brokerHost(b.brokerUrl)}`);
|
|
36091
|
-
externalResources[ref] = conn;
|
|
36092
|
-
deploymentMapping[ch.id] = { ref };
|
|
36093
|
-
}
|
|
36094
|
-
for (const m of req.customModels) {
|
|
36095
|
-
const b = cfg.models[m.id];
|
|
36096
|
-
if (!b) throw new Error(`unbound model ${m.id}`);
|
|
36097
|
-
const url2 = b.location === "device" ? `http://${sidecarServiceName(m.id)}:8080` : b.url;
|
|
36098
|
-
const provider = { type: "selfhosted", url: url2 };
|
|
36099
|
-
if (b.location === "network" && b.apiKey) provider.apiKey = b.apiKey;
|
|
36100
|
-
const ref = refs.alloc(`model:${m.id}`, basename(m.id));
|
|
36101
|
-
externalResources[ref] = provider;
|
|
36102
|
-
deploymentMapping[m.id] = { ref };
|
|
36897
|
+
// cli/deploy/generate.ts
|
|
36898
|
+
import { createHash } from "node:crypto";
|
|
36899
|
+
var CONFIG_PATH = "/etc/foresthub/config.json";
|
|
36900
|
+
var SECRETS_PATH = "/etc/foresthub/secrets.json";
|
|
36901
|
+
function configFileName(name) {
|
|
36902
|
+
return `${name}-config.json`;
|
|
36903
|
+
}
|
|
36904
|
+
function secretsFileName(name) {
|
|
36905
|
+
return `${name}-secrets.json`;
|
|
36906
|
+
}
|
|
36907
|
+
function namedVolumeSource(mount) {
|
|
36908
|
+
const src = mount.split(":")[0] ?? "";
|
|
36909
|
+
return src && !src.startsWith(".") && !src.startsWith("/") ? src : null;
|
|
36910
|
+
}
|
|
36911
|
+
function serviceBlock(c, secretDoc) {
|
|
36912
|
+
const hasSecretDoc = secretDoc !== void 0 && Object.keys(secretDoc).length > 0;
|
|
36913
|
+
const lines = [` ${c.name}:`, ` image: ${c.image}`, ` pull_policy: ${c.pull ?? "missing"}`, " restart: unless-stopped"];
|
|
36914
|
+
const labels = [];
|
|
36915
|
+
if (c.config !== void 0) {
|
|
36916
|
+
const hash2 = createHash("sha256").update(JSON.stringify(c.config)).digest("hex");
|
|
36917
|
+
labels.push(` com.foresthub.config-hash: "${hash2}"`);
|
|
36918
|
+
}
|
|
36919
|
+
if (hasSecretDoc) {
|
|
36920
|
+
const hash2 = createHash("sha256").update(JSON.stringify(secretDoc)).digest("hex");
|
|
36921
|
+
labels.push(` com.foresthub.secrets-hash: "${hash2}"`);
|
|
36922
|
+
}
|
|
36923
|
+
if (labels.length > 0) lines.push(" labels:", ...labels);
|
|
36924
|
+
if (c.user) lines.push(` user: "${c.user}"`);
|
|
36925
|
+
if (c.command && c.command.length > 0) {
|
|
36926
|
+
lines.push(" command:");
|
|
36927
|
+
for (const arg of c.command) lines.push(` - "${arg}"`);
|
|
36928
|
+
}
|
|
36929
|
+
lines.push(" env_file:", ` - path: ${c.name}.env`, " required: false");
|
|
36930
|
+
const volumes = [
|
|
36931
|
+
...c.config !== void 0 ? [`./${configFileName(c.name)}:${CONFIG_PATH}:ro`] : [],
|
|
36932
|
+
...hasSecretDoc ? [`./${secretsFileName(c.name)}:${SECRETS_PATH}:ro`] : [],
|
|
36933
|
+
...c.volumes ?? []
|
|
36934
|
+
];
|
|
36935
|
+
if (volumes.length > 0) {
|
|
36936
|
+
lines.push(" volumes:");
|
|
36937
|
+
for (const v of volumes) lines.push(` - ${v}`);
|
|
36938
|
+
}
|
|
36939
|
+
if (c.devices && c.devices.length > 0) {
|
|
36940
|
+
lines.push(" devices:");
|
|
36941
|
+
for (const d of c.devices) lines.push(` - "${d}:${d}"`);
|
|
36942
|
+
}
|
|
36943
|
+
if (c.ports && c.ports.length > 0) {
|
|
36944
|
+
lines.push(" ports:");
|
|
36945
|
+
for (const p of c.ports) lines.push(` - "${p}"`);
|
|
36946
|
+
}
|
|
36947
|
+
if (c.privileged) {
|
|
36948
|
+
lines.push(
|
|
36949
|
+
" # ADC/DAC/PWM need sysfs access (/sys/class/pwm, /sys/bus/iio). privileged is",
|
|
36950
|
+
" # the simple default \u2014 tighten to specific bind-mounts if your policy requires.",
|
|
36951
|
+
" privileged: true"
|
|
36952
|
+
);
|
|
36103
36953
|
}
|
|
36104
|
-
|
|
36105
|
-
if (Object.keys(gpios).length) deviceManifest.gpios = gpios;
|
|
36106
|
-
if (Object.keys(adcs).length) deviceManifest.adcs = adcs;
|
|
36107
|
-
if (Object.keys(dacs).length) deviceManifest.dacs = dacs;
|
|
36108
|
-
if (Object.keys(pwms).length) deviceManifest.pwms = pwms;
|
|
36109
|
-
if (Object.keys(serials).length) deviceManifest.serials = serials;
|
|
36110
|
-
return { deviceManifest, externalResources, deploymentMapping };
|
|
36954
|
+
return lines.join("\n");
|
|
36111
36955
|
}
|
|
36112
|
-
|
|
36113
|
-
|
|
36114
|
-
|
|
36115
|
-
const
|
|
36116
|
-
|
|
36117
|
-
|
|
36118
|
-
|
|
36119
|
-
|
|
36120
|
-
env.push(`${p.toUpperCase()}_API_KEY: \${${p.toUpperCase()}_API_KEY:-}`);
|
|
36121
|
-
}
|
|
36122
|
-
if (req.hasWebSearch) {
|
|
36123
|
-
env.push("ENGINE_WEB_SEARCH_PROVIDER: ${ENGINE_WEB_SEARCH_PROVIDER:-brave}");
|
|
36124
|
-
env.push("ENGINE_WEB_SEARCH_API_KEY: ${ENGINE_WEB_SEARCH_API_KEY:-}");
|
|
36125
|
-
}
|
|
36126
|
-
env.push("ENGINE_CONFIG_FILE: /etc/foresthub/workflow.json");
|
|
36127
|
-
env.push("ENGINE_MEMORY_DIR: /var/lib/foresthub/memory");
|
|
36128
|
-
if (hasHardware) env.push("ENGINE_DEVICE_MANIFEST_FILE: /etc/foresthub/device_manifest.json");
|
|
36129
|
-
if (hasExternal) env.push("ENGINE_EXTERNAL_RESOURCES_FILE: /etc/foresthub/external_resources.json");
|
|
36130
|
-
if (hasMapping) env.push("ENGINE_DEPLOYMENT_MAPPING_FILE: /etc/foresthub/deployment_mapping.json");
|
|
36131
|
-
const vols = ["./workflow.json:/etc/foresthub/workflow.json:ro"];
|
|
36132
|
-
if (hasHardware) vols.push("./device_manifest.json:/etc/foresthub/device_manifest.json:ro");
|
|
36133
|
-
if (hasExternal) vols.push("./external_resources.json:/etc/foresthub/external_resources.json:ro");
|
|
36134
|
-
if (hasMapping) vols.push("./deployment_mapping.json:/etc/foresthub/deployment_mapping.json:ro");
|
|
36135
|
-
vols.push("engine-memory:/var/lib/foresthub/memory");
|
|
36136
|
-
const cdev = /* @__PURE__ */ new Set();
|
|
36137
|
-
let needsPrivileged = false;
|
|
36138
|
-
for (const ch of req.hardwareChannels) {
|
|
36139
|
-
const dev = cfg.hardware[ch.id]?.chipOrDevice;
|
|
36140
|
-
if (!dev) continue;
|
|
36141
|
-
if (ch.family === "gpio" || ch.family === "serial") cdev.add(dev);
|
|
36142
|
-
else needsPrivileged = true;
|
|
36143
|
-
}
|
|
36144
|
-
const envBlock = env.map((l) => ` ${l}`).join("\n");
|
|
36145
|
-
const volBlock = vols.map((v) => ` - ${v}`).join("\n");
|
|
36146
|
-
const deviceBlock = cdev.size > 0 ? `
|
|
36147
|
-
devices:
|
|
36148
|
-
${[...cdev].map((d) => ` - "${d}:${d}"`).join("\n")}` : "";
|
|
36149
|
-
const privilegedBlock = needsPrivileged ? "\n # ADC/DAC/PWM need sysfs access (/sys/class/pwm, /sys/bus/iio). privileged is\n # the simple default \u2014 tighten to specific bind-mounts if your policy requires.\n privileged: true" : "";
|
|
36150
|
-
const needsRoot = cdev.size > 0 || needsPrivileged;
|
|
36151
|
-
const userBlock = needsRoot ? `
|
|
36152
|
-
# The engine image runs as nonroot and can't open root-owned device nodes /
|
|
36153
|
-
# sysfs files; root is the host-agnostic way to reach the mapped hardware below.
|
|
36154
|
-
user: "0:0"` : "";
|
|
36155
|
-
const deviceModels = [];
|
|
36156
|
-
for (const m of req.customModels) {
|
|
36157
|
-
const b = cfg.models[m.id];
|
|
36158
|
-
if (b?.location === "device")
|
|
36159
|
-
deviceModels.push({ service: sidecarServiceName(m.id), modelFile: b.modelFile, ctxVar: ctxSizeVar(m.id) });
|
|
36160
|
-
}
|
|
36161
|
-
const dependsBlock = deviceModels.length ? "\n depends_on:\n" + deviceModels.map((d) => ` ${d.service}:
|
|
36162
|
-
condition: service_healthy`).join("\n") : "";
|
|
36163
|
-
const sidecars = deviceModels.map(
|
|
36164
|
-
(d) => ` ${d.service}:
|
|
36165
|
-
image: ghcr.io/ggml-org/llama.cpp:server-b8589
|
|
36166
|
-
pull_policy: missing
|
|
36167
|
-
restart: unless-stopped
|
|
36168
|
-
command:
|
|
36169
|
-
- --model
|
|
36170
|
-
- /models/${d.modelFile}
|
|
36171
|
-
- --host
|
|
36172
|
-
- "0.0.0.0"
|
|
36173
|
-
- --port
|
|
36174
|
-
- "8080"
|
|
36175
|
-
- --ctx-size
|
|
36176
|
-
- "\${${d.ctxVar}:-4096}"
|
|
36177
|
-
volumes:
|
|
36178
|
-
- ./models:/models:ro
|
|
36179
|
-
healthcheck:
|
|
36180
|
-
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
|
|
36181
|
-
interval: 10s
|
|
36182
|
-
timeout: 5s
|
|
36183
|
-
retries: 30
|
|
36184
|
-
start_period: 60s`
|
|
36185
|
-
).join("\n\n");
|
|
36186
|
-
const sidecarBlock = sidecars ? `
|
|
36187
|
-
|
|
36188
|
-
${sidecars}` : "";
|
|
36189
|
-
return `# ForestHub engine \u2014 minimal standalone Compose deployment.
|
|
36956
|
+
function composeYaml(spec, secretDocs = {}) {
|
|
36957
|
+
const services = spec.components.map((c) => serviceBlock(c, secretDocs[c.name])).join("\n\n");
|
|
36958
|
+
const named = [...new Set(spec.components.flatMap((c) => (c.volumes ?? []).map(namedVolumeSource).filter((s) => s !== null)))];
|
|
36959
|
+
const volumesBlock = named.length > 0 ? `
|
|
36960
|
+
volumes:
|
|
36961
|
+
${named.map((n) => ` ${n}:`).join("\n")}
|
|
36962
|
+
` : "";
|
|
36963
|
+
return `# Edge Agents engine \u2014 minimal standalone Compose deployment.
|
|
36190
36964
|
# See README.md for the build/transfer/run workflow.
|
|
36191
|
-
# See
|
|
36965
|
+
# See the <component>.env files for operator-set values.
|
|
36192
36966
|
|
|
36193
36967
|
services:
|
|
36194
|
-
|
|
36195
|
-
|
|
36196
|
-
pull_policy: never
|
|
36197
|
-
restart: unless-stopped${userBlock}${dependsBlock}
|
|
36198
|
-
environment:
|
|
36199
|
-
${envBlock}
|
|
36200
|
-
volumes:
|
|
36201
|
-
${volBlock}${deviceBlock}${privilegedBlock}${sidecarBlock}
|
|
36202
|
-
|
|
36203
|
-
volumes:
|
|
36204
|
-
engine-memory:
|
|
36205
|
-
`;
|
|
36968
|
+
${services}
|
|
36969
|
+
${volumesBlock}`;
|
|
36206
36970
|
}
|
|
36207
36971
|
function envFile(cfg) {
|
|
36208
36972
|
const localProviders = ALL_PROVIDERS.filter((p) => Boolean(cfg.llmKeys[p]));
|
|
@@ -36216,48 +36980,33 @@ ENGINE_WEB_SEARCH_PROVIDER=${cfg.webSearch.provider}
|
|
|
36216
36980
|
ENGINE_WEB_SEARCH_API_KEY=${cfg.webSearch.apiKey}
|
|
36217
36981
|
|
|
36218
36982
|
` : "";
|
|
36219
|
-
|
|
36220
|
-
|
|
36221
|
-
#
|
|
36222
|
-
${deviceModelIds.map((id) => `${ctxSizeVar(id)}=4096`).join("\n")}
|
|
36223
|
-
|
|
36224
|
-
` : "";
|
|
36225
|
-
return `# ForestHub engine \u2014 operator configuration.
|
|
36226
|
-
# Auto-generated by \`fh-workflow deploy\`. \`docker compose\` auto-loads this file.
|
|
36227
|
-
# Secret values: chmod 600 .env after editing.
|
|
36983
|
+
return `# Edge Agents engine \u2014 operator configuration.
|
|
36984
|
+
# Auto-generated by \`fh-workflow deploy\`. Loaded into the engine via the compose
|
|
36985
|
+
# \`env_file\`. Secret values: chmod 600 this file after editing.
|
|
36228
36986
|
|
|
36229
|
-
${providerSection}${webSearchSection}
|
|
36987
|
+
${providerSection}${webSearchSection}# ----- Runtime -----
|
|
36230
36988
|
ENGINE_LOG_LEVEL=${cfg.logLevel} # debug | info | warn | error
|
|
36231
36989
|
`;
|
|
36232
36990
|
}
|
|
36233
|
-
function readme(cfg,
|
|
36991
|
+
function readme(spec, cfg, hasProviderModel, hasSecrets = false) {
|
|
36234
36992
|
const localProviders = ALL_PROVIDERS.filter((p) => Boolean(cfg.llmKeys[p]));
|
|
36235
|
-
const providerBlock = localProviders.length === 0 ? "_No local API keys set._ An Agent that uses a catalog model will fail at build \u2014 set the matching key in
|
|
36236
|
-
const
|
|
36237
|
-
const
|
|
36238
|
-
const
|
|
36239
|
-
|
|
36240
|
-
|
|
36241
|
-
|
|
36242
|
-
|
|
36243
|
-
|
|
36244
|
-
}
|
|
36245
|
-
const hasExternalService = req.mqttChannels.length > 0 || hasNetworkModel;
|
|
36246
|
-
const deployFiles = [];
|
|
36247
|
-
if (hasHardware) deployFiles.push("- `device_manifest.json` \u2014 hardware the engine binds at boot");
|
|
36248
|
-
if (hasExternal)
|
|
36249
|
-
deployFiles.push("- `external_resources.json` \u2014 MQTT brokers / model endpoints (**secrets \u2014 `chmod 600`**)");
|
|
36250
|
-
if (hasHardware || hasExternal)
|
|
36251
|
-
deployFiles.push("- `deployment_mapping.json` \u2014 maps the workflow's resources to the entries above");
|
|
36252
|
-
const deployFilesBlock = deployFiles.length ? "\n" + deployFiles.join("\n") : "";
|
|
36993
|
+
const providerBlock = localProviders.length === 0 ? "_No local API keys set._ An Agent that uses a catalog model will fail at build \u2014 set the matching key in `engine.env`." : localProviders.map((p) => `- **${p}** \u2014 runs locally (your API key)`).join("\n");
|
|
36994
|
+
const engine = spec.components.find((c) => c.name === "engine");
|
|
36995
|
+
const hasHardware = (engine?.devices?.length ?? 0) > 0 || Boolean(engine?.privileged);
|
|
36996
|
+
const deviceModels = Object.entries(cfg.models).flatMap(
|
|
36997
|
+
([id, b]) => b.location === "device" ? [{ dir: `./workspaces/${sidecarServiceName(id)}`, file: b.modelFile }] : []
|
|
36998
|
+
);
|
|
36999
|
+
const hasNetworkModel = Object.values(cfg.models).some((b) => b.location === "network");
|
|
37000
|
+
const hasMqtt = Object.keys(cfg.mqtt).length > 0;
|
|
37001
|
+
const hasExternalService = hasMqtt || hasNetworkModel;
|
|
36253
37002
|
const notes = [];
|
|
36254
|
-
if (
|
|
37003
|
+
if (hasProviderModel || localProviders.length > 0) {
|
|
36255
37004
|
notes.push(`## LLM provider keys
|
|
36256
37005
|
|
|
36257
37006
|
${providerBlock}
|
|
36258
37007
|
|
|
36259
|
-
The engine calls each provider directly with the API key from
|
|
36260
|
-
key, an Agent node that uses that provider's catalog model fails at build.`);
|
|
37008
|
+
The engine calls each provider directly with the API key from \`engine.env\`. Without
|
|
37009
|
+
a key, an Agent node that uses that provider's catalog model fails at build.`);
|
|
36261
37010
|
}
|
|
36262
37011
|
if (hasHardware) {
|
|
36263
37012
|
notes.push(`## Hardware access
|
|
@@ -36270,18 +37019,20 @@ and tighten it to your security policy before deploying.`);
|
|
|
36270
37019
|
if (hasExternalService) {
|
|
36271
37020
|
notes.push(`## External resources
|
|
36272
37021
|
|
|
36273
|
-
|
|
36274
|
-
|
|
36275
|
-
|
|
37022
|
+
The broker/endpoint credentials the engine connects with live in the mounted secret
|
|
37023
|
+
document \`engine-secrets.json\` (a JSON map of resource id -> secret value, mounted
|
|
37024
|
+
read-only at \`/etc/foresthub/secrets.json\`) \u2014 keep it \`chmod 600\`. This
|
|
37025
|
+
bundle does not start those services; it assumes the broker/endpoint already exists
|
|
37026
|
+
and is reachable from the controller.`);
|
|
36276
37027
|
}
|
|
36277
|
-
if (
|
|
37028
|
+
if (deviceModels.length > 0) {
|
|
36278
37029
|
notes.push(`## On-device models
|
|
36279
37030
|
|
|
36280
37031
|
This bundle runs a llama-server container per on-device model; the engine reaches it
|
|
36281
|
-
over the compose network by service name.
|
|
36282
|
-
|
|
36283
|
-
|
|
36284
|
-
${
|
|
37032
|
+
over the compose network by service name. Each GGUF must sit in that model's workspace
|
|
37033
|
+
dir below (read-only mounted into its container). They are too large for the main
|
|
37034
|
+
\`scp\` line, so step 3 transfers them separately:
|
|
37035
|
+
${deviceModels.map((m) => `- \`${m.dir}/${m.file}\``).join("\n")}`);
|
|
36285
37036
|
}
|
|
36286
37037
|
if (hasNetworkModel) {
|
|
36287
37038
|
notes.push(`## Network models
|
|
@@ -36289,43 +37040,38 @@ ${deviceModelFiles.map((f) => `- \`./models/${f}\``).join("\n")}`);
|
|
|
36289
37040
|
A network model points at an inference endpoint **you run yourself** (llama-server, vLLM,
|
|
36290
37041
|
Ollama, ...) on another machine. This bundle does not start that server for you.`);
|
|
36291
37042
|
}
|
|
36292
|
-
if (
|
|
37043
|
+
if (cfg.webSearch) {
|
|
36293
37044
|
notes.push(`## Web search
|
|
36294
37045
|
|
|
36295
|
-
The web-search API key is in
|
|
37046
|
+
The web-search API key is in \`engine.env\` (\`ENGINE_WEB_SEARCH_API_KEY\`).`);
|
|
36296
37047
|
}
|
|
36297
37048
|
const notesBlock = notes.length ? "\n" + notes.join("\n\n") + "\n" : "";
|
|
36298
|
-
const
|
|
36299
|
-
hasHardware ? "device_manifest.json" : "",
|
|
36300
|
-
hasExternal ? "external_resources.json" : "",
|
|
36301
|
-
hasHardware || hasExternal ? "deployment_mapping.json" : ""
|
|
36302
|
-
].filter(Boolean);
|
|
36303
|
-
const transferExtraStr = transferExtra.length ? " " + transferExtra.join(" ") : "";
|
|
36304
|
-
const modelsTransfer = deviceModelFiles.length ? `
|
|
37049
|
+
const modelsTransfer = deviceModels.length ? `
|
|
36305
37050
|
|
|
36306
37051
|
# On-device model weights \u2014 too large for the line above (GGUF can be several GB).
|
|
36307
|
-
#
|
|
36308
|
-
scp -r
|
|
36309
|
-
# ...or download them directly into ~/fh-engine/
|
|
36310
|
-
const runBlock =
|
|
37052
|
+
# Put each GGUF in its model's workspace dir, then copy the workspaces tree:
|
|
37053
|
+
scp -r workspaces/ $CONTROLLER_USER@$CONTROLLER_ADDR:~/fh-engine/
|
|
37054
|
+
# ...or download them directly into ~/fh-engine/workspaces/<model>/ on the controller.` : "";
|
|
37055
|
+
const runBlock = deviceModels.length ? `ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker load -i fh-engine.tar'
|
|
36311
37056
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose up -d'
|
|
36312
37057
|
|
|
36313
|
-
# The engine
|
|
36314
|
-
#
|
|
37058
|
+
# The engine and the llama-server sidecar start independently; the engine reaches the
|
|
37059
|
+
# model once it is up. These show every container, not just the engine:
|
|
36315
37060
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose ps'
|
|
36316
37061
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose logs -f'` : `ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker load -i fh-engine.tar'
|
|
36317
37062
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose up -d'
|
|
36318
37063
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose logs -f engine'`;
|
|
36319
|
-
return `#
|
|
37064
|
+
return `# Edge Agents engine \u2014 deployment bundle
|
|
36320
37065
|
|
|
36321
37066
|
Generated by \`fh-workflow deploy\`. This directory contains everything needed to
|
|
36322
37067
|
run one engine instance on an edge controller, **standalone** \u2014 the engine boots
|
|
36323
|
-
the workflow from \`
|
|
37068
|
+
the workflow from \`engine-config.json\` and runs it autonomously:
|
|
36324
37069
|
|
|
36325
37070
|
- \`docker-compose.yml\` \u2014 deployment template
|
|
36326
|
-
- \`
|
|
36327
|
-
-
|
|
36328
|
-
- \`
|
|
37071
|
+
- \`engine-config.json\` \u2014 the engine's single boot config (workflow + bindings + device manifest)
|
|
37072
|
+
- \`deployment-spec.json\` \u2014 the full resolved deployment spec (deployment record)
|
|
37073
|
+
- \`engine.env\` \u2014 operator configuration loaded into the engine (already filled in, \`chmod 600\` it)
|
|
37074
|
+
${hasSecrets ? "- `engine-secrets.json` \u2014 resource credentials, mounted read-only at `/etc/foresthub/secrets.json` (already filled in, `chmod 600` it)\n" : ""}- \`fh-engine.tar\` \u2014 image tarball (you build this in step 1 below)
|
|
36329
37075
|
${notesBlock}
|
|
36330
37076
|
## 1. Build the image (on the dev machine)
|
|
36331
37077
|
|
|
@@ -36341,10 +37087,10 @@ docker buildx build --platform linux/arm64 -t fh-engine:latest --load .
|
|
|
36341
37087
|
docker buildx build --platform linux/amd64 -t fh-engine:latest --load .
|
|
36342
37088
|
\`\`\`
|
|
36343
37089
|
|
|
36344
|
-
## 2. Review the generated
|
|
37090
|
+
## 2. Review the generated \`engine.env\`
|
|
36345
37091
|
|
|
36346
37092
|
\`\`\`bash
|
|
36347
|
-
chmod 600 .env
|
|
37093
|
+
chmod 600 engine.env
|
|
36348
37094
|
\`\`\`
|
|
36349
37095
|
|
|
36350
37096
|
The wizard already filled in your operator values. Double-check before
|
|
@@ -36362,7 +37108,7 @@ docker save fh-engine:latest -o ../path/to/this/bundle/fh-engine.tar
|
|
|
36362
37108
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'mkdir -p ~/fh-engine'
|
|
36363
37109
|
|
|
36364
37110
|
cd path/to/this/bundle
|
|
36365
|
-
scp fh-engine.tar docker-compose.yml
|
|
37111
|
+
scp fh-engine.tar docker-compose.yml engine-config.json deployment-spec.json engine.env ${hasSecrets ? "engine-secrets.json " : ""}\\
|
|
36366
37112
|
$CONTROLLER_USER@$CONTROLLER_ADDR:~/fh-engine/${modelsTransfer}
|
|
36367
37113
|
\`\`\`
|
|
36368
37114
|
|
|
@@ -36372,75 +37118,77 @@ scp fh-engine.tar docker-compose.yml workflow.json${transferExtraStr} .env \\
|
|
|
36372
37118
|
${runBlock}
|
|
36373
37119
|
\`\`\`
|
|
36374
37120
|
|
|
36375
|
-
## Agent memory
|
|
37121
|
+
## Agent memory & durable data
|
|
36376
37122
|
|
|
36377
|
-
|
|
36378
|
-
\`/var/lib/foresthub/
|
|
36379
|
-
only \`docker compose down -v\` deletes them. The volume is namespaced by the
|
|
36380
|
-
compose project name, which defaults to the bundle directory's name \u2014 keep it
|
|
36381
|
-
stable across updates, or pin it via \`COMPOSE_PROJECT_NAME\`. In this standalone
|
|
36382
|
-
bundle the volume is the only copy of the memory.
|
|
37123
|
+
Each container's durable data is a plain host directory in this bundle,
|
|
37124
|
+
\`./workspaces/<container>/\` (mounted read-write at \`/var/lib/foresthub/workspace\`):
|
|
36383
37125
|
|
|
36384
|
-
|
|
36385
|
-
|
|
36386
|
-
directory you must create and chown to the engine's nonroot user yourself:
|
|
37126
|
+
- \`./workspaces/engine/\` \u2014 the engine's memory files.
|
|
37127
|
+
- \`./workspaces/<model>/\` \u2014 an on-device model's GGUF weights (see above).
|
|
36387
37128
|
|
|
36388
|
-
|
|
36389
|
-
|
|
36390
|
-
|
|
36391
|
-
|
|
37129
|
+
These are ordinary files: back them up by copying the folder, inspect them directly.
|
|
37130
|
+
They persist across restarts and redeploys **as long as you keep this bundle directory
|
|
37131
|
+
stable** \u2014 deploy into the same path each time; a fresh directory starts empty. This
|
|
37132
|
+
bundle is the only copy; there is no backend mirror.
|
|
36392
37133
|
`;
|
|
36393
37134
|
}
|
|
36394
|
-
function ctxSizeVar(modelId) {
|
|
36395
|
-
const suffix = modelId.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
36396
|
-
return `LLAMA_CTX_SIZE_${suffix || "MODEL"}`;
|
|
36397
|
-
}
|
|
36398
37135
|
function slugify2(name) {
|
|
36399
37136
|
return name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
36400
37137
|
}
|
|
36401
37138
|
|
|
36402
37139
|
// cli/deploy/write.ts
|
|
36403
|
-
async function writeOutput(
|
|
36404
|
-
const
|
|
36405
|
-
|
|
36406
|
-
|
|
36407
|
-
const contents = await fs7.readdir(dir);
|
|
37140
|
+
async function writeOutput(spec, resourceSecrets, cfg, req, componentEnv = {}) {
|
|
37141
|
+
const dir = path11.resolve(process.cwd(), cfg.outputDir);
|
|
37142
|
+
if (existsSync4(dir)) {
|
|
37143
|
+
const contents = await fs8.readdir(dir);
|
|
36408
37144
|
if (contents.length > 0) {
|
|
36409
37145
|
if (!cfg.force) {
|
|
36410
37146
|
process.stderr.write(`output dir not empty: ${dir} (use --force to overwrite)
|
|
36411
37147
|
`);
|
|
36412
37148
|
process.exit(1);
|
|
36413
37149
|
}
|
|
36414
|
-
await
|
|
36415
|
-
await
|
|
37150
|
+
await fs8.rm(dir, { recursive: true, force: true });
|
|
37151
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
36416
37152
|
}
|
|
36417
37153
|
} else {
|
|
36418
|
-
await
|
|
37154
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
36419
37155
|
}
|
|
36420
37156
|
const written = [];
|
|
36421
37157
|
const emit = async (name, content, secret = false) => {
|
|
36422
|
-
const out =
|
|
37158
|
+
const out = path11.join(dir, name);
|
|
36423
37159
|
const opts = secret ? { encoding: "utf-8", mode: 384 } : { encoding: "utf-8" };
|
|
36424
|
-
await
|
|
37160
|
+
await fs8.writeFile(out, content, opts);
|
|
36425
37161
|
written.push(out);
|
|
36426
37162
|
};
|
|
36427
37163
|
const json2 = (v) => JSON.stringify(v, null, 2) + "\n";
|
|
36428
|
-
|
|
36429
|
-
|
|
36430
|
-
|
|
36431
|
-
|
|
36432
|
-
|
|
36433
|
-
|
|
36434
|
-
|
|
36435
|
-
|
|
36436
|
-
|
|
36437
|
-
|
|
36438
|
-
|
|
37164
|
+
for (const c of spec.components) {
|
|
37165
|
+
if (c.config !== void 0) await emit(configFileName(c.name), json2(c.config));
|
|
37166
|
+
}
|
|
37167
|
+
const secretDocs = Object.keys(resourceSecrets).length > 0 ? { engine: resourceSecrets } : {};
|
|
37168
|
+
await emit("deployment-spec.json", json2(spec));
|
|
37169
|
+
await emit("docker-compose.yml", composeYaml(spec, secretDocs));
|
|
37170
|
+
await emit("engine.env", envFile(cfg), true);
|
|
37171
|
+
for (const [name, doc] of Object.entries(secretDocs)) {
|
|
37172
|
+
await emit(secretsFileName(name), json2(doc), true);
|
|
37173
|
+
}
|
|
37174
|
+
await emit("README.md", readme(spec, cfg, req.hasProviderModel, Object.keys(secretDocs).length > 0));
|
|
37175
|
+
for (const [name, text] of Object.entries(componentEnv)) {
|
|
37176
|
+
await emit(`${name}.env`, text, true);
|
|
37177
|
+
}
|
|
37178
|
+
const workspaceSources = new Set(
|
|
37179
|
+
spec.components.flatMap(
|
|
37180
|
+
(c) => (c.volumes ?? []).map((v) => v.split(":")[0] ?? "").filter((src) => src.startsWith("./workspaces/"))
|
|
37181
|
+
)
|
|
37182
|
+
);
|
|
37183
|
+
for (const src of workspaceSources) {
|
|
37184
|
+
await fs8.mkdir(path11.join(dir, src), { recursive: true });
|
|
36439
37185
|
}
|
|
36440
37186
|
return written;
|
|
36441
37187
|
}
|
|
36442
37188
|
|
|
36443
37189
|
// cli/deploy/index.ts
|
|
37190
|
+
var ENGINE_IMAGE = "fh-engine:latest";
|
|
37191
|
+
var LLAMA_SERVER_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b8589";
|
|
36444
37192
|
var USAGE = `Usage: fh-workflow deploy <workflow.json> [flags]
|
|
36445
37193
|
|
|
36446
37194
|
Generates a self-contained, standalone Engine deployment bundle (workflow.json,
|
|
@@ -36451,6 +37199,9 @@ autonomously. Missing values are filled in interactively, or supplied via
|
|
|
36451
37199
|
LLM provider keys (set one for each catalog model an Agent uses):
|
|
36452
37200
|
${ALL_PROVIDERS.map((p) => ` --${p}-key KEY`).join("\n")}
|
|
36453
37201
|
|
|
37202
|
+
Custom components (extra containers to run alongside the engine):
|
|
37203
|
+
--component DIR folder with a component.json (repeatable)
|
|
37204
|
+
|
|
36454
37205
|
Output:
|
|
36455
37206
|
--output DIR default: ./<workflow-name>-bundle
|
|
36456
37207
|
--force overwrite existing DIR
|
|
@@ -36472,6 +37223,7 @@ function parseFlags(args2) {
|
|
|
36472
37223
|
output: { type: "string" },
|
|
36473
37224
|
"log-level": { type: "string" },
|
|
36474
37225
|
values: { type: "string" },
|
|
37226
|
+
component: { type: "string", multiple: true },
|
|
36475
37227
|
force: { type: "boolean", default: false },
|
|
36476
37228
|
help: { type: "boolean", short: "h", default: false }
|
|
36477
37229
|
},
|
|
@@ -36488,6 +37240,7 @@ function parseFlags(args2) {
|
|
|
36488
37240
|
output: values.output,
|
|
36489
37241
|
logLevel: values["log-level"],
|
|
36490
37242
|
values: values.values,
|
|
37243
|
+
component: values.component ?? [],
|
|
36491
37244
|
force: values.force ?? false,
|
|
36492
37245
|
help: values.help ?? false
|
|
36493
37246
|
};
|
|
@@ -36513,10 +37266,10 @@ function partialFromFlags(flags, fileValues) {
|
|
|
36513
37266
|
};
|
|
36514
37267
|
}
|
|
36515
37268
|
async function loadValues(source) {
|
|
36516
|
-
const abs =
|
|
37269
|
+
const abs = path12.resolve(process.cwd(), source);
|
|
36517
37270
|
let raw;
|
|
36518
37271
|
try {
|
|
36519
|
-
raw = await
|
|
37272
|
+
raw = await fs9.readFile(abs, "utf-8");
|
|
36520
37273
|
} catch (err) {
|
|
36521
37274
|
if (err.code === "ENOENT") {
|
|
36522
37275
|
process.stderr.write(`Values file not found: ${abs}
|
|
@@ -36589,15 +37342,41 @@ function configFromPartial(p, outputDirDefault) {
|
|
|
36589
37342
|
webSearch: p.webSearch
|
|
36590
37343
|
};
|
|
36591
37344
|
}
|
|
37345
|
+
async function loadFlagComponents(dirs) {
|
|
37346
|
+
const uniqueDirs = [...new Set(dirs.map((d) => path12.resolve(process.cwd(), d)))];
|
|
37347
|
+
const entries = await Promise.all(
|
|
37348
|
+
uniqueDirs.map(async (dir) => ({ source: path12.join(dir, "component.json"), data: await readComponentJson(dir), dir }))
|
|
37349
|
+
);
|
|
37350
|
+
parseDeployComponents(entries);
|
|
37351
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37352
|
+
const loaded = [];
|
|
37353
|
+
for (const e of entries) {
|
|
37354
|
+
const component = e.data;
|
|
37355
|
+
if (seen.has(component.name)) {
|
|
37356
|
+
throw new Error(`duplicate component name "${component.name}" (two --component folders declare it)`);
|
|
37357
|
+
}
|
|
37358
|
+
seen.add(component.name);
|
|
37359
|
+
loaded.push({ component, dir: e.dir });
|
|
37360
|
+
}
|
|
37361
|
+
return loaded;
|
|
37362
|
+
}
|
|
37363
|
+
async function resolveComponentsEnv(loaded) {
|
|
37364
|
+
const env = {};
|
|
37365
|
+
for (const { component, dir } of loaded) {
|
|
37366
|
+
const text = await resolveComponentEnv(dir, component.name, { interactive: false });
|
|
37367
|
+
if (text !== null) env[component.name] = text;
|
|
37368
|
+
}
|
|
37369
|
+
return env;
|
|
37370
|
+
}
|
|
36592
37371
|
async function deployCommand(workflowPath, args2) {
|
|
36593
37372
|
if (!workflowPath || workflowPath === "--help" || workflowPath === "-h") {
|
|
36594
37373
|
process.stdout.write(USAGE);
|
|
36595
37374
|
process.exit(workflowPath ? 0 : 1);
|
|
36596
37375
|
}
|
|
36597
|
-
const abs =
|
|
37376
|
+
const abs = path12.resolve(process.cwd(), workflowPath);
|
|
36598
37377
|
let raw;
|
|
36599
37378
|
try {
|
|
36600
|
-
raw = await
|
|
37379
|
+
raw = await fs9.readFile(abs, "utf-8");
|
|
36601
37380
|
} catch (err) {
|
|
36602
37381
|
const code = err.code;
|
|
36603
37382
|
if (code === "ENOENT") {
|
|
@@ -36628,20 +37407,44 @@ async function deployCommand(workflowPath, args2) {
|
|
|
36628
37407
|
process.stdout.write(USAGE);
|
|
36629
37408
|
process.exit(0);
|
|
36630
37409
|
}
|
|
36631
|
-
|
|
37410
|
+
let domain2;
|
|
37411
|
+
try {
|
|
37412
|
+
domain2 = deserialize7(workflow);
|
|
37413
|
+
} catch (err) {
|
|
37414
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37415
|
+
`);
|
|
37416
|
+
process.exit(1);
|
|
37417
|
+
}
|
|
37418
|
+
const req = deriveRequirements(domain2);
|
|
36632
37419
|
if (req.hasRetriever) {
|
|
36633
37420
|
process.stderr.write(
|
|
36634
37421
|
"error: workflow references a Retriever node (RAG).\n A standalone engine has no retriever, so the node cannot resolve and the\n engine fails at build. Remove the Retriever node to deploy standalone.\n"
|
|
36635
37422
|
);
|
|
36636
37423
|
process.exit(1);
|
|
36637
37424
|
}
|
|
36638
|
-
const workflowName =
|
|
37425
|
+
const workflowName = path12.basename(abs, path12.extname(abs));
|
|
36639
37426
|
const outputDirDefault = `./${slugify2(workflowName)}-bundle`;
|
|
36640
37427
|
const fileValues = flags.values ? await loadValues(flags.values) : {};
|
|
36641
37428
|
const partial2 = partialFromFlags(flags, fileValues);
|
|
37429
|
+
let preloaded;
|
|
37430
|
+
try {
|
|
37431
|
+
preloaded = await loadFlagComponents(flags.component);
|
|
37432
|
+
} catch (err) {
|
|
37433
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37434
|
+
`);
|
|
37435
|
+
process.exit(1);
|
|
37436
|
+
}
|
|
36642
37437
|
let cfg;
|
|
37438
|
+
let customComponents;
|
|
37439
|
+
let componentEnv;
|
|
36643
37440
|
if (process.stdin.isTTY) {
|
|
36644
|
-
cfg = await promptMissing(
|
|
37441
|
+
({ config: cfg, customComponents, componentEnv } = await promptMissing(
|
|
37442
|
+
partial2,
|
|
37443
|
+
outputDirDefault,
|
|
37444
|
+
req,
|
|
37445
|
+
workflowName,
|
|
37446
|
+
preloaded
|
|
37447
|
+
));
|
|
36645
37448
|
} else {
|
|
36646
37449
|
const missing = missingRequired(req, partial2);
|
|
36647
37450
|
if (missing.length > 0) {
|
|
@@ -36652,21 +37455,40 @@ async function deployCommand(workflowPath, args2) {
|
|
|
36652
37455
|
process.exit(1);
|
|
36653
37456
|
}
|
|
36654
37457
|
cfg = configFromPartial(partial2, outputDirDefault);
|
|
37458
|
+
customComponents = preloaded.map((c) => c.component);
|
|
37459
|
+
componentEnv = await resolveComponentsEnv(preloaded);
|
|
37460
|
+
}
|
|
37461
|
+
let built;
|
|
37462
|
+
try {
|
|
37463
|
+
built = buildDeploymentSpec(
|
|
37464
|
+
domain2,
|
|
37465
|
+
cfg,
|
|
37466
|
+
{
|
|
37467
|
+
id: slugify2(workflowName),
|
|
37468
|
+
engineImage: ENGINE_IMAGE,
|
|
37469
|
+
llamaServerImage: LLAMA_SERVER_IMAGE
|
|
37470
|
+
},
|
|
37471
|
+
customComponents
|
|
37472
|
+
);
|
|
37473
|
+
} catch (err) {
|
|
37474
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37475
|
+
`);
|
|
37476
|
+
process.exit(1);
|
|
36655
37477
|
}
|
|
36656
37478
|
let files;
|
|
36657
37479
|
try {
|
|
36658
|
-
files = await writeOutput(
|
|
37480
|
+
files = await writeOutput(built.spec, built.resourceSecrets, cfg, req, componentEnv);
|
|
36659
37481
|
} catch (err) {
|
|
36660
37482
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
36661
37483
|
`);
|
|
36662
37484
|
process.exit(1);
|
|
36663
37485
|
}
|
|
36664
|
-
const absOut =
|
|
37486
|
+
const absOut = path12.resolve(process.cwd(), cfg.outputDir);
|
|
36665
37487
|
process.stdout.write(`
|
|
36666
37488
|
\u2713 Deployment bundle written to ${absOut}
|
|
36667
37489
|
`);
|
|
36668
37490
|
for (const f of files) {
|
|
36669
|
-
process.stdout.write(` - ${
|
|
37491
|
+
process.stdout.write(` - ${path12.relative(process.cwd(), f)}
|
|
36670
37492
|
`);
|
|
36671
37493
|
}
|
|
36672
37494
|
process.stdout.write(`
|