@foresthubai/workflow-cli 0.4.2 → 0.4.4
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-CiLOHdeR.css +1 -0
- package/dist/assets/{index-B7syExE5.js → index-D7a-pqL0.js} +11 -11
- package/dist/index.html +14 -14
- package/dist-cli/cli.js +1232 -483
- package/dist-cli/deployment.yaml +83 -0
- package/dist-cli/llmproxy.yaml +300 -300
- package/dist-cli/workflow.yaml +912 -895
- package/package.json +2 -3
- package/dist/assets/index-CnM-MQ9w.css +0 -1
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 });
|
|
@@ -13121,6 +13121,11 @@ function toFunctionInfo(fn) {
|
|
|
13121
13121
|
}
|
|
13122
13122
|
|
|
13123
13123
|
// ../workflow-core/src/function/serialization.ts
|
|
13124
|
+
function serialize(fn) {
|
|
13125
|
+
const outputAssignments = {};
|
|
13126
|
+
for (const o of fn.outputs) outputAssignments[o.uid] = o.expression;
|
|
13127
|
+
return { functionInfo: toFunctionInfo(fn), outputAssignments };
|
|
13128
|
+
}
|
|
13124
13129
|
function deserialize(functionInfo, outputAssignments) {
|
|
13125
13130
|
return {
|
|
13126
13131
|
id: functionInfo.id,
|
|
@@ -13182,6 +13187,51 @@ function isToolFlow(type) {
|
|
|
13182
13187
|
}
|
|
13183
13188
|
|
|
13184
13189
|
// ../workflow-core/src/edge/serialization.ts
|
|
13190
|
+
function serialize2(edge) {
|
|
13191
|
+
const sourceHandle = edge.sourceHandle || "";
|
|
13192
|
+
const targetHandle = edge.targetHandle || "";
|
|
13193
|
+
const edgeType = edge.type;
|
|
13194
|
+
const from = { nodeId: edge.source, port: sourceHandle };
|
|
13195
|
+
const to = { nodeId: edge.target, port: targetHandle };
|
|
13196
|
+
switch (edgeType) {
|
|
13197
|
+
case "agentTask":
|
|
13198
|
+
return {
|
|
13199
|
+
id: edge.id,
|
|
13200
|
+
type: "agentTask",
|
|
13201
|
+
from,
|
|
13202
|
+
to,
|
|
13203
|
+
prompt: edge.data?.prompt ?? { expression: "", references: [], dataType: "string" }
|
|
13204
|
+
};
|
|
13205
|
+
case "agentChoice":
|
|
13206
|
+
return {
|
|
13207
|
+
id: edge.id,
|
|
13208
|
+
type: "agentChoice",
|
|
13209
|
+
from,
|
|
13210
|
+
to,
|
|
13211
|
+
...edge.data?.description ? { description: edge.data.description } : {}
|
|
13212
|
+
};
|
|
13213
|
+
case "agentDelegate":
|
|
13214
|
+
return {
|
|
13215
|
+
id: edge.id,
|
|
13216
|
+
type: "agentDelegate",
|
|
13217
|
+
from,
|
|
13218
|
+
to,
|
|
13219
|
+
...edge.data?.prompt ? { prompt: edge.data.prompt } : {},
|
|
13220
|
+
...edge.data?.description ? { description: edge.data.description } : {}
|
|
13221
|
+
};
|
|
13222
|
+
case "control":
|
|
13223
|
+
return { id: edge.id, type: "control", from, to };
|
|
13224
|
+
case "tool":
|
|
13225
|
+
return { id: edge.id, type: "tool", from, to };
|
|
13226
|
+
default:
|
|
13227
|
+
return {
|
|
13228
|
+
id: edge.id,
|
|
13229
|
+
type: sourceHandle.startsWith("ctrl") || targetHandle.startsWith("ctrl") ? "control" : "tool",
|
|
13230
|
+
from,
|
|
13231
|
+
to
|
|
13232
|
+
};
|
|
13233
|
+
}
|
|
13234
|
+
}
|
|
13185
13235
|
function deserialize2(apiEdge) {
|
|
13186
13236
|
let data;
|
|
13187
13237
|
if ((apiEdge.type === "agentTask" || apiEdge.type === "agentDelegate") && apiEdge.prompt) {
|
|
@@ -13271,6 +13321,14 @@ function isParameterActive(param, parameterValues, isToolInput) {
|
|
|
13271
13321
|
function isEmpty(value) {
|
|
13272
13322
|
return value === void 0 || value === null || value === "";
|
|
13273
13323
|
}
|
|
13324
|
+
function pruneArguments(args2, parameters, isToolInput = false) {
|
|
13325
|
+
for (const param of parameters) {
|
|
13326
|
+
const inactive = !!param.activationRules?.length && !isParameterActive(param, args2, isToolInput);
|
|
13327
|
+
if (inactive || isEmpty(args2[param.id])) {
|
|
13328
|
+
delete args2[param.id];
|
|
13329
|
+
}
|
|
13330
|
+
}
|
|
13331
|
+
}
|
|
13274
13332
|
function resolveExpressionType(param, args2, variables) {
|
|
13275
13333
|
if (param.fromReference) {
|
|
13276
13334
|
const ref = args2[param.fromReference];
|
|
@@ -13724,9 +13782,9 @@ var SerialWriteNodeDefinition = {
|
|
|
13724
13782
|
{
|
|
13725
13783
|
id: "portReference",
|
|
13726
13784
|
label: "Port",
|
|
13727
|
-
description: "Serial port to write to",
|
|
13785
|
+
description: "Serial port or log channel to write to",
|
|
13728
13786
|
type: "channelSelect",
|
|
13729
|
-
channelType: ["UART"]
|
|
13787
|
+
channelType: ["UART", "LOG"]
|
|
13730
13788
|
},
|
|
13731
13789
|
{
|
|
13732
13790
|
id: "value",
|
|
@@ -14105,7 +14163,7 @@ function isNodeUsedAsTool(nodeId, nodeData, edges) {
|
|
|
14105
14163
|
}
|
|
14106
14164
|
|
|
14107
14165
|
// ../workflow-core/src/channel/Channel.ts
|
|
14108
|
-
var ALL_CHANNEL_TYPES = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT"];
|
|
14166
|
+
var ALL_CHANNEL_TYPES = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT", "LOG"];
|
|
14109
14167
|
|
|
14110
14168
|
// ../workflow-core/src/channel/ChannelDefinition.ts
|
|
14111
14169
|
var CHANNEL_DEFINITION = {
|
|
@@ -14153,11 +14211,62 @@ var CHANNEL_DEFINITION = {
|
|
|
14153
14211
|
description: "MQTT topic this channel publishes to / subscribes on (e.g. sensors/temperature)",
|
|
14154
14212
|
type: "string",
|
|
14155
14213
|
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["MQTT"] }]
|
|
14214
|
+
},
|
|
14215
|
+
{
|
|
14216
|
+
id: "level",
|
|
14217
|
+
label: "Level",
|
|
14218
|
+
description: "Severity the engine records messages written to this channel at",
|
|
14219
|
+
type: "selection",
|
|
14220
|
+
default: "info",
|
|
14221
|
+
options: [
|
|
14222
|
+
{ value: "debug", label: "Debug" },
|
|
14223
|
+
{ value: "info", label: "Info" },
|
|
14224
|
+
{ value: "warn", label: "Warn" },
|
|
14225
|
+
{ value: "error", label: "Error" }
|
|
14226
|
+
],
|
|
14227
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["LOG"] }]
|
|
14228
|
+
},
|
|
14229
|
+
{
|
|
14230
|
+
id: "tag",
|
|
14231
|
+
label: "Tag",
|
|
14232
|
+
description: "Optional category stamped on each line so the backend can group workflow-emitted logs",
|
|
14233
|
+
type: "string",
|
|
14234
|
+
optional: true,
|
|
14235
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["LOG"] }]
|
|
14156
14236
|
}
|
|
14157
14237
|
]
|
|
14158
14238
|
};
|
|
14159
14239
|
|
|
14160
14240
|
// ../workflow-core/src/channel/serialization.ts
|
|
14241
|
+
function serialize3(ch) {
|
|
14242
|
+
const { id, label, type } = ch;
|
|
14243
|
+
const args2 = { ...ch.arguments, type };
|
|
14244
|
+
pruneArguments(args2, CHANNEL_DEFINITION.parameters);
|
|
14245
|
+
switch (type) {
|
|
14246
|
+
case "GPIOIN":
|
|
14247
|
+
return {
|
|
14248
|
+
type,
|
|
14249
|
+
id,
|
|
14250
|
+
label,
|
|
14251
|
+
bias: args2.bias,
|
|
14252
|
+
debounceMs: args2.debounceMs
|
|
14253
|
+
};
|
|
14254
|
+
case "GPIOOUT":
|
|
14255
|
+
return { type, id, label };
|
|
14256
|
+
case "ADC":
|
|
14257
|
+
return { type, id, label };
|
|
14258
|
+
case "PWM":
|
|
14259
|
+
return { type, id, label, frequency: args2.frequency };
|
|
14260
|
+
case "DAC":
|
|
14261
|
+
return { type, id, label };
|
|
14262
|
+
case "UART":
|
|
14263
|
+
return { type, id, label };
|
|
14264
|
+
case "MQTT":
|
|
14265
|
+
return { type, id, label, topic: args2.topic };
|
|
14266
|
+
case "LOG":
|
|
14267
|
+
return { type, id, label, level: args2.level, tag: args2.tag };
|
|
14268
|
+
}
|
|
14269
|
+
}
|
|
14161
14270
|
function deserialize3(api) {
|
|
14162
14271
|
const { id, label, type } = api;
|
|
14163
14272
|
const args2 = {};
|
|
@@ -14172,6 +14281,10 @@ function deserialize3(api) {
|
|
|
14172
14281
|
case "MQTT":
|
|
14173
14282
|
args2.topic = api.topic;
|
|
14174
14283
|
break;
|
|
14284
|
+
case "LOG":
|
|
14285
|
+
args2.level = api.level;
|
|
14286
|
+
args2.tag = api.tag;
|
|
14287
|
+
break;
|
|
14175
14288
|
case "GPIOOUT":
|
|
14176
14289
|
case "ADC":
|
|
14177
14290
|
case "DAC":
|
|
@@ -14254,6 +14367,27 @@ var MemoryRegistry = new MemoryDefinitionRegistry();
|
|
|
14254
14367
|
MemoryRegistry.initialize();
|
|
14255
14368
|
|
|
14256
14369
|
// ../workflow-core/src/memory/serialization.ts
|
|
14370
|
+
function serialize4(mem) {
|
|
14371
|
+
const { id, label, type, arguments: args2 } = mem;
|
|
14372
|
+
switch (type) {
|
|
14373
|
+
case "MemoryFile":
|
|
14374
|
+
return {
|
|
14375
|
+
type,
|
|
14376
|
+
id,
|
|
14377
|
+
label,
|
|
14378
|
+
description: args2.description ?? "",
|
|
14379
|
+
content: args2.content ?? "",
|
|
14380
|
+
...args2.maxSizeBytes != null ? { maxSizeBytes: args2.maxSizeBytes } : {}
|
|
14381
|
+
};
|
|
14382
|
+
case "VectorDatabase":
|
|
14383
|
+
return {
|
|
14384
|
+
type,
|
|
14385
|
+
id,
|
|
14386
|
+
label,
|
|
14387
|
+
...args2.description != null ? { description: args2.description } : {}
|
|
14388
|
+
};
|
|
14389
|
+
}
|
|
14390
|
+
}
|
|
14257
14391
|
function deserialize4(api) {
|
|
14258
14392
|
const { id, label, type } = api;
|
|
14259
14393
|
const args2 = {};
|
|
@@ -14301,6 +14435,18 @@ var ModelRegistry = new ModelDefinitionRegistry();
|
|
|
14301
14435
|
ModelRegistry.initialize();
|
|
14302
14436
|
|
|
14303
14437
|
// ../workflow-core/src/model/serialization.ts
|
|
14438
|
+
function serialize5(model) {
|
|
14439
|
+
const { id, label, type, arguments: args2 } = model;
|
|
14440
|
+
switch (type) {
|
|
14441
|
+
case "LLMModel":
|
|
14442
|
+
return {
|
|
14443
|
+
type,
|
|
14444
|
+
id,
|
|
14445
|
+
label,
|
|
14446
|
+
capabilities: args2.capabilities ?? ["chat"]
|
|
14447
|
+
};
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
14304
14450
|
function deserialize5(api) {
|
|
14305
14451
|
const { id, label, type } = api;
|
|
14306
14452
|
const args2 = {};
|
|
@@ -14349,6 +14495,261 @@ function buildFunctionNodeDef(fn, t = (key) => key) {
|
|
|
14349
14495
|
}
|
|
14350
14496
|
|
|
14351
14497
|
// ../workflow-core/src/node/serialization.ts
|
|
14498
|
+
function serialize6(node, isToolInput) {
|
|
14499
|
+
const result = serializeNodeData(node, node.position, isToolInput);
|
|
14500
|
+
if (node.label) {
|
|
14501
|
+
result.label = node.label;
|
|
14502
|
+
}
|
|
14503
|
+
if ("arguments" in result && result.arguments) {
|
|
14504
|
+
const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : void 0;
|
|
14505
|
+
if (def) {
|
|
14506
|
+
pruneArguments(result.arguments, def.parameters, isToolInput);
|
|
14507
|
+
}
|
|
14508
|
+
}
|
|
14509
|
+
return result;
|
|
14510
|
+
}
|
|
14511
|
+
function serializeNodeData(data, position, isToolInput) {
|
|
14512
|
+
switch (data.type) {
|
|
14513
|
+
case "ReadPin":
|
|
14514
|
+
return {
|
|
14515
|
+
id: data.id,
|
|
14516
|
+
type: data.type,
|
|
14517
|
+
position,
|
|
14518
|
+
arguments: {
|
|
14519
|
+
pinReference: data.arguments.pinReference,
|
|
14520
|
+
signalType: data.arguments.signalType,
|
|
14521
|
+
output: data.arguments.output,
|
|
14522
|
+
toolDescription: data.arguments.toolDescription
|
|
14523
|
+
}
|
|
14524
|
+
};
|
|
14525
|
+
case "SerialRead":
|
|
14526
|
+
return {
|
|
14527
|
+
id: data.id,
|
|
14528
|
+
type: data.type,
|
|
14529
|
+
position,
|
|
14530
|
+
arguments: {
|
|
14531
|
+
portReference: data.arguments.portReference,
|
|
14532
|
+
...data.arguments.prompt !== void 0 ? { prompt: data.arguments.prompt } : {},
|
|
14533
|
+
output: data.arguments.output
|
|
14534
|
+
}
|
|
14535
|
+
};
|
|
14536
|
+
case "WritePin":
|
|
14537
|
+
return {
|
|
14538
|
+
id: data.id,
|
|
14539
|
+
type: data.type,
|
|
14540
|
+
position,
|
|
14541
|
+
arguments: {
|
|
14542
|
+
pinReference: data.arguments.pinReference,
|
|
14543
|
+
signalType: data.arguments.signalType,
|
|
14544
|
+
value: data.arguments.value
|
|
14545
|
+
}
|
|
14546
|
+
};
|
|
14547
|
+
case "SerialWrite":
|
|
14548
|
+
return {
|
|
14549
|
+
id: data.id,
|
|
14550
|
+
type: data.type,
|
|
14551
|
+
position,
|
|
14552
|
+
arguments: {
|
|
14553
|
+
portReference: data.arguments.portReference,
|
|
14554
|
+
value: data.arguments.value
|
|
14555
|
+
}
|
|
14556
|
+
};
|
|
14557
|
+
case "Agent": {
|
|
14558
|
+
return {
|
|
14559
|
+
id: data.id,
|
|
14560
|
+
type: data.type,
|
|
14561
|
+
position,
|
|
14562
|
+
arguments: {
|
|
14563
|
+
name: data.arguments.name,
|
|
14564
|
+
model: data.arguments.model,
|
|
14565
|
+
instructions: data.arguments.instructions,
|
|
14566
|
+
maxTurns: data.arguments.maxTurns,
|
|
14567
|
+
outputDeclarations: data.arguments.outputDeclarations,
|
|
14568
|
+
memoryRefs: data.arguments.memoryRefs ?? [],
|
|
14569
|
+
answer: data.arguments.answer,
|
|
14570
|
+
toolDescription: data.arguments.toolDescription
|
|
14571
|
+
}
|
|
14572
|
+
};
|
|
14573
|
+
}
|
|
14574
|
+
case "If":
|
|
14575
|
+
return {
|
|
14576
|
+
id: data.id,
|
|
14577
|
+
type: data.type,
|
|
14578
|
+
position,
|
|
14579
|
+
arguments: {
|
|
14580
|
+
condition: data.arguments.condition
|
|
14581
|
+
}
|
|
14582
|
+
};
|
|
14583
|
+
case "OnFunctionCall":
|
|
14584
|
+
return {
|
|
14585
|
+
id: data.id,
|
|
14586
|
+
type: data.type,
|
|
14587
|
+
position
|
|
14588
|
+
};
|
|
14589
|
+
case "OnStartup":
|
|
14590
|
+
return {
|
|
14591
|
+
id: data.id,
|
|
14592
|
+
type: data.type,
|
|
14593
|
+
position
|
|
14594
|
+
};
|
|
14595
|
+
case "OnPinEdge":
|
|
14596
|
+
return {
|
|
14597
|
+
id: data.id,
|
|
14598
|
+
type: data.type,
|
|
14599
|
+
position,
|
|
14600
|
+
arguments: {
|
|
14601
|
+
pinReference: data.arguments.pinReference,
|
|
14602
|
+
edge: data.arguments.edge
|
|
14603
|
+
}
|
|
14604
|
+
};
|
|
14605
|
+
case "OnSerialReceive":
|
|
14606
|
+
return {
|
|
14607
|
+
id: data.id,
|
|
14608
|
+
type: data.type,
|
|
14609
|
+
position,
|
|
14610
|
+
arguments: {
|
|
14611
|
+
portReference: data.arguments.portReference,
|
|
14612
|
+
output: data.arguments.output
|
|
14613
|
+
}
|
|
14614
|
+
};
|
|
14615
|
+
case "OnThreshold":
|
|
14616
|
+
return {
|
|
14617
|
+
id: data.id,
|
|
14618
|
+
type: data.type,
|
|
14619
|
+
position,
|
|
14620
|
+
arguments: {
|
|
14621
|
+
variable: data.arguments.variable,
|
|
14622
|
+
threshold: data.arguments.threshold,
|
|
14623
|
+
direction: data.arguments.direction,
|
|
14624
|
+
deadband: data.arguments.deadband,
|
|
14625
|
+
output: data.arguments.output
|
|
14626
|
+
}
|
|
14627
|
+
};
|
|
14628
|
+
case "Delay":
|
|
14629
|
+
return {
|
|
14630
|
+
id: data.id,
|
|
14631
|
+
type: data.type,
|
|
14632
|
+
position,
|
|
14633
|
+
arguments: {
|
|
14634
|
+
delayMs: data.arguments.delayMs
|
|
14635
|
+
}
|
|
14636
|
+
};
|
|
14637
|
+
case "Ticker":
|
|
14638
|
+
return {
|
|
14639
|
+
id: data.id,
|
|
14640
|
+
type: data.type,
|
|
14641
|
+
position,
|
|
14642
|
+
arguments: {
|
|
14643
|
+
intervalValue: data.arguments.intervalValue,
|
|
14644
|
+
intervalUnit: data.arguments.intervalUnit
|
|
14645
|
+
}
|
|
14646
|
+
};
|
|
14647
|
+
case "Alarm":
|
|
14648
|
+
return {
|
|
14649
|
+
id: data.id,
|
|
14650
|
+
type: data.type,
|
|
14651
|
+
position,
|
|
14652
|
+
arguments: {
|
|
14653
|
+
time: data.arguments.time,
|
|
14654
|
+
days: data.arguments.days
|
|
14655
|
+
}
|
|
14656
|
+
};
|
|
14657
|
+
case "WebSearchTool":
|
|
14658
|
+
return {
|
|
14659
|
+
id: data.id,
|
|
14660
|
+
type: data.type,
|
|
14661
|
+
position,
|
|
14662
|
+
arguments: {
|
|
14663
|
+
maxResults: data.arguments.maxResults
|
|
14664
|
+
}
|
|
14665
|
+
};
|
|
14666
|
+
case "Retriever":
|
|
14667
|
+
return {
|
|
14668
|
+
id: data.id,
|
|
14669
|
+
type: data.type,
|
|
14670
|
+
position,
|
|
14671
|
+
arguments: {
|
|
14672
|
+
memoryReference: data.arguments.memoryReference,
|
|
14673
|
+
topK: data.arguments.topK,
|
|
14674
|
+
query: data.arguments.query,
|
|
14675
|
+
output: data.arguments.output,
|
|
14676
|
+
toolDescription: data.arguments.toolDescription
|
|
14677
|
+
}
|
|
14678
|
+
};
|
|
14679
|
+
case "WebFetch":
|
|
14680
|
+
return {
|
|
14681
|
+
id: data.id,
|
|
14682
|
+
type: data.type,
|
|
14683
|
+
position,
|
|
14684
|
+
arguments: {
|
|
14685
|
+
url: data.arguments.url,
|
|
14686
|
+
maxChars: data.arguments.maxChars,
|
|
14687
|
+
output: data.arguments.output
|
|
14688
|
+
}
|
|
14689
|
+
};
|
|
14690
|
+
case "FunctionCall": {
|
|
14691
|
+
const inputBindings = {};
|
|
14692
|
+
const outputBindings = {};
|
|
14693
|
+
const args2 = data.arguments;
|
|
14694
|
+
for (const arg of data.functionInfo.arguments) {
|
|
14695
|
+
const key = arg.uid ?? arg.name;
|
|
14696
|
+
const v = args2[key];
|
|
14697
|
+
if (v !== void 0) inputBindings[key] = v;
|
|
14698
|
+
}
|
|
14699
|
+
for (const ret of data.functionInfo.returns) {
|
|
14700
|
+
const key = ret.uid ?? ret.name;
|
|
14701
|
+
const v = args2[key];
|
|
14702
|
+
if (v !== void 0) outputBindings[key] = v;
|
|
14703
|
+
}
|
|
14704
|
+
const toolDescription = args2.toolDescription;
|
|
14705
|
+
return {
|
|
14706
|
+
id: data.id,
|
|
14707
|
+
type: data.type,
|
|
14708
|
+
functionId: data.functionInfo.id,
|
|
14709
|
+
position,
|
|
14710
|
+
arguments: {
|
|
14711
|
+
inputBindings,
|
|
14712
|
+
outputBindings,
|
|
14713
|
+
...isToolInput && toolDescription !== void 0 ? { toolDescription } : {}
|
|
14714
|
+
}
|
|
14715
|
+
};
|
|
14716
|
+
}
|
|
14717
|
+
case "SetVariable":
|
|
14718
|
+
return {
|
|
14719
|
+
id: data.id,
|
|
14720
|
+
type: data.type,
|
|
14721
|
+
position,
|
|
14722
|
+
arguments: {
|
|
14723
|
+
variable: data.arguments.variable,
|
|
14724
|
+
value: data.arguments.value
|
|
14725
|
+
}
|
|
14726
|
+
};
|
|
14727
|
+
case "MqttPublish":
|
|
14728
|
+
return {
|
|
14729
|
+
id: data.id,
|
|
14730
|
+
type: data.type,
|
|
14731
|
+
position,
|
|
14732
|
+
arguments: {
|
|
14733
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
14734
|
+
dataType: data.arguments.dataType,
|
|
14735
|
+
value: data.arguments.value,
|
|
14736
|
+
qos: Number(data.arguments.qos),
|
|
14737
|
+
retain: data.arguments.retain
|
|
14738
|
+
}
|
|
14739
|
+
};
|
|
14740
|
+
case "OnMqttMessage":
|
|
14741
|
+
return {
|
|
14742
|
+
id: data.id,
|
|
14743
|
+
type: data.type,
|
|
14744
|
+
position,
|
|
14745
|
+
arguments: {
|
|
14746
|
+
channelReference: data.arguments.channelReference ?? "",
|
|
14747
|
+
dataType: data.arguments.dataType,
|
|
14748
|
+
output: data.arguments.output
|
|
14749
|
+
}
|
|
14750
|
+
};
|
|
14751
|
+
}
|
|
14752
|
+
}
|
|
14352
14753
|
function deserialize6(apiNode, resolveFunctionInfo) {
|
|
14353
14754
|
return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
|
|
14354
14755
|
}
|
|
@@ -14587,6 +14988,43 @@ var MAIN_CANVAS_ID = "main";
|
|
|
14587
14988
|
|
|
14588
14989
|
// ../workflow-core/src/workflow/serialization.ts
|
|
14589
14990
|
var KNOWN_CHANNEL_TYPES = new Set(ALL_CHANNEL_TYPES);
|
|
14991
|
+
function serialize7(state) {
|
|
14992
|
+
const mainCanvas = state.canvases[MAIN_CANVAS_ID];
|
|
14993
|
+
if (!mainCanvas) {
|
|
14994
|
+
throw new Error("Main canvas missing");
|
|
14995
|
+
}
|
|
14996
|
+
const mainNodes = mainCanvas.nodes.map((n) => serialize6(n, isNodeUsedAsTool(n.id, n, mainCanvas.edges)));
|
|
14997
|
+
const mainEdges = mainCanvas.edges.map(serialize2);
|
|
14998
|
+
const mainDeclared = extractDeclaredVariables(mainCanvas.variables);
|
|
14999
|
+
const functions = [];
|
|
15000
|
+
for (const [id, decl] of Object.entries(state.functions)) {
|
|
15001
|
+
const body = state.canvases[id];
|
|
15002
|
+
if (!body) {
|
|
15003
|
+
throw new Error(`[workflow-core] function ${id} has no body canvas \u2014 cannot serialize`);
|
|
15004
|
+
}
|
|
15005
|
+
const { functionInfo, outputAssignments } = serialize(decl);
|
|
15006
|
+
functions.push({
|
|
15007
|
+
functionInfo,
|
|
15008
|
+
outputAssignments,
|
|
15009
|
+
nodes: body.nodes.map((n) => serialize6(n, isNodeUsedAsTool(n.id, n, body.edges))),
|
|
15010
|
+
edges: body.edges.map(serialize2),
|
|
15011
|
+
declaredVariables: extractDeclaredVariables(body.variables)
|
|
15012
|
+
});
|
|
15013
|
+
}
|
|
15014
|
+
const channels = Object.values(state.channels).map(serialize3);
|
|
15015
|
+
const memory = Object.values(state.memory).map(serialize4);
|
|
15016
|
+
const models = Object.values(state.models).map(serialize5);
|
|
15017
|
+
return {
|
|
15018
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
15019
|
+
nodes: mainNodes,
|
|
15020
|
+
edges: mainEdges,
|
|
15021
|
+
functions,
|
|
15022
|
+
declaredVariables: mainDeclared,
|
|
15023
|
+
channels,
|
|
15024
|
+
memory,
|
|
15025
|
+
models
|
|
15026
|
+
};
|
|
15027
|
+
}
|
|
14590
15028
|
function deserialize7(workflow) {
|
|
14591
15029
|
const canvases = {};
|
|
14592
15030
|
const functions = {};
|
|
@@ -14679,6 +15117,20 @@ function buildCanvasVariables(nodes, fnArgs, declaredVariables) {
|
|
|
14679
15117
|
}
|
|
14680
15118
|
return variables;
|
|
14681
15119
|
}
|
|
15120
|
+
function extractDeclaredVariables(variables) {
|
|
15121
|
+
const result = [];
|
|
15122
|
+
for (const v of Object.values(variables)) {
|
|
15123
|
+
if (v.kind === "declared") {
|
|
15124
|
+
result.push({
|
|
15125
|
+
uid: v.uid,
|
|
15126
|
+
name: v.name,
|
|
15127
|
+
dataType: v.dataType,
|
|
15128
|
+
...v.initialValue !== void 0 ? { initialValue: v.initialValue } : {}
|
|
15129
|
+
});
|
|
15130
|
+
}
|
|
15131
|
+
}
|
|
15132
|
+
return result;
|
|
15133
|
+
}
|
|
14682
15134
|
|
|
14683
15135
|
// ../workflow-core/src/expression/types.ts
|
|
14684
15136
|
function resolveExpression(apiExpr, availableVars) {
|
|
@@ -16595,7 +17047,11 @@ function pluralize(n) {
|
|
|
16595
17047
|
// cli/check-schema.ts
|
|
16596
17048
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
16597
17049
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
16598
|
-
import {
|
|
17050
|
+
import { promises as fs5 } from "node:fs";
|
|
17051
|
+
import path7 from "node:path";
|
|
17052
|
+
|
|
17053
|
+
// cli/contract.ts
|
|
17054
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16599
17055
|
import path6 from "node:path";
|
|
16600
17056
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16601
17057
|
|
|
@@ -18961,7 +19417,7 @@ var import_js_yaml = /* @__PURE__ */ __toESM2((/* @__PURE__ */ __commonJSMin(((e
|
|
|
18961
19417
|
var { Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, load, loadAll, dump, YAMLException, types, safeLoad, safeLoadAll, safeDump } = import_js_yaml.default;
|
|
18962
19418
|
var index_vite_proxy_tmp_default = import_js_yaml.default;
|
|
18963
19419
|
|
|
18964
|
-
// cli/
|
|
19420
|
+
// cli/contract.ts
|
|
18965
19421
|
var here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
18966
19422
|
var CONTRACT_DIR = existsSync(path6.join(here, "workflow.yaml")) ? here : path6.resolve(here, "../../../contract");
|
|
18967
19423
|
function loadContractDocument(fileName) {
|
|
@@ -18987,6 +19443,8 @@ function openApiToJsonSchema(value) {
|
|
|
18987
19443
|
}
|
|
18988
19444
|
return out;
|
|
18989
19445
|
}
|
|
19446
|
+
|
|
19447
|
+
// cli/check-schema.ts
|
|
18990
19448
|
var cachedValidator;
|
|
18991
19449
|
function compileWorkflowValidator() {
|
|
18992
19450
|
if (cachedValidator) return cachedValidator;
|
|
@@ -19011,7 +19469,7 @@ async function checkSchemaCommand(filePath) {
|
|
|
19011
19469
|
process.stderr.write("Usage: fh-workflow check-schema <file.json>\n");
|
|
19012
19470
|
process.exit(1);
|
|
19013
19471
|
}
|
|
19014
|
-
const abs =
|
|
19472
|
+
const abs = path7.resolve(process.cwd(), filePath);
|
|
19015
19473
|
let raw;
|
|
19016
19474
|
try {
|
|
19017
19475
|
raw = await fs5.readFile(abs, "utf-8");
|
|
@@ -19060,35 +19518,35 @@ function describeParams(e) {
|
|
|
19060
19518
|
return "";
|
|
19061
19519
|
}
|
|
19062
19520
|
|
|
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;
|
|
19521
|
+
// ../workflow-core/src/deploy/requirements.ts
|
|
19522
|
+
function getReferencedCatalogModelIds(workflow) {
|
|
19523
|
+
const declaredModel = new Set(Object.keys(workflow.models));
|
|
19524
|
+
const catalogIds = /* @__PURE__ */ new Set();
|
|
19525
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
19526
|
+
for (const node of canvas.nodes) {
|
|
19527
|
+
const def = NodeRegistry.getByType(node.type);
|
|
19528
|
+
if (!def) continue;
|
|
19529
|
+
const args2 = node.arguments;
|
|
19530
|
+
const isToolInput = isNodeUsedAsTool(node.id, node, canvas.edges);
|
|
19531
|
+
for (const param of def.parameters) {
|
|
19532
|
+
if (param.type !== "modelSelect") continue;
|
|
19533
|
+
if (!isParameterActive(param, args2, isToolInput)) continue;
|
|
19534
|
+
const id = args2[param.id];
|
|
19535
|
+
if (typeof id === "string" && id !== "" && !declaredModel.has(id)) {
|
|
19536
|
+
catalogIds.add(id);
|
|
19537
|
+
}
|
|
19538
|
+
}
|
|
19087
19539
|
}
|
|
19088
19540
|
}
|
|
19541
|
+
return [...catalogIds];
|
|
19542
|
+
}
|
|
19543
|
+
function assertNeverChannel(t) {
|
|
19544
|
+
throw new Error(`unhandled channel type: ${String(t)}`);
|
|
19545
|
+
}
|
|
19546
|
+
function deriveRequirements(workflow) {
|
|
19089
19547
|
const hardwareChannels = [];
|
|
19090
19548
|
const mqttChannels = [];
|
|
19091
|
-
for (const channel of workflow.channels) {
|
|
19549
|
+
for (const channel of Object.values(workflow.channels)) {
|
|
19092
19550
|
switch (channel.type) {
|
|
19093
19551
|
case "GPIOIN":
|
|
19094
19552
|
case "GPIOOUT":
|
|
@@ -19109,14 +19567,267 @@ function inspect(workflow) {
|
|
|
19109
19567
|
case "MQTT":
|
|
19110
19568
|
mqttChannels.push({ id: channel.id, label: channel.label });
|
|
19111
19569
|
break;
|
|
19570
|
+
case "LOG":
|
|
19571
|
+
break;
|
|
19112
19572
|
default:
|
|
19113
|
-
return
|
|
19573
|
+
return assertNeverChannel(channel.type);
|
|
19574
|
+
}
|
|
19575
|
+
}
|
|
19576
|
+
let hasRetriever = false;
|
|
19577
|
+
let hasWebSearch = false;
|
|
19578
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
19579
|
+
for (const node of canvas.nodes) {
|
|
19580
|
+
if (node.type === "Retriever") hasRetriever = true;
|
|
19581
|
+
else if (node.type === "WebSearchTool") hasWebSearch = true;
|
|
19114
19582
|
}
|
|
19115
19583
|
}
|
|
19116
|
-
const customModels = (workflow.models
|
|
19117
|
-
return {
|
|
19584
|
+
const customModels = Object.values(workflow.models).map((m) => ({ id: m.id, label: m.label }));
|
|
19585
|
+
return {
|
|
19586
|
+
hasProviderModel: getReferencedCatalogModelIds(workflow).length > 0,
|
|
19587
|
+
hasRetriever,
|
|
19588
|
+
hasWebSearch,
|
|
19589
|
+
hardwareChannels,
|
|
19590
|
+
mqttChannels,
|
|
19591
|
+
customModels
|
|
19592
|
+
};
|
|
19118
19593
|
}
|
|
19119
19594
|
|
|
19595
|
+
// ../workflow-core/src/deploy/spec.ts
|
|
19596
|
+
var ENGINE_MEMORY_PATH = "/var/lib/foresthub/memory";
|
|
19597
|
+
var RefAllocator = class {
|
|
19598
|
+
byKey = /* @__PURE__ */ new Map();
|
|
19599
|
+
used = /* @__PURE__ */ new Set();
|
|
19600
|
+
alloc(key, hint) {
|
|
19601
|
+
const existing = this.byKey.get(key);
|
|
19602
|
+
if (existing) return existing;
|
|
19603
|
+
let ref = hint;
|
|
19604
|
+
for (let n = 2; this.used.has(ref); n++) ref = `${hint}-${n}`;
|
|
19605
|
+
this.used.add(ref);
|
|
19606
|
+
this.byKey.set(key, ref);
|
|
19607
|
+
return ref;
|
|
19608
|
+
}
|
|
19609
|
+
};
|
|
19610
|
+
function basename(p) {
|
|
19611
|
+
const tail = p.replace(/\/+$/, "").split("/").pop() ?? p;
|
|
19612
|
+
return tail.replace(/[^A-Za-z0-9._:-]/g, "-") || "res";
|
|
19613
|
+
}
|
|
19614
|
+
function brokerHost(url2) {
|
|
19615
|
+
try {
|
|
19616
|
+
return new URL(url2).hostname || "mqtt";
|
|
19617
|
+
} catch {
|
|
19618
|
+
return "mqtt";
|
|
19619
|
+
}
|
|
19620
|
+
}
|
|
19621
|
+
function sidecarServiceName(modelId) {
|
|
19622
|
+
const slug = modelId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
19623
|
+
return `llama-${slug || "model"}`;
|
|
19624
|
+
}
|
|
19625
|
+
function ggufNameError(name) {
|
|
19626
|
+
const t = (name ?? "").trim();
|
|
19627
|
+
if (!t) return "a model filename is required";
|
|
19628
|
+
if (!t.toLowerCase().endsWith(".gguf")) return "must be a .gguf file (llama-server only loads GGUF)";
|
|
19629
|
+
if (t.includes("/")) return "just the filename, not a path \u2014 the file goes in ./models/";
|
|
19630
|
+
return null;
|
|
19631
|
+
}
|
|
19632
|
+
function hardwareAddressKey(family, chipOrDevice, index) {
|
|
19633
|
+
const dev = chipOrDevice.trim();
|
|
19634
|
+
return family === "serial" ? `serial:${dev}` : `${family}:${dev}:${index}`;
|
|
19635
|
+
}
|
|
19636
|
+
function hardwareAddressLabel(family, chipOrDevice, index) {
|
|
19637
|
+
const dev = chipOrDevice.trim();
|
|
19638
|
+
if (family === "serial") return dev;
|
|
19639
|
+
return `${dev} ${family === "gpio" ? "line" : "channel"} ${index}`;
|
|
19640
|
+
}
|
|
19641
|
+
function hardwareConflicts(channels, bindings) {
|
|
19642
|
+
const conflicts = [];
|
|
19643
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
19644
|
+
for (const ch of channels) {
|
|
19645
|
+
const b = bindings[ch.id];
|
|
19646
|
+
if (!b?.chipOrDevice || ch.addressable && b.index === void 0) continue;
|
|
19647
|
+
const key = hardwareAddressKey(ch.family, b.chipOrDevice, b.index);
|
|
19648
|
+
const holder = claimed.get(key);
|
|
19649
|
+
if (holder) {
|
|
19650
|
+
conflicts.push(`hardware "${ch.id}": ${hardwareAddressLabel(ch.family, b.chipOrDevice, b.index)} is already used by "${holder}"`);
|
|
19651
|
+
} else {
|
|
19652
|
+
claimed.set(key, ch.id);
|
|
19653
|
+
}
|
|
19654
|
+
}
|
|
19655
|
+
return conflicts;
|
|
19656
|
+
}
|
|
19657
|
+
function familyMismatches(channels, bindings) {
|
|
19658
|
+
const mismatches = [];
|
|
19659
|
+
for (const ch of channels) {
|
|
19660
|
+
const b = bindings[ch.id];
|
|
19661
|
+
if (!b) continue;
|
|
19662
|
+
if (ch.family !== "serial" && b.baud !== void 0) {
|
|
19663
|
+
mismatches.push(`hardware "${ch.id}": "baud" only applies to serial channels (this is a ${ch.family} channel)`);
|
|
19664
|
+
}
|
|
19665
|
+
if (ch.family === "serial" && b.index !== void 0) {
|
|
19666
|
+
mismatches.push(`hardware "${ch.id}": "index" does not apply to serial channels (the device path is the full address)`);
|
|
19667
|
+
}
|
|
19668
|
+
}
|
|
19669
|
+
return mismatches;
|
|
19670
|
+
}
|
|
19671
|
+
function assertNeverFamily(f) {
|
|
19672
|
+
throw new Error(`unhandled hardware family: ${String(f)}`);
|
|
19673
|
+
}
|
|
19674
|
+
function assertDeployable(req, inputs) {
|
|
19675
|
+
const missing = [];
|
|
19676
|
+
for (const ch of req.hardwareChannels) {
|
|
19677
|
+
const b = inputs.hardware[ch.id];
|
|
19678
|
+
if (!b?.chipOrDevice) missing.push(`hardware "${ch.id}": device path`);
|
|
19679
|
+
else if (ch.addressable && b.index === void 0) missing.push(`hardware "${ch.id}": index`);
|
|
19680
|
+
}
|
|
19681
|
+
for (const ch of req.mqttChannels) {
|
|
19682
|
+
if (!inputs.mqtt[ch.id]?.brokerUrl) missing.push(`mqtt "${ch.id}": broker URL`);
|
|
19683
|
+
}
|
|
19684
|
+
for (const m of req.customModels) {
|
|
19685
|
+
const b = inputs.models[m.id];
|
|
19686
|
+
if (b?.location === "device") {
|
|
19687
|
+
const err = ggufNameError(b.modelFile);
|
|
19688
|
+
if (err) missing.push(`model "${m.id}": ${err}`);
|
|
19689
|
+
} else if (!b?.url) {
|
|
19690
|
+
missing.push(`model "${m.id}": endpoint URL`);
|
|
19691
|
+
}
|
|
19692
|
+
}
|
|
19693
|
+
missing.push(...hardwareConflicts(req.hardwareChannels, inputs.hardware));
|
|
19694
|
+
missing.push(...familyMismatches(req.hardwareChannels, inputs.hardware));
|
|
19695
|
+
if (missing.length > 0) {
|
|
19696
|
+
throw new Error(`invalid deploy config:
|
|
19697
|
+
- ${missing.join("\n - ")}`);
|
|
19698
|
+
}
|
|
19699
|
+
}
|
|
19700
|
+
function assertNoNameCollisions(components) {
|
|
19701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19702
|
+
for (const c of components) {
|
|
19703
|
+
if (seen.has(c.name)) throw new Error(`duplicate component name "${c.name}"`);
|
|
19704
|
+
seen.add(c.name);
|
|
19705
|
+
}
|
|
19706
|
+
}
|
|
19707
|
+
function buildDeploymentSpec(workflow, inputs, meta3, customComponents = []) {
|
|
19708
|
+
const req = deriveRequirements(workflow);
|
|
19709
|
+
assertDeployable(req, inputs);
|
|
19710
|
+
const refs = new RefAllocator();
|
|
19711
|
+
const resourceSecrets = {};
|
|
19712
|
+
const gpios = {};
|
|
19713
|
+
const adcs = {};
|
|
19714
|
+
const dacs = {};
|
|
19715
|
+
const pwms = {};
|
|
19716
|
+
const serials = {};
|
|
19717
|
+
const externalResources = {};
|
|
19718
|
+
const mapping = {};
|
|
19719
|
+
const cdev = /* @__PURE__ */ new Set();
|
|
19720
|
+
let privileged = false;
|
|
19721
|
+
for (const ch of req.hardwareChannels) {
|
|
19722
|
+
const b = inputs.hardware[ch.id];
|
|
19723
|
+
if (!b) throw new Error(`unbound hardware channel ${ch.id}`);
|
|
19724
|
+
const dev = b.chipOrDevice;
|
|
19725
|
+
const ref = refs.alloc(`hw:${ch.family}:${dev}`, basename(dev));
|
|
19726
|
+
switch (ch.family) {
|
|
19727
|
+
case "gpio":
|
|
19728
|
+
gpios[ref] = { chip: dev };
|
|
19729
|
+
cdev.add(dev);
|
|
19730
|
+
break;
|
|
19731
|
+
case "serial":
|
|
19732
|
+
serials[ref] = b.baud ? { device: dev, baud: b.baud } : { device: dev };
|
|
19733
|
+
cdev.add(dev);
|
|
19734
|
+
break;
|
|
19735
|
+
case "pwm":
|
|
19736
|
+
pwms[ref] = { chip: dev };
|
|
19737
|
+
privileged = true;
|
|
19738
|
+
break;
|
|
19739
|
+
case "adc":
|
|
19740
|
+
adcs[ref] = { device: dev };
|
|
19741
|
+
privileged = true;
|
|
19742
|
+
break;
|
|
19743
|
+
case "dac":
|
|
19744
|
+
dacs[ref] = { device: dev };
|
|
19745
|
+
privileged = true;
|
|
19746
|
+
break;
|
|
19747
|
+
default:
|
|
19748
|
+
return assertNeverFamily(ch.family);
|
|
19749
|
+
}
|
|
19750
|
+
if (ch.addressable && b.index !== void 0) mapping[ch.id] = { ref, index: b.index };
|
|
19751
|
+
else mapping[ch.id] = { ref };
|
|
19752
|
+
}
|
|
19753
|
+
for (const ch of req.mqttChannels) {
|
|
19754
|
+
const b = inputs.mqtt[ch.id];
|
|
19755
|
+
if (!b) throw new Error(`unbound mqtt channel ${ch.id}`);
|
|
19756
|
+
const conn = { type: "mqtt", brokerUrl: b.brokerUrl };
|
|
19757
|
+
if (b.username) conn.username = b.username;
|
|
19758
|
+
const ref = refs.alloc(`mqtt:${JSON.stringify(conn)}:${b.password ?? ""}`, `mqtt-${brokerHost(b.brokerUrl)}`);
|
|
19759
|
+
externalResources[ref] = conn;
|
|
19760
|
+
mapping[ch.id] = { ref };
|
|
19761
|
+
if (b.password) resourceSecrets[ref] = { ...resourceSecrets[ref], password: b.password };
|
|
19762
|
+
}
|
|
19763
|
+
const llamaComponents = [];
|
|
19764
|
+
for (const m of req.customModels) {
|
|
19765
|
+
const b = inputs.models[m.id];
|
|
19766
|
+
if (!b) throw new Error(`unbound model ${m.id}`);
|
|
19767
|
+
const ref = refs.alloc(`model:${m.id}`, basename(m.id));
|
|
19768
|
+
mapping[m.id] = { ref };
|
|
19769
|
+
if (b.location === "device") {
|
|
19770
|
+
const port = b.port ?? 8080;
|
|
19771
|
+
const service = sidecarServiceName(m.id);
|
|
19772
|
+
externalResources[ref] = { type: "selfhosted", url: `http://${service}:${port}` };
|
|
19773
|
+
llamaComponents.push({
|
|
19774
|
+
name: service,
|
|
19775
|
+
image: meta3.llamaServerImage,
|
|
19776
|
+
command: [
|
|
19777
|
+
"--model",
|
|
19778
|
+
`/models/${b.modelFile}`,
|
|
19779
|
+
"--host",
|
|
19780
|
+
"0.0.0.0",
|
|
19781
|
+
"--port",
|
|
19782
|
+
String(port),
|
|
19783
|
+
"--ctx-size",
|
|
19784
|
+
String(b.ctxSize ?? 4096)
|
|
19785
|
+
],
|
|
19786
|
+
volumes: ["./models:/models:ro"]
|
|
19787
|
+
});
|
|
19788
|
+
} else {
|
|
19789
|
+
externalResources[ref] = { type: "selfhosted", url: b.url };
|
|
19790
|
+
if (b.apiKey) resourceSecrets[ref] = { apiKey: b.apiKey };
|
|
19791
|
+
}
|
|
19792
|
+
}
|
|
19793
|
+
const manifest = {};
|
|
19794
|
+
if (Object.keys(gpios).length) manifest.gpios = gpios;
|
|
19795
|
+
if (Object.keys(adcs).length) manifest.adcs = adcs;
|
|
19796
|
+
if (Object.keys(dacs).length) manifest.dacs = dacs;
|
|
19797
|
+
if (Object.keys(pwms).length) manifest.pwms = pwms;
|
|
19798
|
+
if (Object.keys(serials).length) manifest.serials = serials;
|
|
19799
|
+
const config2 = { workflow: serialize7(workflow) };
|
|
19800
|
+
if (Object.keys(mapping).length) config2.mapping = mapping;
|
|
19801
|
+
if (Object.keys(externalResources).length) config2.externalResources = externalResources;
|
|
19802
|
+
if (Object.keys(manifest).length) config2.manifest = manifest;
|
|
19803
|
+
const engine = {
|
|
19804
|
+
name: "engine",
|
|
19805
|
+
image: meta3.engineImage,
|
|
19806
|
+
pull: "never",
|
|
19807
|
+
// built locally before deploy, in no registry
|
|
19808
|
+
config: config2,
|
|
19809
|
+
volumes: [`engine-memory:${ENGINE_MEMORY_PATH}`]
|
|
19810
|
+
};
|
|
19811
|
+
if (cdev.size > 0) engine.devices = [...cdev];
|
|
19812
|
+
if (privileged) engine.privileged = true;
|
|
19813
|
+
if (cdev.size > 0 || privileged) engine.user = "0:0";
|
|
19814
|
+
const components = [engine, ...llamaComponents, ...customComponents];
|
|
19815
|
+
assertNoNameCollisions(components);
|
|
19816
|
+
const spec = {
|
|
19817
|
+
schemaVersion: 1,
|
|
19818
|
+
id: meta3.id,
|
|
19819
|
+
status: meta3.status,
|
|
19820
|
+
components
|
|
19821
|
+
};
|
|
19822
|
+
if (meta3.createdAt) spec.createdAt = meta3.createdAt;
|
|
19823
|
+
return { spec, resourceSecrets };
|
|
19824
|
+
}
|
|
19825
|
+
|
|
19826
|
+
// cli/deploy/index.ts
|
|
19827
|
+
import { promises as fs9 } from "node:fs";
|
|
19828
|
+
import path12 from "node:path";
|
|
19829
|
+
import { parseArgs } from "node:util";
|
|
19830
|
+
|
|
19120
19831
|
// ../node_modules/@inquirer/core/dist/lib/key.js
|
|
19121
19832
|
var keybindings = ["emacs", "vim"];
|
|
19122
19833
|
var keybindingLookup = new Set(keybindings);
|
|
@@ -20520,7 +21231,7 @@ var PromisePolyfill = class extends Promise {
|
|
|
20520
21231
|
};
|
|
20521
21232
|
|
|
20522
21233
|
// ../node_modules/@inquirer/core/dist/lib/create-prompt.js
|
|
20523
|
-
import
|
|
21234
|
+
import path8 from "node:path";
|
|
20524
21235
|
var nativeSetImmediate = globalThis.setImmediate;
|
|
20525
21236
|
function getCallSites() {
|
|
20526
21237
|
const savedPrepareStackTrace = Error.prepareStackTrace;
|
|
@@ -20591,7 +21302,7 @@ function createPrompt(view) {
|
|
|
20591
21302
|
if (nextView === void 0) {
|
|
20592
21303
|
let callerFilename = callSites[1]?.getFileName();
|
|
20593
21304
|
if (callerFilename && !callerFilename.startsWith("file://")) {
|
|
20594
|
-
callerFilename =
|
|
21305
|
+
callerFilename = path8.resolve(callerFilename);
|
|
20595
21306
|
}
|
|
20596
21307
|
throw new Error(`Prompt functions must return a string.
|
|
20597
21308
|
at ${callerFilename}`);
|
|
@@ -20836,11 +21547,57 @@ var dist_default4 = createPrompt((config2, done) => {
|
|
|
20836
21547
|
return `${lines}${cursorHide}`;
|
|
20837
21548
|
});
|
|
20838
21549
|
|
|
21550
|
+
// ../node_modules/@inquirer/confirm/dist/index.js
|
|
21551
|
+
function getBooleanValue(value, defaultValue) {
|
|
21552
|
+
let answer = defaultValue !== false;
|
|
21553
|
+
if (/^(y|yes)/i.test(value))
|
|
21554
|
+
answer = true;
|
|
21555
|
+
else if (/^(n|no)/i.test(value))
|
|
21556
|
+
answer = false;
|
|
21557
|
+
return answer;
|
|
21558
|
+
}
|
|
21559
|
+
function boolToString(value) {
|
|
21560
|
+
return value ? "Yes" : "No";
|
|
21561
|
+
}
|
|
21562
|
+
var dist_default5 = createPrompt((config2, done) => {
|
|
21563
|
+
const { transformer = boolToString } = config2;
|
|
21564
|
+
const [status, setStatus] = useState("idle");
|
|
21565
|
+
const [value, setValue] = useState("");
|
|
21566
|
+
const theme = makeTheme(config2.theme);
|
|
21567
|
+
const prefix = usePrefix({ status, theme });
|
|
21568
|
+
useKeypress((key, rl) => {
|
|
21569
|
+
if (status !== "idle")
|
|
21570
|
+
return;
|
|
21571
|
+
if (isEnterKey(key)) {
|
|
21572
|
+
const answer = getBooleanValue(value, config2.default);
|
|
21573
|
+
setValue(transformer(answer));
|
|
21574
|
+
setStatus("done");
|
|
21575
|
+
done(answer);
|
|
21576
|
+
} else if (isTabKey(key)) {
|
|
21577
|
+
const answer = boolToString(!getBooleanValue(value, config2.default));
|
|
21578
|
+
rl.clearLine(0);
|
|
21579
|
+
rl.write(answer);
|
|
21580
|
+
setValue(answer);
|
|
21581
|
+
} else {
|
|
21582
|
+
setValue(rl.line);
|
|
21583
|
+
}
|
|
21584
|
+
});
|
|
21585
|
+
let formattedValue = value;
|
|
21586
|
+
let defaultValue = "";
|
|
21587
|
+
if (status === "done") {
|
|
21588
|
+
formattedValue = theme.style.answer(value);
|
|
21589
|
+
} else {
|
|
21590
|
+
defaultValue = ` ${theme.style.defaultAnswer(config2.default === false ? "y/N" : "Y/n")}`;
|
|
21591
|
+
}
|
|
21592
|
+
const message = theme.style.message(config2.message, status);
|
|
21593
|
+
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
|
|
21594
|
+
});
|
|
21595
|
+
|
|
20839
21596
|
// ../node_modules/@inquirer/input/dist/index.js
|
|
20840
21597
|
var inputTheme = {
|
|
20841
21598
|
validationFailureMode: "keep"
|
|
20842
21599
|
};
|
|
20843
|
-
var
|
|
21600
|
+
var dist_default6 = createPrompt((config2, done) => {
|
|
20844
21601
|
const { prefill = "tab" } = config2;
|
|
20845
21602
|
const theme = makeTheme(inputTheme, config2.theme);
|
|
20846
21603
|
const [status, setStatus] = useState("idle");
|
|
@@ -20927,7 +21684,7 @@ var passwordTheme = {
|
|
|
20927
21684
|
maskedText: "[input is masked]"
|
|
20928
21685
|
}
|
|
20929
21686
|
};
|
|
20930
|
-
var
|
|
21687
|
+
var dist_default7 = createPrompt((config2, done) => {
|
|
20931
21688
|
const { validate = () => true } = config2;
|
|
20932
21689
|
const theme = makeTheme(passwordTheme, config2.theme);
|
|
20933
21690
|
const [status, setStatus] = useState("idle");
|
|
@@ -21019,7 +21776,7 @@ function normalizeChoices2(choices) {
|
|
|
21019
21776
|
return normalizedChoice;
|
|
21020
21777
|
});
|
|
21021
21778
|
}
|
|
21022
|
-
var
|
|
21779
|
+
var dist_default8 = createPrompt((config2, done) => {
|
|
21023
21780
|
const { loop = true, pageSize = 7 } = config2;
|
|
21024
21781
|
const theme = makeTheme(selectTheme, config2.theme);
|
|
21025
21782
|
const { keybindings: keybindings2 } = theme;
|
|
@@ -21148,8 +21905,145 @@ var dist_default7 = createPrompt((config2, done) => {
|
|
|
21148
21905
|
});
|
|
21149
21906
|
|
|
21150
21907
|
// cli/deploy/prompts.ts
|
|
21908
|
+
import { existsSync as existsSync3, promises as fs7 } from "node:fs";
|
|
21909
|
+
import path10 from "node:path";
|
|
21910
|
+
|
|
21911
|
+
// cli/deploy/components.ts
|
|
21151
21912
|
import { existsSync as existsSync2, promises as fs6 } from "node:fs";
|
|
21152
|
-
import
|
|
21913
|
+
import path9 from "node:path";
|
|
21914
|
+
var import_ajv2 = __toESM(require_ajv(), 1);
|
|
21915
|
+
var import_ajv_formats2 = __toESM(require_dist(), 1);
|
|
21916
|
+
async function readComponentJson(dir) {
|
|
21917
|
+
const file2 = path9.join(dir, "component.json");
|
|
21918
|
+
let raw;
|
|
21919
|
+
try {
|
|
21920
|
+
raw = await fs6.readFile(file2, "utf-8");
|
|
21921
|
+
} catch (err) {
|
|
21922
|
+
if (err.code === "ENOENT") {
|
|
21923
|
+
throw new Error(`custom component: no component.json in ${dir}`, { cause: err });
|
|
21924
|
+
}
|
|
21925
|
+
throw err;
|
|
21926
|
+
}
|
|
21927
|
+
try {
|
|
21928
|
+
return JSON.parse(raw);
|
|
21929
|
+
} catch (err) {
|
|
21930
|
+
throw new Error(`${file2}: invalid JSON: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
21931
|
+
}
|
|
21932
|
+
}
|
|
21933
|
+
var cachedValidator2;
|
|
21934
|
+
function compileDeployComponentValidator() {
|
|
21935
|
+
if (cachedValidator2) return cachedValidator2;
|
|
21936
|
+
const deployment = openApiToJsonSchema(loadContractDocument("deployment.yaml"));
|
|
21937
|
+
deployment.$id = "deployment.yaml";
|
|
21938
|
+
const schemas = deployment.components.schemas;
|
|
21939
|
+
schemas.DeployComponent.additionalProperties = false;
|
|
21940
|
+
const ajv = new import_ajv2.default({ strict: false, allErrors: true });
|
|
21941
|
+
(0, import_ajv_formats2.default)(ajv);
|
|
21942
|
+
ajv.addSchema(deployment);
|
|
21943
|
+
cachedValidator2 = ajv.compile({ $ref: "deployment.yaml#/components/schemas/DeployComponent" });
|
|
21944
|
+
return cachedValidator2;
|
|
21945
|
+
}
|
|
21946
|
+
function validateComponent(data) {
|
|
21947
|
+
const validate = compileDeployComponentValidator();
|
|
21948
|
+
if (validate(data)) return [];
|
|
21949
|
+
return (validate.errors ?? []).map((e) => {
|
|
21950
|
+
const p = e.params;
|
|
21951
|
+
const which = p.additionalProperty ?? p.missingProperty;
|
|
21952
|
+
const detail = typeof which === "string" ? ` "${which}"` : "";
|
|
21953
|
+
return `${e.instancePath || "/"} ${e.message ?? "invalid"}${detail}`;
|
|
21954
|
+
});
|
|
21955
|
+
}
|
|
21956
|
+
function parseDeployComponents(raw) {
|
|
21957
|
+
const errors = [];
|
|
21958
|
+
for (const { source, data } of raw) {
|
|
21959
|
+
for (const message of validateComponent(data)) errors.push(`${source}: ${message}`);
|
|
21960
|
+
}
|
|
21961
|
+
if (errors.length > 0) {
|
|
21962
|
+
throw new Error(`invalid custom component(s):
|
|
21963
|
+
- ${errors.join("\n - ")}`);
|
|
21964
|
+
}
|
|
21965
|
+
return raw.map((r) => r.data);
|
|
21966
|
+
}
|
|
21967
|
+
function parseEnvExample(text) {
|
|
21968
|
+
return text.split("\n").map((line) => {
|
|
21969
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
21970
|
+
return m ? { kind: "kv", key: m[1] ?? "", value: m[2] ?? "" } : { kind: "comment", text: line };
|
|
21971
|
+
});
|
|
21972
|
+
}
|
|
21973
|
+
var SECRET_HINT = /password|secret|key|token/i;
|
|
21974
|
+
async function resolveComponentEnv(dir, name, opts) {
|
|
21975
|
+
const file2 = path9.join(dir, `${name}.env.example`);
|
|
21976
|
+
let raw;
|
|
21977
|
+
try {
|
|
21978
|
+
raw = await fs6.readFile(file2, "utf-8");
|
|
21979
|
+
} catch (err) {
|
|
21980
|
+
if (err.code === "ENOENT") return null;
|
|
21981
|
+
throw err;
|
|
21982
|
+
}
|
|
21983
|
+
const lines = [];
|
|
21984
|
+
for (const e of parseEnvExample(raw)) {
|
|
21985
|
+
if (e.kind === "comment") {
|
|
21986
|
+
lines.push(e.text);
|
|
21987
|
+
} else if (e.value !== "" || !opts.interactive) {
|
|
21988
|
+
lines.push(`${e.key}=${e.value}`);
|
|
21989
|
+
} else {
|
|
21990
|
+
const value = SECRET_HINT.test(e.key) ? await dist_default7({ message: `${name}: ${e.key}`, mask: "*" }) : await dist_default6({ message: `${name}: ${e.key}` });
|
|
21991
|
+
lines.push(`${e.key}=${value}`);
|
|
21992
|
+
}
|
|
21993
|
+
}
|
|
21994
|
+
const header = `# Auto-generated by \`fh-workflow deploy\` from ${name}.env.example.
|
|
21995
|
+
# Loaded into the ${name} container via compose env_file; chmod 600 if it holds
|
|
21996
|
+
# secrets, and fill any empty values before \`up\`.
|
|
21997
|
+
`;
|
|
21998
|
+
return `${header}
|
|
21999
|
+
${lines.join("\n")}
|
|
22000
|
+
`;
|
|
22001
|
+
}
|
|
22002
|
+
async function promptCustomComponents(preloaded) {
|
|
22003
|
+
const components = [];
|
|
22004
|
+
const env = {};
|
|
22005
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22006
|
+
const add = async (loaded) => {
|
|
22007
|
+
seen.add(loaded.component.name);
|
|
22008
|
+
components.push(loaded.component);
|
|
22009
|
+
const text = await resolveComponentEnv(loaded.dir, loaded.component.name, { interactive: true });
|
|
22010
|
+
if (text !== null) env[loaded.component.name] = text;
|
|
22011
|
+
};
|
|
22012
|
+
if (preloaded.length > 0) {
|
|
22013
|
+
process.stdout.write(` Already added: ${preloaded.map((c) => c.component.name).join(", ")}
|
|
22014
|
+
`);
|
|
22015
|
+
}
|
|
22016
|
+
for (const loaded of preloaded) await add(loaded);
|
|
22017
|
+
while (await dist_default5({
|
|
22018
|
+
message: components.length === 0 ? "Add a custom component?" : "Add another custom component?",
|
|
22019
|
+
default: false
|
|
22020
|
+
})) {
|
|
22021
|
+
const dir = (await dist_default6({
|
|
22022
|
+
message: "Custom component folder",
|
|
22023
|
+
// Full inline validation: path, component.json presence, contract shape,
|
|
22024
|
+
// and a name not already taken — the same checks the batch path runs, so
|
|
22025
|
+
// a bad folder is rejected here instead of failing late in the build.
|
|
22026
|
+
validate: async (v) => {
|
|
22027
|
+
const t = v.trim();
|
|
22028
|
+
if (!t) return "enter a folder path";
|
|
22029
|
+
if (!existsSync2(t)) return `folder not found: ${t}`;
|
|
22030
|
+
if (!existsSync2(path9.join(t, "component.json"))) return `no component.json in ${t}`;
|
|
22031
|
+
let data;
|
|
22032
|
+
try {
|
|
22033
|
+
data = await readComponentJson(t);
|
|
22034
|
+
} catch (err) {
|
|
22035
|
+
return err instanceof Error ? err.message : String(err);
|
|
22036
|
+
}
|
|
22037
|
+
const errors = validateComponent(data);
|
|
22038
|
+
if (errors.length > 0) return errors.join("; ");
|
|
22039
|
+
const name = data.name;
|
|
22040
|
+
return seen.has(name) ? `"${name}" already added` : true;
|
|
22041
|
+
}
|
|
22042
|
+
})).trim();
|
|
22043
|
+
await add({ component: await readComponentJson(dir), dir });
|
|
22044
|
+
}
|
|
22045
|
+
return { components, env };
|
|
22046
|
+
}
|
|
21153
22047
|
|
|
21154
22048
|
// ../node_modules/zod/v4/classic/external.js
|
|
21155
22049
|
var external_exports = {};
|
|
@@ -21761,7 +22655,7 @@ __export(util_exports, {
|
|
|
21761
22655
|
assert: () => assert,
|
|
21762
22656
|
assertEqual: () => assertEqual,
|
|
21763
22657
|
assertIs: () => assertIs,
|
|
21764
|
-
assertNever: () =>
|
|
22658
|
+
assertNever: () => assertNever,
|
|
21765
22659
|
assertNotEqual: () => assertNotEqual,
|
|
21766
22660
|
assignProp: () => assignProp,
|
|
21767
22661
|
base64ToUint8Array: () => base64ToUint8Array,
|
|
@@ -21825,7 +22719,7 @@ function assertNotEqual(val) {
|
|
|
21825
22719
|
}
|
|
21826
22720
|
function assertIs(_arg) {
|
|
21827
22721
|
}
|
|
21828
|
-
function
|
|
22722
|
+
function assertNever(_x) {
|
|
21829
22723
|
throw new Error("Unexpected value in exhaustive check");
|
|
21830
22724
|
}
|
|
21831
22725
|
function assert(_) {
|
|
@@ -21917,10 +22811,10 @@ function mergeDefs(...defs) {
|
|
|
21917
22811
|
function cloneDef(schema) {
|
|
21918
22812
|
return mergeDefs(schema._zod.def);
|
|
21919
22813
|
}
|
|
21920
|
-
function getElementAtPath(obj,
|
|
21921
|
-
if (!
|
|
22814
|
+
function getElementAtPath(obj, path13) {
|
|
22815
|
+
if (!path13)
|
|
21922
22816
|
return obj;
|
|
21923
|
-
return
|
|
22817
|
+
return path13.reduce((acc, key) => acc?.[key], obj);
|
|
21924
22818
|
}
|
|
21925
22819
|
function promiseAllObject(promisesObj) {
|
|
21926
22820
|
const keys = Object.keys(promisesObj);
|
|
@@ -22329,11 +23223,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
22329
23223
|
}
|
|
22330
23224
|
return false;
|
|
22331
23225
|
}
|
|
22332
|
-
function prefixIssues(
|
|
23226
|
+
function prefixIssues(path13, issues) {
|
|
22333
23227
|
return issues.map((iss) => {
|
|
22334
23228
|
var _a3;
|
|
22335
23229
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
22336
|
-
iss.path.unshift(
|
|
23230
|
+
iss.path.unshift(path13);
|
|
22337
23231
|
return iss;
|
|
22338
23232
|
});
|
|
22339
23233
|
}
|
|
@@ -22480,16 +23374,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22480
23374
|
}
|
|
22481
23375
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
22482
23376
|
const fieldErrors = { _errors: [] };
|
|
22483
|
-
const processError = (error52,
|
|
23377
|
+
const processError = (error52, path13 = []) => {
|
|
22484
23378
|
for (const issue2 of error52.issues) {
|
|
22485
23379
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
22486
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
23380
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
22487
23381
|
} else if (issue2.code === "invalid_key") {
|
|
22488
|
-
processError({ issues: issue2.issues }, [...
|
|
23382
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22489
23383
|
} else if (issue2.code === "invalid_element") {
|
|
22490
|
-
processError({ issues: issue2.issues }, [...
|
|
23384
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22491
23385
|
} else {
|
|
22492
|
-
const fullpath = [...
|
|
23386
|
+
const fullpath = [...path13, ...issue2.path];
|
|
22493
23387
|
if (fullpath.length === 0) {
|
|
22494
23388
|
fieldErrors._errors.push(mapper(issue2));
|
|
22495
23389
|
} else {
|
|
@@ -22516,17 +23410,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22516
23410
|
}
|
|
22517
23411
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
22518
23412
|
const result = { errors: [] };
|
|
22519
|
-
const processError = (error52,
|
|
23413
|
+
const processError = (error52, path13 = []) => {
|
|
22520
23414
|
var _a3, _b;
|
|
22521
23415
|
for (const issue2 of error52.issues) {
|
|
22522
23416
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
22523
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
23417
|
+
issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
|
|
22524
23418
|
} else if (issue2.code === "invalid_key") {
|
|
22525
|
-
processError({ issues: issue2.issues }, [...
|
|
23419
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22526
23420
|
} else if (issue2.code === "invalid_element") {
|
|
22527
|
-
processError({ issues: issue2.issues }, [...
|
|
23421
|
+
processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
|
|
22528
23422
|
} else {
|
|
22529
|
-
const fullpath = [...
|
|
23423
|
+
const fullpath = [...path13, ...issue2.path];
|
|
22530
23424
|
if (fullpath.length === 0) {
|
|
22531
23425
|
result.errors.push(mapper(issue2));
|
|
22532
23426
|
continue;
|
|
@@ -22558,8 +23452,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
22558
23452
|
}
|
|
22559
23453
|
function toDotPath(_path) {
|
|
22560
23454
|
const segs = [];
|
|
22561
|
-
const
|
|
22562
|
-
for (const seg of
|
|
23455
|
+
const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
23456
|
+
for (const seg of path13) {
|
|
22563
23457
|
if (typeof seg === "number")
|
|
22564
23458
|
segs.push(`[${seg}]`);
|
|
22565
23459
|
else if (typeof seg === "symbol")
|
|
@@ -35251,13 +36145,13 @@ function resolveRef(ref, ctx) {
|
|
|
35251
36145
|
if (!ref.startsWith("#")) {
|
|
35252
36146
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
35253
36147
|
}
|
|
35254
|
-
const
|
|
35255
|
-
if (
|
|
36148
|
+
const path13 = ref.slice(1).split("/").filter(Boolean);
|
|
36149
|
+
if (path13.length === 0) {
|
|
35256
36150
|
return ctx.rootSchema;
|
|
35257
36151
|
}
|
|
35258
36152
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
35259
|
-
if (
|
|
35260
|
-
const key =
|
|
36153
|
+
if (path13[0] === defsKey) {
|
|
36154
|
+
const key = path13[1];
|
|
35261
36155
|
if (!key || !ctx.defs[key]) {
|
|
35262
36156
|
throw new Error(`Reference not found: ${ref}`);
|
|
35263
36157
|
}
|
|
@@ -35679,55 +36573,18 @@ var mqttBindingSchema = external_exports.strictObject({
|
|
|
35679
36573
|
password: external_exports.string().optional()
|
|
35680
36574
|
});
|
|
35681
36575
|
var modelBindingSchema = external_exports.discriminatedUnion("location", [
|
|
35682
|
-
external_exports.strictObject({
|
|
36576
|
+
external_exports.strictObject({
|
|
36577
|
+
location: external_exports.literal("device"),
|
|
36578
|
+
modelFile: external_exports.string(),
|
|
36579
|
+
port: external_exports.number().int().positive().optional(),
|
|
36580
|
+
ctxSize: external_exports.number().int().positive().optional()
|
|
36581
|
+
}),
|
|
35683
36582
|
external_exports.strictObject({ location: external_exports.literal("network"), url: external_exports.string(), apiKey: external_exports.string().optional() })
|
|
35684
36583
|
]);
|
|
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
|
-
}
|
|
36584
|
+
var webSearchBindingSchema = external_exports.strictObject({
|
|
36585
|
+
provider: external_exports.string(),
|
|
36586
|
+
apiKey: external_exports.string()
|
|
36587
|
+
});
|
|
35731
36588
|
function unknownIds(req, p) {
|
|
35732
36589
|
const unknown2 = [];
|
|
35733
36590
|
const check3 = (kind, ids, bindings) => {
|
|
@@ -35741,10 +36598,6 @@ function unknownIds(req, p) {
|
|
|
35741
36598
|
check3("model", req.customModels.map((m) => m.id), p.models);
|
|
35742
36599
|
return unknown2;
|
|
35743
36600
|
}
|
|
35744
|
-
var webSearchBindingSchema = external_exports.strictObject({
|
|
35745
|
-
provider: external_exports.string(),
|
|
35746
|
-
apiKey: external_exports.string()
|
|
35747
|
-
});
|
|
35748
36601
|
var deployConfigSchema = external_exports.strictObject({
|
|
35749
36602
|
llmKeys: external_exports.partialRecord(external_exports.enum(ALL_PROVIDERS), external_exports.string()),
|
|
35750
36603
|
outputDir: external_exports.string(),
|
|
@@ -35780,7 +36633,7 @@ async function promptHardware(channels, seed) {
|
|
|
35780
36633
|
}
|
|
35781
36634
|
for (const ch of channels) {
|
|
35782
36635
|
if (result[ch.id]) continue;
|
|
35783
|
-
const chipOrDevice = await
|
|
36636
|
+
const chipOrDevice = await dist_default6({
|
|
35784
36637
|
message: `${ch.label} (${ch.family}): device path`,
|
|
35785
36638
|
default: HARDWARE_EXAMPLE[ch.family],
|
|
35786
36639
|
// Only serial claims the whole device; gpio/adc/dac/pwm share the path
|
|
@@ -35794,7 +36647,7 @@ async function promptHardware(channels, seed) {
|
|
|
35794
36647
|
const dev = chipOrDevice.trim();
|
|
35795
36648
|
const binding = { chipOrDevice: dev };
|
|
35796
36649
|
if (ch.addressable) {
|
|
35797
|
-
const idx = await
|
|
36650
|
+
const idx = await dist_default6({
|
|
35798
36651
|
message: `${ch.label}: channel index / GPIO line`,
|
|
35799
36652
|
validate: (v) => {
|
|
35800
36653
|
const uint = isUint(v);
|
|
@@ -35806,7 +36659,7 @@ async function promptHardware(channels, seed) {
|
|
|
35806
36659
|
binding.index = Number(idx.trim());
|
|
35807
36660
|
}
|
|
35808
36661
|
if (ch.family === "serial") {
|
|
35809
|
-
const baud = await
|
|
36662
|
+
const baud = await dist_default6({ message: `${ch.label}: baud rate`, default: "115200", validate: isUint });
|
|
35810
36663
|
binding.baud = Number(baud.trim());
|
|
35811
36664
|
}
|
|
35812
36665
|
result[ch.id] = binding;
|
|
@@ -35821,7 +36674,7 @@ async function promptMqtt(channels, seed) {
|
|
|
35821
36674
|
const brokers = /* @__PURE__ */ new Map();
|
|
35822
36675
|
for (const b of Object.values(result)) brokers.set(b.brokerUrl, b);
|
|
35823
36676
|
if (brokers.size > 0) {
|
|
35824
|
-
const choice = await
|
|
36677
|
+
const choice = await dist_default8({
|
|
35825
36678
|
message: `${ch.label}: MQTT broker`,
|
|
35826
36679
|
choices: [
|
|
35827
36680
|
...[...brokers.keys()].map((url2) => ({ value: url2, name: `reuse ${url2}` })),
|
|
@@ -35834,13 +36687,13 @@ async function promptMqtt(channels, seed) {
|
|
|
35834
36687
|
continue;
|
|
35835
36688
|
}
|
|
35836
36689
|
}
|
|
35837
|
-
const brokerUrl = await
|
|
36690
|
+
const brokerUrl = await dist_default6({
|
|
35838
36691
|
message: `${ch.label}: broker URL`,
|
|
35839
36692
|
default: "tcp://localhost:1883",
|
|
35840
36693
|
validate: (v) => v.trim().length > 0 || "broker URL is required"
|
|
35841
36694
|
});
|
|
35842
|
-
const username = (await
|
|
35843
|
-
const pass = await
|
|
36695
|
+
const username = (await dist_default6({ message: `${ch.label}: username (optional)` })).trim();
|
|
36696
|
+
const pass = await dist_default7({ message: `${ch.label}: password (optional)`, mask: "*" });
|
|
35844
36697
|
const binding = { brokerUrl: brokerUrl.trim() };
|
|
35845
36698
|
if (username) binding.username = username;
|
|
35846
36699
|
if (pass) binding.password = pass;
|
|
@@ -35852,7 +36705,7 @@ async function promptModels(models, seed) {
|
|
|
35852
36705
|
const result = { ...seed };
|
|
35853
36706
|
for (const m of models) {
|
|
35854
36707
|
if (result[m.id]) continue;
|
|
35855
|
-
const location = await
|
|
36708
|
+
const location = await dist_default8({
|
|
35856
36709
|
message: `${m.label}: where does this model run?`,
|
|
35857
36710
|
choices: [
|
|
35858
36711
|
{ value: "device", name: "on this device (generate a llama-server container)" },
|
|
@@ -35860,19 +36713,26 @@ async function promptModels(models, seed) {
|
|
|
35860
36713
|
]
|
|
35861
36714
|
});
|
|
35862
36715
|
if (location === "device") {
|
|
35863
|
-
const modelFile = await
|
|
36716
|
+
const modelFile = await dist_default6({
|
|
35864
36717
|
message: `${m.label}: model filename in ./models/ (e.g. model.gguf)`,
|
|
35865
36718
|
validate: (v) => ggufNameError(v) ?? true
|
|
35866
36719
|
});
|
|
35867
|
-
|
|
36720
|
+
const ctxSize = await dist_default6({ message: `${m.label}: context window in tokens`, default: "4096", validate: isUint });
|
|
36721
|
+
const port = await dist_default6({ message: `${m.label}: sidecar port`, default: "8080", validate: isUint });
|
|
36722
|
+
result[m.id] = {
|
|
36723
|
+
location: "device",
|
|
36724
|
+
modelFile: modelFile.trim(),
|
|
36725
|
+
ctxSize: Number(ctxSize.trim()),
|
|
36726
|
+
port: Number(port.trim())
|
|
36727
|
+
};
|
|
35868
36728
|
continue;
|
|
35869
36729
|
}
|
|
35870
|
-
const url2 = await
|
|
36730
|
+
const url2 = await dist_default6({
|
|
35871
36731
|
message: `${m.label}: inference endpoint URL (a server you run \u2014 llama-server/vLLM/Ollama)`,
|
|
35872
36732
|
default: "http://localhost:8080",
|
|
35873
36733
|
validate: (v) => v.trim().length > 0 || "endpoint URL is required"
|
|
35874
36734
|
});
|
|
35875
|
-
const apiKey = await
|
|
36735
|
+
const apiKey = await dist_default7({ message: `${m.label}: API key (optional)`, mask: "*" });
|
|
35876
36736
|
const binding = { location: "network", url: url2.trim() };
|
|
35877
36737
|
if (apiKey) binding.apiKey = apiKey;
|
|
35878
36738
|
result[m.id] = binding;
|
|
@@ -35881,11 +36741,11 @@ async function promptModels(models, seed) {
|
|
|
35881
36741
|
}
|
|
35882
36742
|
async function promptWebSearch(seed) {
|
|
35883
36743
|
if (seed) return seed;
|
|
35884
|
-
const provider = (await
|
|
35885
|
-
const apiKey = await
|
|
36744
|
+
const provider = (await dist_default6({ message: "Web search provider", default: "brave" })).trim() || "brave";
|
|
36745
|
+
const apiKey = await dist_default7({ message: "Web search API key", mask: "*" });
|
|
35886
36746
|
return { provider, apiKey };
|
|
35887
36747
|
}
|
|
35888
|
-
async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
36748
|
+
async function promptMissing(partial2, outputDirDefault, req, workflowName, preloadedComponents) {
|
|
35889
36749
|
const hwTodo = req.hardwareChannels.filter((ch) => !partial2.hardware?.[ch.id]);
|
|
35890
36750
|
const mqttTodo = req.mqttChannels.filter((ch) => !partial2.mqtt?.[ch.id]);
|
|
35891
36751
|
const modelsTodo = req.customModels.filter((m) => !partial2.models?.[m.id]);
|
|
@@ -35893,13 +36753,11 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35893
36753
|
const askWeb = req.hasWebSearch && !partial2.webSearch;
|
|
35894
36754
|
let askOut = true;
|
|
35895
36755
|
if (partial2.outputDir) {
|
|
35896
|
-
const resolved =
|
|
35897
|
-
const nonEmpty =
|
|
36756
|
+
const resolved = path10.resolve(process.cwd(), partial2.outputDir);
|
|
36757
|
+
const nonEmpty = existsSync3(resolved) && (await fs7.readdir(resolved)).length > 0;
|
|
35898
36758
|
askOut = nonEmpty && !(partial2.force ?? false);
|
|
35899
36759
|
}
|
|
35900
|
-
const total = [askKeys, hwTodo.length > 0, mqttTodo.length > 0, modelsTodo.length > 0, askWeb, askOut].filter(
|
|
35901
|
-
Boolean
|
|
35902
|
-
).length;
|
|
36760
|
+
const total = [askKeys, hwTodo.length > 0, mqttTodo.length > 0, modelsTodo.length > 0, askWeb, askOut].filter(Boolean).length + 1;
|
|
35903
36761
|
let step = 0;
|
|
35904
36762
|
const section = (label, count = 0) => {
|
|
35905
36763
|
step += 1;
|
|
@@ -35925,7 +36783,7 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35925
36783
|
}
|
|
35926
36784
|
for (const provider of selectedProviders) {
|
|
35927
36785
|
if (llmKeys[provider]) continue;
|
|
35928
|
-
const key = await
|
|
36786
|
+
const key = await dist_default7({ message: `${provider} API key`, mask: "*" });
|
|
35929
36787
|
if (key) llmKeys[provider] = key;
|
|
35930
36788
|
}
|
|
35931
36789
|
}
|
|
@@ -35937,6 +36795,8 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35937
36795
|
const models = await promptModels(req.customModels, partial2.models ?? {});
|
|
35938
36796
|
if (askWeb) section("Web search");
|
|
35939
36797
|
const webSearch = req.hasWebSearch ? await promptWebSearch(partial2.webSearch) : void 0;
|
|
36798
|
+
section("Custom components");
|
|
36799
|
+
const { components: customComponents, env: componentEnv } = await promptCustomComponents(preloadedComponents);
|
|
35940
36800
|
if (askOut) section("Output");
|
|
35941
36801
|
let outputDir;
|
|
35942
36802
|
let force = partial2.force ?? false;
|
|
@@ -35944,16 +36804,16 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35944
36804
|
let candidate = partial2.outputDir;
|
|
35945
36805
|
while (true) {
|
|
35946
36806
|
if (!candidate) {
|
|
35947
|
-
candidate = await
|
|
36807
|
+
candidate = await dist_default6({ message: "Output directory", default: outputDirDefault });
|
|
35948
36808
|
}
|
|
35949
|
-
const resolved =
|
|
35950
|
-
const exists =
|
|
35951
|
-
const nonEmpty = exists && (await
|
|
36809
|
+
const resolved = path10.resolve(process.cwd(), candidate);
|
|
36810
|
+
const exists = existsSync3(resolved);
|
|
36811
|
+
const nonEmpty = exists && (await fs7.readdir(resolved)).length > 0;
|
|
35952
36812
|
if (!nonEmpty || force) {
|
|
35953
36813
|
outputDir = candidate;
|
|
35954
36814
|
break;
|
|
35955
36815
|
}
|
|
35956
|
-
const action = await
|
|
36816
|
+
const action = await dist_default8({
|
|
35957
36817
|
message: `${resolved} is not empty.`,
|
|
35958
36818
|
choices: [
|
|
35959
36819
|
{ value: "overwrite", name: "overwrite the existing files" },
|
|
@@ -35970,7 +36830,7 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35970
36830
|
candidate = void 0;
|
|
35971
36831
|
}
|
|
35972
36832
|
}
|
|
35973
|
-
|
|
36833
|
+
const config2 = {
|
|
35974
36834
|
llmKeys,
|
|
35975
36835
|
outputDir,
|
|
35976
36836
|
force,
|
|
@@ -35980,231 +36840,73 @@ async function promptMissing(partial2, outputDirDefault, req, workflowName) {
|
|
|
35980
36840
|
models,
|
|
35981
36841
|
webSearch
|
|
35982
36842
|
};
|
|
36843
|
+
return { config: config2, customComponents, componentEnv };
|
|
35983
36844
|
}
|
|
35984
36845
|
|
|
35985
36846
|
// cli/deploy/write.ts
|
|
35986
|
-
import { existsSync as
|
|
35987
|
-
import
|
|
36847
|
+
import { existsSync as existsSync4, promises as fs8 } from "node:fs";
|
|
36848
|
+
import path11 from "node:path";
|
|
35988
36849
|
|
|
35989
|
-
// cli/deploy/
|
|
35990
|
-
|
|
35991
|
-
|
|
35992
|
-
|
|
35993
|
-
|
|
35994
|
-
|
|
35995
|
-
|
|
35996
|
-
|
|
35997
|
-
|
|
35998
|
-
|
|
35999
|
-
|
|
36000
|
-
|
|
36001
|
-
|
|
36002
|
-
|
|
36003
|
-
|
|
36004
|
-
|
|
36005
|
-
|
|
36006
|
-
|
|
36007
|
-
|
|
36008
|
-
|
|
36009
|
-
|
|
36010
|
-
}
|
|
36011
|
-
|
|
36012
|
-
|
|
36013
|
-
|
|
36014
|
-
|
|
36015
|
-
|
|
36016
|
-
|
|
36017
|
-
|
|
36018
|
-
|
|
36019
|
-
|
|
36020
|
-
|
|
36021
|
-
|
|
36022
|
-
|
|
36023
|
-
|
|
36024
|
-
|
|
36025
|
-
|
|
36026
|
-
|
|
36027
|
-
|
|
36028
|
-
|
|
36029
|
-
|
|
36030
|
-
}
|
|
36031
|
-
for (const m of req.customModels) {
|
|
36032
|
-
const b = cfg.models[m.id];
|
|
36033
|
-
if (b?.location === "device") {
|
|
36034
|
-
const err = ggufNameError(b.modelFile);
|
|
36035
|
-
if (err) missing.push(`model "${m.id}": ${err}`);
|
|
36036
|
-
} else if (!b?.url) {
|
|
36037
|
-
missing.push(`model "${m.id}": endpoint URL`);
|
|
36038
|
-
}
|
|
36039
|
-
}
|
|
36040
|
-
missing.push(...hardwareConflicts(req.hardwareChannels, cfg.hardware));
|
|
36041
|
-
missing.push(...familyMismatches(req.hardwareChannels, cfg.hardware));
|
|
36042
|
-
missing.push(...unknownIds(req, cfg));
|
|
36043
|
-
if (missing.length > 0) {
|
|
36044
|
-
throw new Error(`invalid deploy config:
|
|
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 };
|
|
36850
|
+
// cli/deploy/generate.ts
|
|
36851
|
+
import { createHash } from "node:crypto";
|
|
36852
|
+
var DEFAULT_CONFIG_PATH = "/etc/foresthub/config.json";
|
|
36853
|
+
function configFileName(name) {
|
|
36854
|
+
return `${name}-config.json`;
|
|
36855
|
+
}
|
|
36856
|
+
function namedVolumeSource(mount) {
|
|
36857
|
+
const src = mount.split(":")[0] ?? "";
|
|
36858
|
+
return src && !src.startsWith(".") && !src.startsWith("/") ? src : null;
|
|
36859
|
+
}
|
|
36860
|
+
function serviceBlock(c) {
|
|
36861
|
+
const lines = [` ${c.name}:`, ` image: ${c.image}`, ` pull_policy: ${c.pull ?? "missing"}`, " restart: unless-stopped"];
|
|
36862
|
+
if (c.config !== void 0) {
|
|
36863
|
+
const hash2 = createHash("sha256").update(JSON.stringify(c.config)).digest("hex");
|
|
36864
|
+
lines.push(" labels:", ` com.foresthub.config-hash: "${hash2}"`);
|
|
36865
|
+
}
|
|
36866
|
+
if (c.user) lines.push(` user: "${c.user}"`);
|
|
36867
|
+
if (c.command && c.command.length > 0) {
|
|
36868
|
+
lines.push(" command:");
|
|
36869
|
+
for (const arg of c.command) lines.push(` - "${arg}"`);
|
|
36870
|
+
}
|
|
36871
|
+
lines.push(" env_file:", ` - path: ${c.name}.env`, " required: false");
|
|
36872
|
+
const volumes = [...c.config !== void 0 ? [`./${configFileName(c.name)}:${c.configPath ?? DEFAULT_CONFIG_PATH}:ro`] : [], ...c.volumes ?? []];
|
|
36873
|
+
if (volumes.length > 0) {
|
|
36874
|
+
lines.push(" volumes:");
|
|
36875
|
+
for (const v of volumes) lines.push(` - ${v}`);
|
|
36876
|
+
}
|
|
36877
|
+
if (c.devices && c.devices.length > 0) {
|
|
36878
|
+
lines.push(" devices:");
|
|
36879
|
+
for (const d of c.devices) lines.push(` - "${d}:${d}"`);
|
|
36880
|
+
}
|
|
36881
|
+
if (c.ports && c.ports.length > 0) {
|
|
36882
|
+
lines.push(" ports:");
|
|
36883
|
+
for (const p of c.ports) lines.push(` - "${p}"`);
|
|
36884
|
+
}
|
|
36885
|
+
if (c.privileged) {
|
|
36886
|
+
lines.push(
|
|
36887
|
+
" # ADC/DAC/PWM need sysfs access (/sys/class/pwm, /sys/bus/iio). privileged is",
|
|
36888
|
+
" # the simple default \u2014 tighten to specific bind-mounts if your policy requires.",
|
|
36889
|
+
" privileged: true"
|
|
36890
|
+
);
|
|
36103
36891
|
}
|
|
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 };
|
|
36892
|
+
return lines.join("\n");
|
|
36111
36893
|
}
|
|
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.
|
|
36894
|
+
function composeYaml(spec) {
|
|
36895
|
+
const services = spec.components.map(serviceBlock).join("\n\n");
|
|
36896
|
+
const named = [...new Set(spec.components.flatMap((c) => (c.volumes ?? []).map(namedVolumeSource).filter((s) => s !== null)))];
|
|
36897
|
+
const volumesBlock = named.length > 0 ? `
|
|
36898
|
+
volumes:
|
|
36899
|
+
${named.map((n) => ` ${n}:`).join("\n")}
|
|
36900
|
+
` : "";
|
|
36901
|
+
return `# Edge Agents engine \u2014 minimal standalone Compose deployment.
|
|
36190
36902
|
# See README.md for the build/transfer/run workflow.
|
|
36191
|
-
# See
|
|
36903
|
+
# See the <component>.env files for operator-set values.
|
|
36192
36904
|
|
|
36193
36905
|
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
|
-
`;
|
|
36906
|
+
${services}
|
|
36907
|
+
${volumesBlock}`;
|
|
36206
36908
|
}
|
|
36207
|
-
function envFile(cfg) {
|
|
36909
|
+
function envFile(cfg, resourceSecrets = {}) {
|
|
36208
36910
|
const localProviders = ALL_PROVIDERS.filter((p) => Boolean(cfg.llmKeys[p]));
|
|
36209
36911
|
const providerSection = localProviders.length === 0 ? "" : `# ----- LLM provider keys -----
|
|
36210
36912
|
# Each key here = that provider runs locally with your API key.
|
|
@@ -36216,48 +36918,35 @@ ENGINE_WEB_SEARCH_PROVIDER=${cfg.webSearch.provider}
|
|
|
36216
36918
|
ENGINE_WEB_SEARCH_API_KEY=${cfg.webSearch.apiKey}
|
|
36217
36919
|
|
|
36218
36920
|
` : "";
|
|
36219
|
-
const
|
|
36220
|
-
|
|
36221
|
-
# Context window per llama-server sidecar.
|
|
36222
|
-
${deviceModelIds.map((id) => `${ctxSizeVar(id)}=4096`).join("\n")}
|
|
36921
|
+
const resourceSecretsSection = Object.keys(resourceSecrets).length > 0 ? `# ----- External-resource secrets -----
|
|
36922
|
+
FH_RESOURCE_SECRETS=${JSON.stringify(resourceSecrets)}
|
|
36223
36923
|
|
|
36224
36924
|
` : "";
|
|
36225
|
-
return `#
|
|
36226
|
-
# Auto-generated by \`fh-workflow deploy\`.
|
|
36227
|
-
# Secret values: chmod 600
|
|
36925
|
+
return `# Edge Agents engine \u2014 operator configuration.
|
|
36926
|
+
# Auto-generated by \`fh-workflow deploy\`. Loaded into the engine via the compose
|
|
36927
|
+
# \`env_file\`. Secret values: chmod 600 this file after editing.
|
|
36228
36928
|
|
|
36229
|
-
${providerSection}${webSearchSection}${
|
|
36929
|
+
${providerSection}${webSearchSection}${resourceSecretsSection}# ----- Runtime -----
|
|
36230
36930
|
ENGINE_LOG_LEVEL=${cfg.logLevel} # debug | info | warn | error
|
|
36231
36931
|
`;
|
|
36232
36932
|
}
|
|
36233
|
-
function readme(cfg,
|
|
36933
|
+
function readme(spec, cfg, hasProviderModel) {
|
|
36234
36934
|
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 deviceModelFiles = [];
|
|
36239
|
-
|
|
36240
|
-
|
|
36241
|
-
|
|
36242
|
-
if (b?.location === "device") deviceModelFiles.push(b.modelFile);
|
|
36243
|
-
else if (b?.location === "network") hasNetworkModel = true;
|
|
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") : "";
|
|
36935
|
+
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");
|
|
36936
|
+
const engine = spec.components.find((c) => c.name === "engine");
|
|
36937
|
+
const hasHardware = (engine?.devices?.length ?? 0) > 0 || Boolean(engine?.privileged);
|
|
36938
|
+
const deviceModelFiles = Object.values(cfg.models).flatMap((b) => b.location === "device" ? [b.modelFile] : []);
|
|
36939
|
+
const hasNetworkModel = Object.values(cfg.models).some((b) => b.location === "network");
|
|
36940
|
+
const hasMqtt = Object.keys(cfg.mqtt).length > 0;
|
|
36941
|
+
const hasExternalService = hasMqtt || hasNetworkModel;
|
|
36253
36942
|
const notes = [];
|
|
36254
|
-
if (
|
|
36943
|
+
if (hasProviderModel || localProviders.length > 0) {
|
|
36255
36944
|
notes.push(`## LLM provider keys
|
|
36256
36945
|
|
|
36257
36946
|
${providerBlock}
|
|
36258
36947
|
|
|
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.`);
|
|
36948
|
+
The engine calls each provider directly with the API key from \`engine.env\`. Without
|
|
36949
|
+
a key, an Agent node that uses that provider's catalog model fails at build.`);
|
|
36261
36950
|
}
|
|
36262
36951
|
if (hasHardware) {
|
|
36263
36952
|
notes.push(`## Hardware access
|
|
@@ -36270,9 +36959,10 @@ and tighten it to your security policy before deploying.`);
|
|
|
36270
36959
|
if (hasExternalService) {
|
|
36271
36960
|
notes.push(`## External resources
|
|
36272
36961
|
|
|
36273
|
-
|
|
36274
|
-
|
|
36275
|
-
broker/endpoint already exists and is reachable from the
|
|
36962
|
+
The broker/endpoint credentials the engine connects with live in \`engine.env\`
|
|
36963
|
+
(\`FH_RESOURCE_SECRETS\`) \u2014 keep it \`chmod 600\`. This bundle does not start those
|
|
36964
|
+
services; it assumes the broker/endpoint already exists and is reachable from the
|
|
36965
|
+
controller.`);
|
|
36276
36966
|
}
|
|
36277
36967
|
if (deviceModelFiles.length > 0) {
|
|
36278
36968
|
notes.push(`## On-device models
|
|
@@ -36289,18 +36979,12 @@ ${deviceModelFiles.map((f) => `- \`./models/${f}\``).join("\n")}`);
|
|
|
36289
36979
|
A network model points at an inference endpoint **you run yourself** (llama-server, vLLM,
|
|
36290
36980
|
Ollama, ...) on another machine. This bundle does not start that server for you.`);
|
|
36291
36981
|
}
|
|
36292
|
-
if (
|
|
36982
|
+
if (cfg.webSearch) {
|
|
36293
36983
|
notes.push(`## Web search
|
|
36294
36984
|
|
|
36295
|
-
The web-search API key is in
|
|
36985
|
+
The web-search API key is in \`engine.env\` (\`ENGINE_WEB_SEARCH_API_KEY\`).`);
|
|
36296
36986
|
}
|
|
36297
36987
|
const notesBlock = notes.length ? "\n" + notes.join("\n\n") + "\n" : "";
|
|
36298
|
-
const transferExtra = [
|
|
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
36988
|
const modelsTransfer = deviceModelFiles.length ? `
|
|
36305
36989
|
|
|
36306
36990
|
# On-device model weights \u2014 too large for the line above (GGUF can be several GB).
|
|
@@ -36310,21 +36994,22 @@ scp -r models/ $CONTROLLER_USER@$CONTROLLER_ADDR:~/fh-engine/
|
|
|
36310
36994
|
const runBlock = deviceModelFiles.length ? `ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker load -i fh-engine.tar'
|
|
36311
36995
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose up -d'
|
|
36312
36996
|
|
|
36313
|
-
# The engine
|
|
36314
|
-
#
|
|
36997
|
+
# The engine and the llama-server sidecar start independently; the engine reaches the
|
|
36998
|
+
# model once it is up. These show every container, not just the engine:
|
|
36315
36999
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose ps'
|
|
36316
37000
|
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
37001
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose up -d'
|
|
36318
37002
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'cd ~/fh-engine && docker compose logs -f engine'`;
|
|
36319
|
-
return `#
|
|
37003
|
+
return `# Edge Agents engine \u2014 deployment bundle
|
|
36320
37004
|
|
|
36321
37005
|
Generated by \`fh-workflow deploy\`. This directory contains everything needed to
|
|
36322
37006
|
run one engine instance on an edge controller, **standalone** \u2014 the engine boots
|
|
36323
|
-
the workflow from \`
|
|
37007
|
+
the workflow from \`engine-config.json\` and runs it autonomously:
|
|
36324
37008
|
|
|
36325
37009
|
- \`docker-compose.yml\` \u2014 deployment template
|
|
36326
|
-
- \`
|
|
36327
|
-
-
|
|
37010
|
+
- \`engine-config.json\` \u2014 the engine's single boot config (workflow + bindings + device manifest)
|
|
37011
|
+
- \`deployment-spec.json\` \u2014 the full resolved deployment spec (deployment record)
|
|
37012
|
+
- \`engine.env\` \u2014 operator configuration loaded into the engine (already filled in, \`chmod 600\` it)
|
|
36328
37013
|
- \`fh-engine.tar\` \u2014 image tarball (you build this in step 1 below)
|
|
36329
37014
|
${notesBlock}
|
|
36330
37015
|
## 1. Build the image (on the dev machine)
|
|
@@ -36341,10 +37026,10 @@ docker buildx build --platform linux/arm64 -t fh-engine:latest --load .
|
|
|
36341
37026
|
docker buildx build --platform linux/amd64 -t fh-engine:latest --load .
|
|
36342
37027
|
\`\`\`
|
|
36343
37028
|
|
|
36344
|
-
## 2. Review the generated
|
|
37029
|
+
## 2. Review the generated \`engine.env\`
|
|
36345
37030
|
|
|
36346
37031
|
\`\`\`bash
|
|
36347
|
-
chmod 600 .env
|
|
37032
|
+
chmod 600 engine.env
|
|
36348
37033
|
\`\`\`
|
|
36349
37034
|
|
|
36350
37035
|
The wizard already filled in your operator values. Double-check before
|
|
@@ -36362,7 +37047,7 @@ docker save fh-engine:latest -o ../path/to/this/bundle/fh-engine.tar
|
|
|
36362
37047
|
ssh $CONTROLLER_USER@$CONTROLLER_ADDR 'mkdir -p ~/fh-engine'
|
|
36363
37048
|
|
|
36364
37049
|
cd path/to/this/bundle
|
|
36365
|
-
scp fh-engine.tar docker-compose.yml
|
|
37050
|
+
scp fh-engine.tar docker-compose.yml engine-config.json deployment-spec.json engine.env \\
|
|
36366
37051
|
$CONTROLLER_USER@$CONTROLLER_ADDR:~/fh-engine/${modelsTransfer}
|
|
36367
37052
|
\`\`\`
|
|
36368
37053
|
|
|
@@ -36380,67 +37065,56 @@ only \`docker compose down -v\` deletes them. The volume is namespaced by the
|
|
|
36380
37065
|
compose project name, which defaults to the bundle directory's name \u2014 keep it
|
|
36381
37066
|
stable across updates, or pin it via \`COMPOSE_PROJECT_NAME\`. In this standalone
|
|
36382
37067
|
bundle the volume is the only copy of the memory.
|
|
36383
|
-
|
|
36384
|
-
To keep the data at a host path instead, replace the named volume with a bind
|
|
36385
|
-
mount. Named-volume ownership is handled by the image; a bind-mounted host
|
|
36386
|
-
directory you must create and chown to the engine's nonroot user yourself:
|
|
36387
|
-
|
|
36388
|
-
\`\`\`bash
|
|
36389
|
-
sudo mkdir -p /data/foresthub/memory && sudo chown -R 65532:65532 /data/foresthub/memory
|
|
36390
|
-
# docker-compose.yml: - /data/foresthub/memory:/var/lib/foresthub/memory
|
|
36391
|
-
\`\`\`
|
|
36392
37068
|
`;
|
|
36393
37069
|
}
|
|
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
37070
|
function slugify2(name) {
|
|
36399
37071
|
return name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
36400
37072
|
}
|
|
36401
37073
|
|
|
36402
37074
|
// cli/deploy/write.ts
|
|
36403
|
-
async function writeOutput(
|
|
36404
|
-
const
|
|
36405
|
-
|
|
36406
|
-
|
|
36407
|
-
const contents = await fs7.readdir(dir);
|
|
37075
|
+
async function writeOutput(spec, resourceSecrets, cfg, req, componentEnv = {}) {
|
|
37076
|
+
const dir = path11.resolve(process.cwd(), cfg.outputDir);
|
|
37077
|
+
if (existsSync4(dir)) {
|
|
37078
|
+
const contents = await fs8.readdir(dir);
|
|
36408
37079
|
if (contents.length > 0) {
|
|
36409
37080
|
if (!cfg.force) {
|
|
36410
37081
|
process.stderr.write(`output dir not empty: ${dir} (use --force to overwrite)
|
|
36411
37082
|
`);
|
|
36412
37083
|
process.exit(1);
|
|
36413
37084
|
}
|
|
36414
|
-
await
|
|
36415
|
-
await
|
|
37085
|
+
await fs8.rm(dir, { recursive: true, force: true });
|
|
37086
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
36416
37087
|
}
|
|
36417
37088
|
} else {
|
|
36418
|
-
await
|
|
37089
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
36419
37090
|
}
|
|
36420
37091
|
const written = [];
|
|
36421
37092
|
const emit = async (name, content, secret = false) => {
|
|
36422
|
-
const out =
|
|
37093
|
+
const out = path11.join(dir, name);
|
|
36423
37094
|
const opts = secret ? { encoding: "utf-8", mode: 384 } : { encoding: "utf-8" };
|
|
36424
|
-
await
|
|
37095
|
+
await fs8.writeFile(out, content, opts);
|
|
36425
37096
|
written.push(out);
|
|
36426
37097
|
};
|
|
36427
37098
|
const json2 = (v) => JSON.stringify(v, null, 2) + "\n";
|
|
36428
|
-
|
|
36429
|
-
|
|
36430
|
-
|
|
36431
|
-
await emit("
|
|
36432
|
-
|
|
36433
|
-
|
|
36434
|
-
|
|
36435
|
-
|
|
36436
|
-
|
|
37099
|
+
for (const c of spec.components) {
|
|
37100
|
+
if (c.config !== void 0) await emit(configFileName(c.name), json2(c.config));
|
|
37101
|
+
}
|
|
37102
|
+
await emit("deployment-spec.json", json2(spec));
|
|
37103
|
+
await emit("docker-compose.yml", composeYaml(spec));
|
|
37104
|
+
await emit("engine.env", envFile(cfg, resourceSecrets), true);
|
|
37105
|
+
await emit("README.md", readme(spec, cfg, req.hasProviderModel));
|
|
37106
|
+
for (const [name, text] of Object.entries(componentEnv)) {
|
|
37107
|
+
await emit(`${name}.env`, text, true);
|
|
37108
|
+
}
|
|
36437
37109
|
if (Object.values(cfg.models).some((b) => b.location === "device")) {
|
|
36438
|
-
await
|
|
37110
|
+
await fs8.mkdir(path11.join(dir, "models"), { recursive: true });
|
|
36439
37111
|
}
|
|
36440
37112
|
return written;
|
|
36441
37113
|
}
|
|
36442
37114
|
|
|
36443
37115
|
// cli/deploy/index.ts
|
|
37116
|
+
var ENGINE_IMAGE = "fh-engine:latest";
|
|
37117
|
+
var LLAMA_SERVER_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b8589";
|
|
36444
37118
|
var USAGE = `Usage: fh-workflow deploy <workflow.json> [flags]
|
|
36445
37119
|
|
|
36446
37120
|
Generates a self-contained, standalone Engine deployment bundle (workflow.json,
|
|
@@ -36451,6 +37125,9 @@ autonomously. Missing values are filled in interactively, or supplied via
|
|
|
36451
37125
|
LLM provider keys (set one for each catalog model an Agent uses):
|
|
36452
37126
|
${ALL_PROVIDERS.map((p) => ` --${p}-key KEY`).join("\n")}
|
|
36453
37127
|
|
|
37128
|
+
Custom components (extra containers to run alongside the engine):
|
|
37129
|
+
--component DIR folder with a component.json (repeatable)
|
|
37130
|
+
|
|
36454
37131
|
Output:
|
|
36455
37132
|
--output DIR default: ./<workflow-name>-bundle
|
|
36456
37133
|
--force overwrite existing DIR
|
|
@@ -36472,6 +37149,7 @@ function parseFlags(args2) {
|
|
|
36472
37149
|
output: { type: "string" },
|
|
36473
37150
|
"log-level": { type: "string" },
|
|
36474
37151
|
values: { type: "string" },
|
|
37152
|
+
component: { type: "string", multiple: true },
|
|
36475
37153
|
force: { type: "boolean", default: false },
|
|
36476
37154
|
help: { type: "boolean", short: "h", default: false }
|
|
36477
37155
|
},
|
|
@@ -36488,6 +37166,7 @@ function parseFlags(args2) {
|
|
|
36488
37166
|
output: values.output,
|
|
36489
37167
|
logLevel: values["log-level"],
|
|
36490
37168
|
values: values.values,
|
|
37169
|
+
component: values.component ?? [],
|
|
36491
37170
|
force: values.force ?? false,
|
|
36492
37171
|
help: values.help ?? false
|
|
36493
37172
|
};
|
|
@@ -36513,10 +37192,10 @@ function partialFromFlags(flags, fileValues) {
|
|
|
36513
37192
|
};
|
|
36514
37193
|
}
|
|
36515
37194
|
async function loadValues(source) {
|
|
36516
|
-
const abs =
|
|
37195
|
+
const abs = path12.resolve(process.cwd(), source);
|
|
36517
37196
|
let raw;
|
|
36518
37197
|
try {
|
|
36519
|
-
raw = await
|
|
37198
|
+
raw = await fs9.readFile(abs, "utf-8");
|
|
36520
37199
|
} catch (err) {
|
|
36521
37200
|
if (err.code === "ENOENT") {
|
|
36522
37201
|
process.stderr.write(`Values file not found: ${abs}
|
|
@@ -36589,15 +37268,41 @@ function configFromPartial(p, outputDirDefault) {
|
|
|
36589
37268
|
webSearch: p.webSearch
|
|
36590
37269
|
};
|
|
36591
37270
|
}
|
|
37271
|
+
async function loadFlagComponents(dirs) {
|
|
37272
|
+
const uniqueDirs = [...new Set(dirs.map((d) => path12.resolve(process.cwd(), d)))];
|
|
37273
|
+
const entries = await Promise.all(
|
|
37274
|
+
uniqueDirs.map(async (dir) => ({ source: path12.join(dir, "component.json"), data: await readComponentJson(dir), dir }))
|
|
37275
|
+
);
|
|
37276
|
+
parseDeployComponents(entries);
|
|
37277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37278
|
+
const loaded = [];
|
|
37279
|
+
for (const e of entries) {
|
|
37280
|
+
const component = e.data;
|
|
37281
|
+
if (seen.has(component.name)) {
|
|
37282
|
+
throw new Error(`duplicate component name "${component.name}" (two --component folders declare it)`);
|
|
37283
|
+
}
|
|
37284
|
+
seen.add(component.name);
|
|
37285
|
+
loaded.push({ component, dir: e.dir });
|
|
37286
|
+
}
|
|
37287
|
+
return loaded;
|
|
37288
|
+
}
|
|
37289
|
+
async function resolveComponentsEnv(loaded) {
|
|
37290
|
+
const env = {};
|
|
37291
|
+
for (const { component, dir } of loaded) {
|
|
37292
|
+
const text = await resolveComponentEnv(dir, component.name, { interactive: false });
|
|
37293
|
+
if (text !== null) env[component.name] = text;
|
|
37294
|
+
}
|
|
37295
|
+
return env;
|
|
37296
|
+
}
|
|
36592
37297
|
async function deployCommand(workflowPath, args2) {
|
|
36593
37298
|
if (!workflowPath || workflowPath === "--help" || workflowPath === "-h") {
|
|
36594
37299
|
process.stdout.write(USAGE);
|
|
36595
37300
|
process.exit(workflowPath ? 0 : 1);
|
|
36596
37301
|
}
|
|
36597
|
-
const abs =
|
|
37302
|
+
const abs = path12.resolve(process.cwd(), workflowPath);
|
|
36598
37303
|
let raw;
|
|
36599
37304
|
try {
|
|
36600
|
-
raw = await
|
|
37305
|
+
raw = await fs9.readFile(abs, "utf-8");
|
|
36601
37306
|
} catch (err) {
|
|
36602
37307
|
const code = err.code;
|
|
36603
37308
|
if (code === "ENOENT") {
|
|
@@ -36628,20 +37333,44 @@ async function deployCommand(workflowPath, args2) {
|
|
|
36628
37333
|
process.stdout.write(USAGE);
|
|
36629
37334
|
process.exit(0);
|
|
36630
37335
|
}
|
|
36631
|
-
|
|
37336
|
+
let domain2;
|
|
37337
|
+
try {
|
|
37338
|
+
domain2 = deserialize7(workflow);
|
|
37339
|
+
} catch (err) {
|
|
37340
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37341
|
+
`);
|
|
37342
|
+
process.exit(1);
|
|
37343
|
+
}
|
|
37344
|
+
const req = deriveRequirements(domain2);
|
|
36632
37345
|
if (req.hasRetriever) {
|
|
36633
37346
|
process.stderr.write(
|
|
36634
37347
|
"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
37348
|
);
|
|
36636
37349
|
process.exit(1);
|
|
36637
37350
|
}
|
|
36638
|
-
const workflowName =
|
|
37351
|
+
const workflowName = path12.basename(abs, path12.extname(abs));
|
|
36639
37352
|
const outputDirDefault = `./${slugify2(workflowName)}-bundle`;
|
|
36640
37353
|
const fileValues = flags.values ? await loadValues(flags.values) : {};
|
|
36641
37354
|
const partial2 = partialFromFlags(flags, fileValues);
|
|
37355
|
+
let preloaded;
|
|
37356
|
+
try {
|
|
37357
|
+
preloaded = await loadFlagComponents(flags.component);
|
|
37358
|
+
} catch (err) {
|
|
37359
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37360
|
+
`);
|
|
37361
|
+
process.exit(1);
|
|
37362
|
+
}
|
|
36642
37363
|
let cfg;
|
|
37364
|
+
let customComponents;
|
|
37365
|
+
let componentEnv;
|
|
36643
37366
|
if (process.stdin.isTTY) {
|
|
36644
|
-
cfg = await promptMissing(
|
|
37367
|
+
({ config: cfg, customComponents, componentEnv } = await promptMissing(
|
|
37368
|
+
partial2,
|
|
37369
|
+
outputDirDefault,
|
|
37370
|
+
req,
|
|
37371
|
+
workflowName,
|
|
37372
|
+
preloaded
|
|
37373
|
+
));
|
|
36645
37374
|
} else {
|
|
36646
37375
|
const missing = missingRequired(req, partial2);
|
|
36647
37376
|
if (missing.length > 0) {
|
|
@@ -36652,21 +37381,41 @@ async function deployCommand(workflowPath, args2) {
|
|
|
36652
37381
|
process.exit(1);
|
|
36653
37382
|
}
|
|
36654
37383
|
cfg = configFromPartial(partial2, outputDirDefault);
|
|
37384
|
+
customComponents = preloaded.map((c) => c.component);
|
|
37385
|
+
componentEnv = await resolveComponentsEnv(preloaded);
|
|
37386
|
+
}
|
|
37387
|
+
let built;
|
|
37388
|
+
try {
|
|
37389
|
+
built = buildDeploymentSpec(
|
|
37390
|
+
domain2,
|
|
37391
|
+
cfg,
|
|
37392
|
+
{
|
|
37393
|
+
id: slugify2(workflowName),
|
|
37394
|
+
status: "active",
|
|
37395
|
+
engineImage: ENGINE_IMAGE,
|
|
37396
|
+
llamaServerImage: LLAMA_SERVER_IMAGE
|
|
37397
|
+
},
|
|
37398
|
+
customComponents
|
|
37399
|
+
);
|
|
37400
|
+
} catch (err) {
|
|
37401
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
37402
|
+
`);
|
|
37403
|
+
process.exit(1);
|
|
36655
37404
|
}
|
|
36656
37405
|
let files;
|
|
36657
37406
|
try {
|
|
36658
|
-
files = await writeOutput(
|
|
37407
|
+
files = await writeOutput(built.spec, built.resourceSecrets, cfg, req, componentEnv);
|
|
36659
37408
|
} catch (err) {
|
|
36660
37409
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
36661
37410
|
`);
|
|
36662
37411
|
process.exit(1);
|
|
36663
37412
|
}
|
|
36664
|
-
const absOut =
|
|
37413
|
+
const absOut = path12.resolve(process.cwd(), cfg.outputDir);
|
|
36665
37414
|
process.stdout.write(`
|
|
36666
37415
|
\u2713 Deployment bundle written to ${absOut}
|
|
36667
37416
|
`);
|
|
36668
37417
|
for (const f of files) {
|
|
36669
|
-
process.stdout.write(` - ${
|
|
37418
|
+
process.stdout.write(` - ${path12.relative(process.cwd(), f)}
|
|
36670
37419
|
`);
|
|
36671
37420
|
}
|
|
36672
37421
|
process.stdout.write(`
|