@dforge-core/dforge-mcp 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +287 -0
- package/README.md +106 -72
- package/dist/server.js +2322 -388
- package/docs/creating-modules.md +21 -10
- package/package.json +46 -43
- package/resources/docs/dsl-reference.md +26 -0
- package/scripts/install-skills.mjs +165 -0
- package/skills/dforge-mcp-author/SKILL.md +147 -468
- package/skills/dforge-mcp-author/references/column-domains.md +94 -0
- package/skills/dforge-mcp-author/references/document-extraction.md +113 -0
- package/skills/dforge-mcp-author/references/field-types.md +2 -0
- package/skills/dforge-mcp-author/references/formulas.md +83 -20
- package/skills/dforge-mcp-author/references/translations.md +43 -0
- package/skills/dforge-module-build/SKILL.md +309 -0
- package/skills/dforge-module-design/SKILL.md +125 -0
- package/skills/dforge-module-ship/SKILL.md +163 -0
package/dist/server.js
CHANGED
|
@@ -98,7 +98,7 @@ var require_code = __commonJS({
|
|
|
98
98
|
}
|
|
99
99
|
exports2._ = _;
|
|
100
100
|
var plus = new _Code("+");
|
|
101
|
-
function
|
|
101
|
+
function str2(strs, ...args) {
|
|
102
102
|
const expr = [safeStringify(strs[0])];
|
|
103
103
|
let i = 0;
|
|
104
104
|
while (i < args.length) {
|
|
@@ -109,7 +109,7 @@ var require_code = __commonJS({
|
|
|
109
109
|
optimize(expr);
|
|
110
110
|
return new _Code(expr);
|
|
111
111
|
}
|
|
112
|
-
exports2.str =
|
|
112
|
+
exports2.str = str2;
|
|
113
113
|
function addCodeArg(code2, arg) {
|
|
114
114
|
if (arg instanceof _Code)
|
|
115
115
|
code2.push(...arg._items);
|
|
@@ -152,7 +152,7 @@ var require_code = __commonJS({
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
function strConcat(c1, c2) {
|
|
155
|
-
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 :
|
|
155
|
+
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
|
|
156
156
|
}
|
|
157
157
|
exports2.strConcat = strConcat;
|
|
158
158
|
function interpolate(x) {
|
|
@@ -1114,22 +1114,22 @@ var require_util = __commonJS({
|
|
|
1114
1114
|
return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
|
|
1115
1115
|
}
|
|
1116
1116
|
exports2.schemaRefOrVal = schemaRefOrVal;
|
|
1117
|
-
function unescapeFragment(
|
|
1118
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
1117
|
+
function unescapeFragment(str2) {
|
|
1118
|
+
return unescapeJsonPointer(decodeURIComponent(str2));
|
|
1119
1119
|
}
|
|
1120
1120
|
exports2.unescapeFragment = unescapeFragment;
|
|
1121
|
-
function escapeFragment(
|
|
1122
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
1121
|
+
function escapeFragment(str2) {
|
|
1122
|
+
return encodeURIComponent(escapeJsonPointer(str2));
|
|
1123
1123
|
}
|
|
1124
1124
|
exports2.escapeFragment = escapeFragment;
|
|
1125
|
-
function escapeJsonPointer(
|
|
1126
|
-
if (typeof
|
|
1127
|
-
return `${
|
|
1128
|
-
return
|
|
1125
|
+
function escapeJsonPointer(str2) {
|
|
1126
|
+
if (typeof str2 == "number")
|
|
1127
|
+
return `${str2}`;
|
|
1128
|
+
return str2.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1129
1129
|
}
|
|
1130
1130
|
exports2.escapeJsonPointer = escapeJsonPointer;
|
|
1131
|
-
function unescapeJsonPointer(
|
|
1132
|
-
return
|
|
1131
|
+
function unescapeJsonPointer(str2) {
|
|
1132
|
+
return str2.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1133
1133
|
}
|
|
1134
1134
|
exports2.unescapeJsonPointer = unescapeJsonPointer;
|
|
1135
1135
|
function eachItem(xs, f) {
|
|
@@ -2154,8 +2154,8 @@ var require_json_schema_traverse = __commonJS({
|
|
|
2154
2154
|
post(schema2, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
|
|
2155
2155
|
}
|
|
2156
2156
|
}
|
|
2157
|
-
function escapeJsonPtr(
|
|
2158
|
-
return
|
|
2157
|
+
function escapeJsonPtr(str2) {
|
|
2158
|
+
return str2.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2159
2159
|
}
|
|
2160
2160
|
}
|
|
2161
2161
|
});
|
|
@@ -2981,7 +2981,7 @@ var require_compile = __commonJS({
|
|
|
2981
2981
|
const schOrFunc = root.refs[ref];
|
|
2982
2982
|
if (schOrFunc)
|
|
2983
2983
|
return schOrFunc;
|
|
2984
|
-
let _sch =
|
|
2984
|
+
let _sch = resolve8.call(this, root, ref);
|
|
2985
2985
|
if (_sch === void 0) {
|
|
2986
2986
|
const schema2 = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
2987
2987
|
const { schemaId } = this.opts;
|
|
@@ -3008,7 +3008,7 @@ var require_compile = __commonJS({
|
|
|
3008
3008
|
function sameSchemaEnv(s1, s2) {
|
|
3009
3009
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3010
3010
|
}
|
|
3011
|
-
function
|
|
3011
|
+
function resolve8(root, ref) {
|
|
3012
3012
|
let sch;
|
|
3013
3013
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3014
3014
|
ref = sch;
|
|
@@ -3219,15 +3219,15 @@ var require_utils = __commonJS({
|
|
|
3219
3219
|
return { host, isIPV6: false };
|
|
3220
3220
|
}
|
|
3221
3221
|
}
|
|
3222
|
-
function findToken(
|
|
3222
|
+
function findToken(str2, token) {
|
|
3223
3223
|
let ind = 0;
|
|
3224
|
-
for (let i = 0; i <
|
|
3225
|
-
if (
|
|
3224
|
+
for (let i = 0; i < str2.length; i++) {
|
|
3225
|
+
if (str2[i] === token) ind++;
|
|
3226
3226
|
}
|
|
3227
3227
|
return ind;
|
|
3228
3228
|
}
|
|
3229
|
-
function removeDotSegments(
|
|
3230
|
-
let input =
|
|
3229
|
+
function removeDotSegments(path17) {
|
|
3230
|
+
let input = path17;
|
|
3231
3231
|
const output = [];
|
|
3232
3232
|
let nextSlash = -1;
|
|
3233
3233
|
let len = 0;
|
|
@@ -3479,8 +3479,8 @@ var require_schemes = __commonJS({
|
|
|
3479
3479
|
wsComponent.secure = void 0;
|
|
3480
3480
|
}
|
|
3481
3481
|
if (wsComponent.resourceName) {
|
|
3482
|
-
const [
|
|
3483
|
-
wsComponent.path =
|
|
3482
|
+
const [path17, query] = wsComponent.resourceName.split("?");
|
|
3483
|
+
wsComponent.path = path17 && path17 !== "/" ? path17 : void 0;
|
|
3484
3484
|
wsComponent.query = query;
|
|
3485
3485
|
wsComponent.resourceName = void 0;
|
|
3486
3486
|
}
|
|
@@ -3635,59 +3635,59 @@ var require_fast_uri = __commonJS({
|
|
|
3635
3635
|
normalizeString(uri, options);
|
|
3636
3636
|
} else if (typeof uri === "object") {
|
|
3637
3637
|
uri = /** @type {T} */
|
|
3638
|
-
parse3(
|
|
3638
|
+
parse3(serialize2(uri, options), options);
|
|
3639
3639
|
}
|
|
3640
3640
|
return uri;
|
|
3641
3641
|
}
|
|
3642
|
-
function
|
|
3642
|
+
function resolve8(baseURI, relativeURI, options) {
|
|
3643
3643
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3644
3644
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3645
3645
|
schemelessOptions.skipEscape = true;
|
|
3646
|
-
return
|
|
3646
|
+
return serialize2(resolved, schemelessOptions);
|
|
3647
3647
|
}
|
|
3648
|
-
function resolveComponent(base,
|
|
3648
|
+
function resolveComponent(base, relative3, options, skipNormalization) {
|
|
3649
3649
|
const target = {};
|
|
3650
3650
|
if (!skipNormalization) {
|
|
3651
|
-
base = parse3(
|
|
3652
|
-
|
|
3651
|
+
base = parse3(serialize2(base, options), options);
|
|
3652
|
+
relative3 = parse3(serialize2(relative3, options), options);
|
|
3653
3653
|
}
|
|
3654
3654
|
options = options || {};
|
|
3655
|
-
if (!options.tolerant &&
|
|
3656
|
-
target.scheme =
|
|
3657
|
-
target.userinfo =
|
|
3658
|
-
target.host =
|
|
3659
|
-
target.port =
|
|
3660
|
-
target.path = removeDotSegments(
|
|
3661
|
-
target.query =
|
|
3655
|
+
if (!options.tolerant && relative3.scheme) {
|
|
3656
|
+
target.scheme = relative3.scheme;
|
|
3657
|
+
target.userinfo = relative3.userinfo;
|
|
3658
|
+
target.host = relative3.host;
|
|
3659
|
+
target.port = relative3.port;
|
|
3660
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3661
|
+
target.query = relative3.query;
|
|
3662
3662
|
} else {
|
|
3663
|
-
if (
|
|
3664
|
-
target.userinfo =
|
|
3665
|
-
target.host =
|
|
3666
|
-
target.port =
|
|
3667
|
-
target.path = removeDotSegments(
|
|
3668
|
-
target.query =
|
|
3663
|
+
if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
|
|
3664
|
+
target.userinfo = relative3.userinfo;
|
|
3665
|
+
target.host = relative3.host;
|
|
3666
|
+
target.port = relative3.port;
|
|
3667
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3668
|
+
target.query = relative3.query;
|
|
3669
3669
|
} else {
|
|
3670
|
-
if (!
|
|
3670
|
+
if (!relative3.path) {
|
|
3671
3671
|
target.path = base.path;
|
|
3672
|
-
if (
|
|
3673
|
-
target.query =
|
|
3672
|
+
if (relative3.query !== void 0) {
|
|
3673
|
+
target.query = relative3.query;
|
|
3674
3674
|
} else {
|
|
3675
3675
|
target.query = base.query;
|
|
3676
3676
|
}
|
|
3677
3677
|
} else {
|
|
3678
|
-
if (
|
|
3679
|
-
target.path = removeDotSegments(
|
|
3678
|
+
if (relative3.path[0] === "/") {
|
|
3679
|
+
target.path = removeDotSegments(relative3.path);
|
|
3680
3680
|
} else {
|
|
3681
3681
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3682
|
-
target.path = "/" +
|
|
3682
|
+
target.path = "/" + relative3.path;
|
|
3683
3683
|
} else if (!base.path) {
|
|
3684
|
-
target.path =
|
|
3684
|
+
target.path = relative3.path;
|
|
3685
3685
|
} else {
|
|
3686
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3686
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
|
|
3687
3687
|
}
|
|
3688
3688
|
target.path = removeDotSegments(target.path);
|
|
3689
3689
|
}
|
|
3690
|
-
target.query =
|
|
3690
|
+
target.query = relative3.query;
|
|
3691
3691
|
}
|
|
3692
3692
|
target.userinfo = base.userinfo;
|
|
3693
3693
|
target.host = base.host;
|
|
@@ -3695,7 +3695,7 @@ var require_fast_uri = __commonJS({
|
|
|
3695
3695
|
}
|
|
3696
3696
|
target.scheme = base.scheme;
|
|
3697
3697
|
}
|
|
3698
|
-
target.fragment =
|
|
3698
|
+
target.fragment = relative3.fragment;
|
|
3699
3699
|
return target;
|
|
3700
3700
|
}
|
|
3701
3701
|
function equal(uriA, uriB, options) {
|
|
@@ -3703,7 +3703,7 @@ var require_fast_uri = __commonJS({
|
|
|
3703
3703
|
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3704
3704
|
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3705
3705
|
}
|
|
3706
|
-
function
|
|
3706
|
+
function serialize2(cmpts, opts) {
|
|
3707
3707
|
const component = {
|
|
3708
3708
|
host: cmpts.host,
|
|
3709
3709
|
scheme: cmpts.scheme,
|
|
@@ -3881,7 +3881,7 @@ var require_fast_uri = __commonJS({
|
|
|
3881
3881
|
function normalizeStringWithStatus(uri, opts) {
|
|
3882
3882
|
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3883
3883
|
return {
|
|
3884
|
-
normalized: malformedAuthorityOrPort ? uri :
|
|
3884
|
+
normalized: malformedAuthorityOrPort ? uri : serialize2(parsed, opts),
|
|
3885
3885
|
malformedAuthorityOrPort
|
|
3886
3886
|
};
|
|
3887
3887
|
}
|
|
@@ -3891,16 +3891,16 @@ var require_fast_uri = __commonJS({
|
|
|
3891
3891
|
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3892
3892
|
}
|
|
3893
3893
|
if (typeof uri === "object") {
|
|
3894
|
-
return
|
|
3894
|
+
return serialize2(uri, opts);
|
|
3895
3895
|
}
|
|
3896
3896
|
}
|
|
3897
3897
|
var fastUri = {
|
|
3898
3898
|
SCHEMES,
|
|
3899
3899
|
normalize,
|
|
3900
|
-
resolve:
|
|
3900
|
+
resolve: resolve8,
|
|
3901
3901
|
resolveComponent,
|
|
3902
3902
|
equal,
|
|
3903
|
-
serialize,
|
|
3903
|
+
serialize: serialize2,
|
|
3904
3904
|
parse: parse3
|
|
3905
3905
|
};
|
|
3906
3906
|
module2.exports = fastUri;
|
|
@@ -3959,7 +3959,7 @@ var require_core = __commonJS({
|
|
|
3959
3959
|
var util_1 = require_util();
|
|
3960
3960
|
var $dataRefSchema = require_data();
|
|
3961
3961
|
var uri_1 = require_uri();
|
|
3962
|
-
var defaultRegExp = (
|
|
3962
|
+
var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
|
|
3963
3963
|
defaultRegExp.code = "new RegExp";
|
|
3964
3964
|
var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
|
|
3965
3965
|
var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -4754,16 +4754,16 @@ var require_ucs2length = __commonJS({
|
|
|
4754
4754
|
"node_modules/ajv/dist/runtime/ucs2length.js"(exports2) {
|
|
4755
4755
|
"use strict";
|
|
4756
4756
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
4757
|
-
function ucs2length(
|
|
4758
|
-
const len =
|
|
4757
|
+
function ucs2length(str2) {
|
|
4758
|
+
const len = str2.length;
|
|
4759
4759
|
let length = 0;
|
|
4760
4760
|
let pos = 0;
|
|
4761
4761
|
let value;
|
|
4762
4762
|
while (pos < len) {
|
|
4763
4763
|
length++;
|
|
4764
|
-
value =
|
|
4764
|
+
value = str2.charCodeAt(pos++);
|
|
4765
4765
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
4766
|
-
value =
|
|
4766
|
+
value = str2.charCodeAt(pos);
|
|
4767
4767
|
if ((value & 64512) === 56320)
|
|
4768
4768
|
pos++;
|
|
4769
4769
|
}
|
|
@@ -6646,8 +6646,8 @@ var require_formats = __commonJS({
|
|
|
6646
6646
|
}
|
|
6647
6647
|
var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
6648
6648
|
var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
6649
|
-
function date5(
|
|
6650
|
-
const matches = DATE.exec(
|
|
6649
|
+
function date5(str2) {
|
|
6650
|
+
const matches = DATE.exec(str2);
|
|
6651
6651
|
if (!matches)
|
|
6652
6652
|
return false;
|
|
6653
6653
|
const year = +matches[1];
|
|
@@ -6666,8 +6666,8 @@ var require_formats = __commonJS({
|
|
|
6666
6666
|
}
|
|
6667
6667
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6668
6668
|
function getTime(strictTimeZone) {
|
|
6669
|
-
return function time3(
|
|
6670
|
-
const matches = TIME.exec(
|
|
6669
|
+
return function time3(str2) {
|
|
6670
|
+
const matches = TIME.exec(str2);
|
|
6671
6671
|
if (!matches)
|
|
6672
6672
|
return false;
|
|
6673
6673
|
const hr = +matches[1];
|
|
@@ -6713,8 +6713,8 @@ var require_formats = __commonJS({
|
|
|
6713
6713
|
var DATE_TIME_SEPARATOR = /t|\s/i;
|
|
6714
6714
|
function getDateTime(strictTimeZone) {
|
|
6715
6715
|
const time3 = getTime(strictTimeZone);
|
|
6716
|
-
return function date_time(
|
|
6717
|
-
const dateTime =
|
|
6716
|
+
return function date_time(str2) {
|
|
6717
|
+
const dateTime = str2.split(DATE_TIME_SEPARATOR);
|
|
6718
6718
|
return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
|
|
6719
6719
|
};
|
|
6720
6720
|
}
|
|
@@ -6739,13 +6739,13 @@ var require_formats = __commonJS({
|
|
|
6739
6739
|
}
|
|
6740
6740
|
var NOT_URI_FRAGMENT = /\/|:/;
|
|
6741
6741
|
var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
6742
|
-
function uri(
|
|
6743
|
-
return NOT_URI_FRAGMENT.test(
|
|
6742
|
+
function uri(str2) {
|
|
6743
|
+
return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
|
|
6744
6744
|
}
|
|
6745
6745
|
var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
|
|
6746
|
-
function byte(
|
|
6746
|
+
function byte(str2) {
|
|
6747
6747
|
BYTE.lastIndex = 0;
|
|
6748
|
-
return BYTE.test(
|
|
6748
|
+
return BYTE.test(str2);
|
|
6749
6749
|
}
|
|
6750
6750
|
var MIN_INT32 = -(2 ** 31);
|
|
6751
6751
|
var MAX_INT32 = 2 ** 31 - 1;
|
|
@@ -6759,11 +6759,11 @@ var require_formats = __commonJS({
|
|
|
6759
6759
|
return true;
|
|
6760
6760
|
}
|
|
6761
6761
|
var Z_ANCHOR = /[^\\]\\Z/;
|
|
6762
|
-
function regex(
|
|
6763
|
-
if (Z_ANCHOR.test(
|
|
6762
|
+
function regex(str2) {
|
|
6763
|
+
if (Z_ANCHOR.test(str2))
|
|
6764
6764
|
return false;
|
|
6765
6765
|
try {
|
|
6766
|
-
new RegExp(
|
|
6766
|
+
new RegExp(str2);
|
|
6767
6767
|
return true;
|
|
6768
6768
|
} catch (e) {
|
|
6769
6769
|
return false;
|
|
@@ -6873,12 +6873,12 @@ var require_dist = __commonJS({
|
|
|
6873
6873
|
throw new Error(`Unknown format "${name}"`);
|
|
6874
6874
|
return f;
|
|
6875
6875
|
};
|
|
6876
|
-
function addFormats(ajv, list,
|
|
6876
|
+
function addFormats(ajv, list, fs14, exportName) {
|
|
6877
6877
|
var _a3;
|
|
6878
6878
|
var _b;
|
|
6879
6879
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6880
6880
|
for (const f of list)
|
|
6881
|
-
ajv.addFormat(f,
|
|
6881
|
+
ajv.addFormat(f, fs14[f]);
|
|
6882
6882
|
}
|
|
6883
6883
|
module2.exports = exports2 = formatsPlugin;
|
|
6884
6884
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -7245,8 +7245,8 @@ function getErrorMap() {
|
|
|
7245
7245
|
|
|
7246
7246
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
7247
7247
|
var makeIssue = (params) => {
|
|
7248
|
-
const { data, path:
|
|
7249
|
-
const fullPath = [...
|
|
7248
|
+
const { data, path: path17, errorMaps, issueData } = params;
|
|
7249
|
+
const fullPath = [...path17, ...issueData.path || []];
|
|
7250
7250
|
const fullIssue = {
|
|
7251
7251
|
...issueData,
|
|
7252
7252
|
path: fullPath
|
|
@@ -7361,11 +7361,11 @@ var errorUtil;
|
|
|
7361
7361
|
|
|
7362
7362
|
// node_modules/zod/v3/types.js
|
|
7363
7363
|
var ParseInputLazyPath = class {
|
|
7364
|
-
constructor(parent, value,
|
|
7364
|
+
constructor(parent, value, path17, key) {
|
|
7365
7365
|
this._cachedPath = [];
|
|
7366
7366
|
this.parent = parent;
|
|
7367
7367
|
this.data = value;
|
|
7368
|
-
this._path =
|
|
7368
|
+
this._path = path17;
|
|
7369
7369
|
this._key = key;
|
|
7370
7370
|
}
|
|
7371
7371
|
get path() {
|
|
@@ -11285,10 +11285,10 @@ function mergeDefs(...defs) {
|
|
|
11285
11285
|
function cloneDef(schema2) {
|
|
11286
11286
|
return mergeDefs(schema2._zod.def);
|
|
11287
11287
|
}
|
|
11288
|
-
function getElementAtPath(obj,
|
|
11289
|
-
if (!
|
|
11288
|
+
function getElementAtPath(obj, path17) {
|
|
11289
|
+
if (!path17)
|
|
11290
11290
|
return obj;
|
|
11291
|
-
return
|
|
11291
|
+
return path17.reduce((acc, key) => acc?.[key], obj);
|
|
11292
11292
|
}
|
|
11293
11293
|
function promiseAllObject(promisesObj) {
|
|
11294
11294
|
const keys = Object.keys(promisesObj);
|
|
@@ -11303,14 +11303,14 @@ function promiseAllObject(promisesObj) {
|
|
|
11303
11303
|
}
|
|
11304
11304
|
function randomString(length = 10) {
|
|
11305
11305
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
11306
|
-
let
|
|
11306
|
+
let str2 = "";
|
|
11307
11307
|
for (let i = 0; i < length; i++) {
|
|
11308
|
-
|
|
11308
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
11309
11309
|
}
|
|
11310
|
-
return
|
|
11310
|
+
return str2;
|
|
11311
11311
|
}
|
|
11312
|
-
function esc(
|
|
11313
|
-
return JSON.stringify(
|
|
11312
|
+
function esc(str2) {
|
|
11313
|
+
return JSON.stringify(str2);
|
|
11314
11314
|
}
|
|
11315
11315
|
function slugify(input) {
|
|
11316
11316
|
return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -11424,8 +11424,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
|
|
|
11424
11424
|
"symbol",
|
|
11425
11425
|
"undefined"
|
|
11426
11426
|
]);
|
|
11427
|
-
function escapeRegex(
|
|
11428
|
-
return
|
|
11427
|
+
function escapeRegex(str2) {
|
|
11428
|
+
return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11429
11429
|
}
|
|
11430
11430
|
function clone(inst, def, params) {
|
|
11431
11431
|
const cl = new inst._zod.constr(def ?? inst._zod.def);
|
|
@@ -11697,11 +11697,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11697
11697
|
}
|
|
11698
11698
|
return false;
|
|
11699
11699
|
}
|
|
11700
|
-
function prefixIssues(
|
|
11700
|
+
function prefixIssues(path17, issues) {
|
|
11701
11701
|
return issues.map((iss) => {
|
|
11702
11702
|
var _a3;
|
|
11703
11703
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
11704
|
-
iss.path.unshift(
|
|
11704
|
+
iss.path.unshift(path17);
|
|
11705
11705
|
return iss;
|
|
11706
11706
|
});
|
|
11707
11707
|
}
|
|
@@ -11848,16 +11848,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11848
11848
|
}
|
|
11849
11849
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
11850
11850
|
const fieldErrors = { _errors: [] };
|
|
11851
|
-
const processError = (error52,
|
|
11851
|
+
const processError = (error52, path17 = []) => {
|
|
11852
11852
|
for (const issue2 of error52.issues) {
|
|
11853
11853
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
11854
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
11854
|
+
issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
|
|
11855
11855
|
} else if (issue2.code === "invalid_key") {
|
|
11856
|
-
processError({ issues: issue2.issues }, [...
|
|
11856
|
+
processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
|
|
11857
11857
|
} else if (issue2.code === "invalid_element") {
|
|
11858
|
-
processError({ issues: issue2.issues }, [...
|
|
11858
|
+
processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
|
|
11859
11859
|
} else {
|
|
11860
|
-
const fullpath = [...
|
|
11860
|
+
const fullpath = [...path17, ...issue2.path];
|
|
11861
11861
|
if (fullpath.length === 0) {
|
|
11862
11862
|
fieldErrors._errors.push(mapper(issue2));
|
|
11863
11863
|
} else {
|
|
@@ -11884,17 +11884,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11884
11884
|
}
|
|
11885
11885
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
11886
11886
|
const result = { errors: [] };
|
|
11887
|
-
const processError = (error52,
|
|
11887
|
+
const processError = (error52, path17 = []) => {
|
|
11888
11888
|
var _a3, _b;
|
|
11889
11889
|
for (const issue2 of error52.issues) {
|
|
11890
11890
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
11891
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
11891
|
+
issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
|
|
11892
11892
|
} else if (issue2.code === "invalid_key") {
|
|
11893
|
-
processError({ issues: issue2.issues }, [...
|
|
11893
|
+
processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
|
|
11894
11894
|
} else if (issue2.code === "invalid_element") {
|
|
11895
|
-
processError({ issues: issue2.issues }, [...
|
|
11895
|
+
processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
|
|
11896
11896
|
} else {
|
|
11897
|
-
const fullpath = [...
|
|
11897
|
+
const fullpath = [...path17, ...issue2.path];
|
|
11898
11898
|
if (fullpath.length === 0) {
|
|
11899
11899
|
result.errors.push(mapper(issue2));
|
|
11900
11900
|
continue;
|
|
@@ -11926,8 +11926,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
11926
11926
|
}
|
|
11927
11927
|
function toDotPath(_path) {
|
|
11928
11928
|
const segs = [];
|
|
11929
|
-
const
|
|
11930
|
-
for (const seg of
|
|
11929
|
+
const path17 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
11930
|
+
for (const seg of path17) {
|
|
11931
11931
|
if (typeof seg === "number")
|
|
11932
11932
|
segs.push(`[${seg}]`);
|
|
11933
11933
|
else if (typeof seg === "symbol")
|
|
@@ -25052,13 +25052,13 @@ function resolveRef(ref, ctx) {
|
|
|
25052
25052
|
if (!ref.startsWith("#")) {
|
|
25053
25053
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
25054
25054
|
}
|
|
25055
|
-
const
|
|
25056
|
-
if (
|
|
25055
|
+
const path17 = ref.slice(1).split("/").filter(Boolean);
|
|
25056
|
+
if (path17.length === 0) {
|
|
25057
25057
|
return ctx.rootSchema;
|
|
25058
25058
|
}
|
|
25059
25059
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
25060
|
-
if (
|
|
25061
|
-
const key =
|
|
25060
|
+
if (path17[0] === defsKey) {
|
|
25061
|
+
const key = path17[1];
|
|
25062
25062
|
if (!key || !ctx.defs[key]) {
|
|
25063
25063
|
throw new Error(`Reference not found: ${ref}`);
|
|
25064
25064
|
}
|
|
@@ -28828,7 +28828,7 @@ var Protocol = class {
|
|
|
28828
28828
|
return;
|
|
28829
28829
|
}
|
|
28830
28830
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
28831
|
-
await new Promise((
|
|
28831
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
28832
28832
|
options?.signal?.throwIfAborted();
|
|
28833
28833
|
}
|
|
28834
28834
|
} catch (error51) {
|
|
@@ -28845,7 +28845,7 @@ var Protocol = class {
|
|
|
28845
28845
|
*/
|
|
28846
28846
|
request(request, resultSchema, options) {
|
|
28847
28847
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
28848
|
-
return new Promise((
|
|
28848
|
+
return new Promise((resolve8, reject) => {
|
|
28849
28849
|
const earlyReject = (error51) => {
|
|
28850
28850
|
reject(error51);
|
|
28851
28851
|
};
|
|
@@ -28923,7 +28923,7 @@ var Protocol = class {
|
|
|
28923
28923
|
if (!parseResult.success) {
|
|
28924
28924
|
reject(parseResult.error);
|
|
28925
28925
|
} else {
|
|
28926
|
-
|
|
28926
|
+
resolve8(parseResult.data);
|
|
28927
28927
|
}
|
|
28928
28928
|
} catch (error51) {
|
|
28929
28929
|
reject(error51);
|
|
@@ -29184,12 +29184,12 @@ var Protocol = class {
|
|
|
29184
29184
|
}
|
|
29185
29185
|
} catch {
|
|
29186
29186
|
}
|
|
29187
|
-
return new Promise((
|
|
29187
|
+
return new Promise((resolve8, reject) => {
|
|
29188
29188
|
if (signal.aborted) {
|
|
29189
29189
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
29190
29190
|
return;
|
|
29191
29191
|
}
|
|
29192
|
-
const timeoutId = setTimeout(
|
|
29192
|
+
const timeoutId = setTimeout(resolve8, interval);
|
|
29193
29193
|
signal.addEventListener("abort", () => {
|
|
29194
29194
|
clearTimeout(timeoutId);
|
|
29195
29195
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -30289,7 +30289,7 @@ var McpServer = class {
|
|
|
30289
30289
|
let task = createTaskResult.task;
|
|
30290
30290
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
30291
30291
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
30292
|
-
await new Promise((
|
|
30292
|
+
await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
|
|
30293
30293
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
30294
30294
|
if (!updatedTask) {
|
|
30295
30295
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -30938,12 +30938,12 @@ var StdioServerTransport = class {
|
|
|
30938
30938
|
this.onclose?.();
|
|
30939
30939
|
}
|
|
30940
30940
|
send(message) {
|
|
30941
|
-
return new Promise((
|
|
30941
|
+
return new Promise((resolve8) => {
|
|
30942
30942
|
const json2 = serializeMessage(message);
|
|
30943
30943
|
if (this._stdout.write(json2)) {
|
|
30944
|
-
|
|
30944
|
+
resolve8();
|
|
30945
30945
|
} else {
|
|
30946
|
-
this._stdout.once("drain",
|
|
30946
|
+
this._stdout.once("drain", resolve8);
|
|
30947
30947
|
}
|
|
30948
30948
|
});
|
|
30949
30949
|
}
|
|
@@ -31410,7 +31410,8 @@ function modulePaths(moduleDir) {
|
|
|
31410
31410
|
jobs: path.join(root, "logic", "jobs.json"),
|
|
31411
31411
|
triggers: path.join(root, "logic", "triggers.json"),
|
|
31412
31412
|
webhooks: path.join(root, "logic", "webhooks.json"),
|
|
31413
|
-
settings: path.join(root, "settings.json")
|
|
31413
|
+
settings: path.join(root, "settings.json"),
|
|
31414
|
+
domains: path.join(root, "domains.json")
|
|
31414
31415
|
};
|
|
31415
31416
|
}
|
|
31416
31417
|
function readJson(absPath) {
|
|
@@ -31460,6 +31461,7 @@ function withTodayStamp(manifest) {
|
|
|
31460
31461
|
return { ...manifest, updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) };
|
|
31461
31462
|
}
|
|
31462
31463
|
var PHASE_STATE_FILE = "docs/phase.json";
|
|
31464
|
+
var BUILD_PHASES = ["1", "2", "3", "4", "5", "6"];
|
|
31463
31465
|
function phaseStateJson(state) {
|
|
31464
31466
|
return JSON.stringify(state, null, " ") + "\n";
|
|
31465
31467
|
}
|
|
@@ -31472,6 +31474,21 @@ function readPhaseState(moduleDir) {
|
|
|
31472
31474
|
return null;
|
|
31473
31475
|
}
|
|
31474
31476
|
}
|
|
31477
|
+
function markPhase(moduleDir, phase, opts = {}) {
|
|
31478
|
+
const prior = readPhaseState(moduleDir) ?? {};
|
|
31479
|
+
const state = {
|
|
31480
|
+
...prior,
|
|
31481
|
+
phases: {
|
|
31482
|
+
...prior.phases ?? {},
|
|
31483
|
+
[phase]: {
|
|
31484
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
31485
|
+
...opts.skipped ? { skipped: true } : {},
|
|
31486
|
+
...opts.note ? { note: opts.note } : {}
|
|
31487
|
+
}
|
|
31488
|
+
}
|
|
31489
|
+
};
|
|
31490
|
+
return { state, json: phaseStateJson(state) };
|
|
31491
|
+
}
|
|
31475
31492
|
function isReadyToScaffold(moduleDir) {
|
|
31476
31493
|
const state = readPhaseState(moduleDir);
|
|
31477
31494
|
if (state && typeof state.readyToScaffold === "boolean") return state.readyToScaffold;
|
|
@@ -31555,6 +31572,36 @@ function assertSecurityCoverage(moduleDir) {
|
|
|
31555
31572
|
}
|
|
31556
31573
|
return void 0;
|
|
31557
31574
|
}
|
|
31575
|
+
var KEY_SEP = "::";
|
|
31576
|
+
function compositeKey(...parts) {
|
|
31577
|
+
return parts.join(KEY_SEP);
|
|
31578
|
+
}
|
|
31579
|
+
function walkFolders(root) {
|
|
31580
|
+
const out = [];
|
|
31581
|
+
const visit = (node, trail) => {
|
|
31582
|
+
const children = node.children ?? {};
|
|
31583
|
+
for (const [code2, child] of Object.entries(children)) {
|
|
31584
|
+
if (!child || typeof child !== "object") continue;
|
|
31585
|
+
const next = [...trail, code2];
|
|
31586
|
+
out.push({ code: code2, path: next.join("/"), node: child });
|
|
31587
|
+
visit(child, next);
|
|
31588
|
+
}
|
|
31589
|
+
};
|
|
31590
|
+
visit(root, []);
|
|
31591
|
+
return out;
|
|
31592
|
+
}
|
|
31593
|
+
function duplicateFolderCodes(root) {
|
|
31594
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
31595
|
+
for (const f of walkFolders(root)) {
|
|
31596
|
+
const paths = byCode.get(f.code) ?? [];
|
|
31597
|
+
paths.push(f.path);
|
|
31598
|
+
byCode.set(f.code, paths);
|
|
31599
|
+
}
|
|
31600
|
+
for (const [code2, paths] of byCode) {
|
|
31601
|
+
if (paths.length < 2) byCode.delete(code2);
|
|
31602
|
+
}
|
|
31603
|
+
return byCode;
|
|
31604
|
+
}
|
|
31558
31605
|
var TRAIT_CODES = traits.map((t) => t.cd);
|
|
31559
31606
|
var TRAIT_CODE_SET = new Set(TRAIT_CODES);
|
|
31560
31607
|
var traitsInput = external_exports.array(external_exports.string()).default(["identity", "audit"]).superRefine((arr, ctx) => {
|
|
@@ -31569,9 +31616,25 @@ var traitsInput = external_exports.array(external_exports.string()).default(["id
|
|
|
31569
31616
|
}).describe(
|
|
31570
31617
|
"Entity trait codes \u2014 identity, audit, audit-full, soft-delete, sorting, postable, accumulation, ledger, period. 'identity' makes the PK '{entity}_id'. Traits expand into columns server-side at install; list only the codes."
|
|
31571
31618
|
);
|
|
31619
|
+
function unknownTraits(traitCodes) {
|
|
31620
|
+
return traitCodes.filter((cd) => !TRAIT_CODE_SET.has(cd));
|
|
31621
|
+
}
|
|
31622
|
+
function assertKnownTraits(traitCodes, entityName) {
|
|
31623
|
+
const bad = unknownTraits(traitCodes);
|
|
31624
|
+
if (bad.length === 0) return;
|
|
31625
|
+
throw new Error(
|
|
31626
|
+
`Entity '${entityName}' declares unknown trait(s): ${bad.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is silently ignored when columns are expanded, so its columns would quietly go missing rather than fail loudly. (See dforge://reference/traits.)`
|
|
31627
|
+
);
|
|
31628
|
+
}
|
|
31572
31629
|
function withTraits(entity, traitCodes) {
|
|
31573
31630
|
return { ...entity, traits: [...traitCodes] };
|
|
31574
31631
|
}
|
|
31632
|
+
function withIdentityToString(entity, entityName, traitCodes) {
|
|
31633
|
+
const ts = Object.prototype.hasOwnProperty.call(entity, "toString") ? entity.toString : void 0;
|
|
31634
|
+
if (typeof ts !== "string" || ts !== "{id}") return entity;
|
|
31635
|
+
const pk = traitCodes.includes("identity") ? `${entityName}_id` : "id";
|
|
31636
|
+
return { ...entity, toString: `{${pk}}` };
|
|
31637
|
+
}
|
|
31575
31638
|
|
|
31576
31639
|
// src/tools/create-module.ts
|
|
31577
31640
|
var createModuleSchema = {
|
|
@@ -31652,7 +31715,11 @@ function createModuleFiles(args) {
|
|
|
31652
31715
|
};
|
|
31653
31716
|
write("manifest.json", buildManifest(opts, moduleId));
|
|
31654
31717
|
for (const e of opts.entities) {
|
|
31655
|
-
|
|
31718
|
+
const traits2 = traitsByName[e.name];
|
|
31719
|
+
write(
|
|
31720
|
+
`entities/${e.name}.json`,
|
|
31721
|
+
withIdentityToString(withTraits(buildEntity(e), traits2), e.name, traits2)
|
|
31722
|
+
);
|
|
31656
31723
|
}
|
|
31657
31724
|
write("ui/data_views.json", buildDataViews(opts.entities));
|
|
31658
31725
|
write("ui/folders.json", buildFolders(opts));
|
|
@@ -31736,8 +31803,15 @@ Total FKs: <N>
|
|
|
31736
31803
|
*Approved: <date>*
|
|
31737
31804
|
`;
|
|
31738
31805
|
var planModuleSchema = {
|
|
31739
|
-
action: external_exports.enum([
|
|
31740
|
-
"
|
|
31806
|
+
action: external_exports.enum([
|
|
31807
|
+
"check",
|
|
31808
|
+
"write_identity",
|
|
31809
|
+
"write_requirements",
|
|
31810
|
+
"write_design",
|
|
31811
|
+
"validate",
|
|
31812
|
+
"complete_phase"
|
|
31813
|
+
]).describe(
|
|
31814
|
+
"Sub-command: 'check' = current lifecycle state (Phase 0 sub-phase, or the build/ship phase once Phase 0 is done) + which skill to run next; 'write_identity' = Phase 0a; 'write_requirements' = Phase 0b; 'write_design' = Phase 0c; 'validate' = Phase 0d; 'complete_phase' = record a build phase (1-6) as done or deliberately skipped."
|
|
31741
31815
|
),
|
|
31742
31816
|
moduleDir: external_exports.string().describe(
|
|
31743
31817
|
"Absolute path to the module directory. Ask the user for this before the first call."
|
|
@@ -31767,7 +31841,15 @@ var planModuleSchema = {
|
|
|
31767
31841
|
})
|
|
31768
31842
|
).optional().describe(
|
|
31769
31843
|
"[validate] Semantic check results from agent evaluation. Omit on the first call (structural pre-check only). Provide after evaluating the returned semanticChecks."
|
|
31770
|
-
)
|
|
31844
|
+
),
|
|
31845
|
+
// ── complete_phase fields (Phases 1-6) ───────────────────────────────
|
|
31846
|
+
phase: external_exports.enum(BUILD_PHASES).optional().describe(
|
|
31847
|
+
"[complete_phase] Which build phase finished: '1' domain, '2' behavior, '3' views/reports, '4' polish, '5' security, '6' verify."
|
|
31848
|
+
),
|
|
31849
|
+
skipped: external_exports.boolean().optional().describe(
|
|
31850
|
+
"[complete_phase] True when the phase was deliberately skipped rather than done (only valid for the optional phases 2 and 4). Recording it stops a resumed session from re-proposing work the user already declined."
|
|
31851
|
+
),
|
|
31852
|
+
note: external_exports.string().optional().describe("[complete_phase] One-line record of what was done or why it was skipped.")
|
|
31771
31853
|
};
|
|
31772
31854
|
function planModule(rawArgs) {
|
|
31773
31855
|
const root = path3.resolve(rawArgs.moduleDir);
|
|
@@ -31782,7 +31864,168 @@ function planModule(rawArgs) {
|
|
|
31782
31864
|
return handleWriteDesign(root, rawArgs);
|
|
31783
31865
|
case "validate":
|
|
31784
31866
|
return handleValidate(root, rawArgs);
|
|
31867
|
+
case "complete_phase":
|
|
31868
|
+
return handleCompletePhase(root, rawArgs);
|
|
31869
|
+
}
|
|
31870
|
+
}
|
|
31871
|
+
var SKILL_FOR_PHASE = {
|
|
31872
|
+
"0a": "dforge-module-design",
|
|
31873
|
+
"0b": "dforge-module-design",
|
|
31874
|
+
"0c": "dforge-module-design",
|
|
31875
|
+
"0d": "dforge-module-design",
|
|
31876
|
+
"1": "dforge-module-build",
|
|
31877
|
+
"2": "dforge-module-build",
|
|
31878
|
+
"3": "dforge-module-build",
|
|
31879
|
+
"4": "dforge-module-build",
|
|
31880
|
+
"5": "dforge-module-build",
|
|
31881
|
+
"6": "dforge-module-ship"
|
|
31882
|
+
};
|
|
31883
|
+
var PHASE_TITLES = {
|
|
31884
|
+
"1": "Domain \u2014 entities, fields, relations (required)",
|
|
31885
|
+
"2": "Behavior \u2014 actions, triggers, jobs, webhooks (optional)",
|
|
31886
|
+
"3": "Views + menus, reports (3a grids required)",
|
|
31887
|
+
"4": "Polish \u2014 settings, translations, seed data (optional)",
|
|
31888
|
+
"5": "Security \u2014 roles + rights matrix (5a required)",
|
|
31889
|
+
"6": "Verify \u2014 validate, pack, install (required)"
|
|
31890
|
+
};
|
|
31891
|
+
var REQUIRED_PHASES = ["1", "3", "5", "6"];
|
|
31892
|
+
function gatherEvidence(root) {
|
|
31893
|
+
const paths = modulePaths(root);
|
|
31894
|
+
const empty = {
|
|
31895
|
+
scaffolded: false,
|
|
31896
|
+
entityCount: 0,
|
|
31897
|
+
entitiesWithoutFields: [],
|
|
31898
|
+
entitiesWithoutView: [],
|
|
31899
|
+
entitiesWithoutSelect: [],
|
|
31900
|
+
actionCount: 0,
|
|
31901
|
+
viewCount: 0,
|
|
31902
|
+
roleCount: 0
|
|
31903
|
+
};
|
|
31904
|
+
if (!fileExists(paths.manifest)) return empty;
|
|
31905
|
+
let manifest;
|
|
31906
|
+
try {
|
|
31907
|
+
manifest = JSON.parse(readFile(paths.manifest));
|
|
31908
|
+
} catch {
|
|
31909
|
+
return empty;
|
|
31910
|
+
}
|
|
31911
|
+
const entityMap = manifest.entities ?? {};
|
|
31912
|
+
const names = Object.keys(entityMap).filter((n) => !n.includes("."));
|
|
31913
|
+
const entitiesWithoutFields = [];
|
|
31914
|
+
for (const name of names) {
|
|
31915
|
+
const abs = path3.join(root, entityMap[name].replace(/^\.\//, ""));
|
|
31916
|
+
if (!fileExists(abs)) {
|
|
31917
|
+
entitiesWithoutFields.push(name);
|
|
31918
|
+
continue;
|
|
31919
|
+
}
|
|
31920
|
+
try {
|
|
31921
|
+
const e = JSON.parse(readFile(abs));
|
|
31922
|
+
if (Object.keys(e.fields ?? {}).length === 0) entitiesWithoutFields.push(name);
|
|
31923
|
+
} catch {
|
|
31924
|
+
entitiesWithoutFields.push(name);
|
|
31925
|
+
}
|
|
31926
|
+
}
|
|
31927
|
+
const views = readJsonOrDefault(paths.dataViews, {});
|
|
31928
|
+
const covered = /* @__PURE__ */ new Set();
|
|
31929
|
+
for (const v of Object.values(views)) {
|
|
31930
|
+
for (const s of v.dataSources ?? []) {
|
|
31931
|
+
if (typeof s.entityCode === "string") covered.add(s.entityCode);
|
|
31932
|
+
}
|
|
31933
|
+
}
|
|
31934
|
+
let entitiesWithoutSelect = [];
|
|
31935
|
+
try {
|
|
31936
|
+
entitiesWithoutSelect = checkSecurityCoverage(root).uncoveredEntities;
|
|
31937
|
+
} catch {
|
|
31938
|
+
entitiesWithoutSelect = names;
|
|
31939
|
+
}
|
|
31940
|
+
return {
|
|
31941
|
+
scaffolded: true,
|
|
31942
|
+
entityCount: names.length,
|
|
31943
|
+
entitiesWithoutFields,
|
|
31944
|
+
entitiesWithoutView: names.filter((n) => !covered.has(n)),
|
|
31945
|
+
entitiesWithoutSelect,
|
|
31946
|
+
actionCount: Object.keys(readJsonOrDefault(paths.actions, {})).length,
|
|
31947
|
+
viewCount: Object.keys(views).length,
|
|
31948
|
+
roleCount: Object.keys(readJsonOrDefault(paths.roles, {})).length
|
|
31949
|
+
};
|
|
31950
|
+
}
|
|
31951
|
+
function buildPhaseCheck(root) {
|
|
31952
|
+
const ledger = readPhaseState(root)?.phases ?? {};
|
|
31953
|
+
const ev = gatherEvidence(root);
|
|
31954
|
+
const recorded = (p) => Boolean(ledger[p]);
|
|
31955
|
+
const completed = BUILD_PHASES.filter(recorded);
|
|
31956
|
+
const gaps = [];
|
|
31957
|
+
if (!ev.scaffolded) {
|
|
31958
|
+
gaps.push("no manifest.json \u2014 the module has not been scaffolded (dforge_module_create).");
|
|
31959
|
+
} else {
|
|
31960
|
+
if (ev.entitiesWithoutFields.length > 0) {
|
|
31961
|
+
gaps.push(
|
|
31962
|
+
`Phase 1: entities with no fields yet: ${ev.entitiesWithoutFields.join(", ")}.`
|
|
31963
|
+
);
|
|
31964
|
+
}
|
|
31965
|
+
if (ev.entitiesWithoutView.length > 0) {
|
|
31966
|
+
gaps.push(
|
|
31967
|
+
`Phase 3a: entities with no data view: ${ev.entitiesWithoutView.join(", ")} (every entity needs a default grid).`
|
|
31968
|
+
);
|
|
31969
|
+
}
|
|
31970
|
+
if (ev.entitiesWithoutSelect.length > 0) {
|
|
31971
|
+
gaps.push(
|
|
31972
|
+
`Phase 5a: entities with no role granting Select: ${ev.entitiesWithoutSelect.join(", ")} (dforge_module_pack refuses to build until fixed).`
|
|
31973
|
+
);
|
|
31974
|
+
}
|
|
31975
|
+
}
|
|
31976
|
+
const next = BUILD_PHASES.find((p) => !recorded(p));
|
|
31977
|
+
const allRequiredDone = REQUIRED_PHASES.every(recorded);
|
|
31978
|
+
if (!next || allRequiredDone && gaps.length === 0) {
|
|
31979
|
+
return {
|
|
31980
|
+
summary: `Module complete \u2014 all build phases recorded${gaps.length ? " (with open gaps, see below)" : ""}.`,
|
|
31981
|
+
currentPhase: "complete",
|
|
31982
|
+
phase0: "complete",
|
|
31983
|
+
readyToScaffold: true,
|
|
31984
|
+
completed,
|
|
31985
|
+
pending: [],
|
|
31986
|
+
gaps,
|
|
31987
|
+
evidence: ev,
|
|
31988
|
+
nextStep: gaps.length > 0 ? "Every phase is recorded as done, but the module still shows the gaps listed above. Resolve them, then re-run dforge_module_validate." : "Nothing left to do. Run dforge_module_validate for a final check if the module has changed since install."
|
|
31989
|
+
};
|
|
31990
|
+
}
|
|
31991
|
+
return {
|
|
31992
|
+
summary: `Phase 0 complete. ${completed.length ? `Recorded: ${completed.join(", ")}. ` : ""}Next: Phase ${next} \u2014 ${PHASE_TITLES[next]}.`,
|
|
31993
|
+
currentPhase: next,
|
|
31994
|
+
phase0: "complete",
|
|
31995
|
+
readyToScaffold: true,
|
|
31996
|
+
completed,
|
|
31997
|
+
pending: BUILD_PHASES.filter((p) => !recorded(p)),
|
|
31998
|
+
optionalPhases: ["2", "4"],
|
|
31999
|
+
gaps,
|
|
32000
|
+
evidence: ev,
|
|
32001
|
+
nextSkill: SKILL_FOR_PHASE[next],
|
|
32002
|
+
nextStep: `Run the '${SKILL_FOR_PHASE[next]}' skill and work Phase ${next} (${PHASE_TITLES[next]}). ` + (gaps.length > 0 ? `Open gaps to resolve first: ${gaps.join(" ")} ` : "") + `When the phase is done (or the user declines an optional one), record it with dforge_module_plan({ action: 'complete_phase', moduleDir, phase: '${next}', note: '...' }).`
|
|
32003
|
+
};
|
|
32004
|
+
}
|
|
32005
|
+
function handleCompletePhase(root, args) {
|
|
32006
|
+
if (!args.phase) {
|
|
32007
|
+
throw new Error(
|
|
32008
|
+
`complete_phase needs a 'phase' \u2014 one of ${BUILD_PHASES.join(", ")}. (Phase 0 sub-phases are recorded by write_identity / write_requirements / write_design / validate.)`
|
|
32009
|
+
);
|
|
32010
|
+
}
|
|
32011
|
+
if (!isReadyToScaffold(root)) {
|
|
32012
|
+
throw new Error(
|
|
32013
|
+
"Phase 0 has not passed validation yet \u2014 there are no build phases to record. Call dforge_module_plan({ action: 'check', moduleDir }) to see what Phase 0 still needs."
|
|
32014
|
+
);
|
|
32015
|
+
}
|
|
32016
|
+
if (args.skipped && args.phase !== "2" && args.phase !== "4") {
|
|
32017
|
+
throw new Error(
|
|
32018
|
+
`Phase ${args.phase} (${PHASE_TITLES[args.phase]}) is required and can't be marked skipped. Only Phases 2 and 4 are optional.`
|
|
32019
|
+
);
|
|
31785
32020
|
}
|
|
32021
|
+
const { json: json2 } = markPhase(root, args.phase, { skipped: args.skipped, note: args.note });
|
|
32022
|
+
const { currentPhase, nextSkill, nextStep, gaps } = buildPhaseCheck(root);
|
|
32023
|
+
return {
|
|
32024
|
+
summary: `Recorded Phase ${args.phase} as ${args.skipped ? "SKIPPED" : "complete"}${args.note ? ` \u2014 ${args.note}` : ""}.`,
|
|
32025
|
+
files: { [P0.phaseState]: json2 },
|
|
32026
|
+
next: { currentPhase, nextSkill, nextStep, gaps },
|
|
32027
|
+
nextStep: `Write the updated ${P0.phaseState} to disk, then continue: ${nextStep}`
|
|
32028
|
+
};
|
|
31786
32029
|
}
|
|
31787
32030
|
function handleCheck(root) {
|
|
31788
32031
|
const claudePath = path3.join(root, P0.identity);
|
|
@@ -31800,16 +32043,7 @@ function handleCheck(root) {
|
|
|
31800
32043
|
if (has0d) completed.push("0d");
|
|
31801
32044
|
const allPhases = ["0a", "0b", "0c", "0d"];
|
|
31802
32045
|
const pending = allPhases.filter((p) => !completed.includes(p));
|
|
31803
|
-
if (has0d)
|
|
31804
|
-
return {
|
|
31805
|
-
summary: "Phase 0 complete \u2014 all docs present and validated.",
|
|
31806
|
-
currentPhase: "complete",
|
|
31807
|
-
completed,
|
|
31808
|
-
pending: [],
|
|
31809
|
-
readyToScaffold: true,
|
|
31810
|
-
nextStep: "All Phase 0 docs are in place. Call dforge_module_create({ moduleDir, code, displayName, entities, ... }) to scaffold the module."
|
|
31811
|
-
};
|
|
31812
|
-
}
|
|
32046
|
+
if (has0d) return buildPhaseCheck(root);
|
|
31813
32047
|
if (!has0a) {
|
|
31814
32048
|
return {
|
|
31815
32049
|
summary: "Phase 0 not started.",
|
|
@@ -32305,7 +32539,15 @@ function addEntityFiles(args) {
|
|
|
32305
32539
|
};
|
|
32306
32540
|
const files = {};
|
|
32307
32541
|
files["manifest.json"] = JSON.stringify(newManifest, null, " ") + "\n";
|
|
32308
|
-
files[`entities/${newEntity.name}.json`] = JSON.stringify(
|
|
32542
|
+
files[`entities/${newEntity.name}.json`] = JSON.stringify(
|
|
32543
|
+
withIdentityToString(
|
|
32544
|
+
withTraits(buildEntity(newEntity), args.entity.traits),
|
|
32545
|
+
args.entity.name,
|
|
32546
|
+
args.entity.traits
|
|
32547
|
+
),
|
|
32548
|
+
null,
|
|
32549
|
+
" "
|
|
32550
|
+
) + "\n";
|
|
32309
32551
|
files["ui/data_views.json"] = JSON.stringify(buildDataViews(allEntities), null, " ") + "\n";
|
|
32310
32552
|
files["ui/folders.json"] = JSON.stringify(buildFolders(opts), null, " ") + "\n";
|
|
32311
32553
|
files["ui/menus.json"] = JSON.stringify(buildMenus(opts), null, " ") + "\n";
|
|
@@ -32798,6 +33040,8 @@ function installModule(args) {
|
|
|
32798
33040
|
|
|
32799
33041
|
// src/tools/entity-field.ts
|
|
32800
33042
|
var path6 = __toESM(require("path"));
|
|
33043
|
+
|
|
33044
|
+
// src/tools/field-rules.ts
|
|
32801
33045
|
var FIELD_TYPE_ALIASES = {
|
|
32802
33046
|
integer: "number",
|
|
32803
33047
|
int: "number",
|
|
@@ -32815,6 +33059,141 @@ var FIELD_TYPE_ALIASES = {
|
|
|
32815
33059
|
select: "dropdown",
|
|
32816
33060
|
multiselect: "flags"
|
|
32817
33061
|
};
|
|
33062
|
+
var DB_DATATYPE_ALIASES = {
|
|
33063
|
+
boolean: "bool",
|
|
33064
|
+
string: "varchar",
|
|
33065
|
+
datetime: "timestamptz",
|
|
33066
|
+
timestamp: "timestamptz",
|
|
33067
|
+
integer: "int",
|
|
33068
|
+
number: ["int", "bigint", "numeric"],
|
|
33069
|
+
float: "numeric",
|
|
33070
|
+
decimal: "numeric",
|
|
33071
|
+
double: "numeric",
|
|
33072
|
+
uuid: "cuid"
|
|
33073
|
+
};
|
|
33074
|
+
function formatSuggestion(fix) {
|
|
33075
|
+
return Array.isArray(fix) ? fix.map((f) => `'${f}'`).join(" / ") : `'${fix}'`;
|
|
33076
|
+
}
|
|
33077
|
+
function checkFieldSpec(where, field) {
|
|
33078
|
+
const issues = [];
|
|
33079
|
+
if (!field || typeof field !== "object") {
|
|
33080
|
+
return [{ level: "error", message: `${where}: field spec is not an object.` }];
|
|
33081
|
+
}
|
|
33082
|
+
const v = field;
|
|
33083
|
+
const err = (message) => issues.push({ level: "error", message: `${where}: ${message}` });
|
|
33084
|
+
const warn = (message) => issues.push({ level: "warning", message: `${where}: ${message}` });
|
|
33085
|
+
if (v.defaultValue !== void 0 || v.default !== void 0) {
|
|
33086
|
+
err(
|
|
33087
|
+
`entity fields have no 'defaultValue'/'default' key (settings-only). Set a default with 'formula' (e.g. "formula": "'draft'" or "formula": "TODAY()"), a numberSequence, or DSL logic.`
|
|
33088
|
+
);
|
|
33089
|
+
}
|
|
33090
|
+
if (v.options !== void 0) {
|
|
33091
|
+
err(
|
|
33092
|
+
'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
|
|
33093
|
+
);
|
|
33094
|
+
}
|
|
33095
|
+
if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
|
|
33096
|
+
err(
|
|
33097
|
+
`flags '${v.flags}' contains invalid letters \u2014 use only V/I/E/M/H (e.g. VEM, VE, V, EM). U/S/P are not flag letters.`
|
|
33098
|
+
);
|
|
33099
|
+
}
|
|
33100
|
+
if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
|
|
33101
|
+
const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
|
|
33102
|
+
const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
|
|
33103
|
+
err(
|
|
33104
|
+
`fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
|
|
33105
|
+
);
|
|
33106
|
+
}
|
|
33107
|
+
const columnType = typeof v.columnType === "string" ? v.columnType : "";
|
|
33108
|
+
if (columnType.length > 0 && !getColumnType(columnType)) {
|
|
33109
|
+
err(
|
|
33110
|
+
`columnType '${columnType}' is invalid. Use 'R' (reference), 'S' (set/child list), or 'F' (formula) \u2014 or omit it for a plain data column. (A/L/G exist for register columns.)`
|
|
33111
|
+
);
|
|
33112
|
+
}
|
|
33113
|
+
if (typeof v.dbDatatype === "string" && v.dbDatatype.length > 0) {
|
|
33114
|
+
const base = v.dbDatatype.replace(/\(.*$/, "").trim().toLowerCase();
|
|
33115
|
+
const fix = DB_DATATYPE_ALIASES[base];
|
|
33116
|
+
if (fix) {
|
|
33117
|
+
err(
|
|
33118
|
+
`dbDatatype '${v.dbDatatype}' is not a SQL type \u2014 use ${formatSuggestion(fix)}. (dbDatatype = SQL type; fieldTypeCd = UI control. See dforge://reference/field-types.)`
|
|
33119
|
+
);
|
|
33120
|
+
} else if (isFieldTypeCd(base) && base !== "date" && base !== "time" && base !== "text") {
|
|
33121
|
+
err(
|
|
33122
|
+
`dbDatatype '${v.dbDatatype}' is a fieldTypeCd, not a SQL type. Use a SQL type (bool, varchar, text, int, bigint, numeric, timestamptz, date, time, cuid, json), or omit it \u2014 it's derived from fieldTypeCd.`
|
|
33123
|
+
);
|
|
33124
|
+
}
|
|
33125
|
+
}
|
|
33126
|
+
if (columnType === "F") {
|
|
33127
|
+
if (typeof v.baseDatatypeCd !== "string" || v.baseDatatypeCd.length === 0) {
|
|
33128
|
+
err("formula column (columnType 'F') requires 'baseDatatypeCd'. (See dforge://reference/formulas.)");
|
|
33129
|
+
}
|
|
33130
|
+
if (v.dbDatatype !== void 0) {
|
|
33131
|
+
err(
|
|
33132
|
+
"formula column (columnType 'F') must NOT set 'dbDatatype' \u2014 it owns no physical column. Use 'baseDatatypeCd' instead."
|
|
33133
|
+
);
|
|
33134
|
+
}
|
|
33135
|
+
if (typeof v.formula !== "string" || v.formula.trim() === "") {
|
|
33136
|
+
err("formula column (columnType 'F') requires a non-empty 'formula' expression.");
|
|
33137
|
+
}
|
|
33138
|
+
if (typeof v.flags === "string" && v.flags !== "V" && /^[VIEMH]+$/.test(v.flags)) {
|
|
33139
|
+
warn(
|
|
33140
|
+
`formula column has flags '${v.flags}' \u2014 a computed column is read-only, expected flags 'V'.`
|
|
33141
|
+
);
|
|
33142
|
+
}
|
|
33143
|
+
}
|
|
33144
|
+
if (columnType === "G") {
|
|
33145
|
+
if (typeof v.formula !== "string" || v.formula.trim() === "") {
|
|
33146
|
+
err("generated column (columnType 'G') requires a 'formula' expression.");
|
|
33147
|
+
}
|
|
33148
|
+
if (typeof v.dbDatatype !== "string" || v.dbDatatype.length === 0) {
|
|
33149
|
+
err(
|
|
33150
|
+
"generated column (columnType 'G') requires 'dbDatatype' \u2014 it's a physical, trigger-maintained column. (See dforge://reference/column-types.)"
|
|
33151
|
+
);
|
|
33152
|
+
}
|
|
33153
|
+
if (v.baseDatatypeCd !== void 0) {
|
|
33154
|
+
err("generated column (columnType 'G') must NOT set 'baseDatatypeCd' \u2014 use 'dbDatatype'.");
|
|
33155
|
+
}
|
|
33156
|
+
}
|
|
33157
|
+
if (columnType === "R") {
|
|
33158
|
+
const link = v.link;
|
|
33159
|
+
if (!link || typeof link !== "object") {
|
|
33160
|
+
err(
|
|
33161
|
+
"reference column (columnType 'R') requires a 'link' object { entity, thisKey, otherKey }. (See dforge://reference/column-types.)"
|
|
33162
|
+
);
|
|
33163
|
+
} else {
|
|
33164
|
+
for (const k of ["entity", "thisKey", "otherKey"]) {
|
|
33165
|
+
if (typeof link[k] !== "string" || link[k].length === 0) {
|
|
33166
|
+
err(`reference column's link.${k} is missing \u2014 link needs { entity, thisKey, otherKey }.`);
|
|
33167
|
+
}
|
|
33168
|
+
}
|
|
33169
|
+
if (link.otherKey === "id") {
|
|
33170
|
+
err(
|
|
33171
|
+
`link.otherKey is 'id' \u2014 the identity trait names the PK '{entity}_id' (e.g. '${String(link.entity ?? "target")}_id'), never 'id'.`
|
|
33172
|
+
);
|
|
33173
|
+
}
|
|
33174
|
+
}
|
|
33175
|
+
if (v.fieldTypeCd !== void 0 && v.fieldTypeCd !== "lookup") {
|
|
33176
|
+
warn(
|
|
33177
|
+
`reference column has fieldTypeCd '${String(v.fieldTypeCd)}' \u2014 the visible half of an FK+Reference pair is normally 'lookup'.`
|
|
33178
|
+
);
|
|
33179
|
+
}
|
|
33180
|
+
if (v.dbDatatype !== void 0) {
|
|
33181
|
+
err(
|
|
33182
|
+
"reference column (columnType 'R') must NOT set 'dbDatatype' \u2014 the physical column is the paired hidden FK (flags 'EM'), not this one."
|
|
33183
|
+
);
|
|
33184
|
+
}
|
|
33185
|
+
}
|
|
33186
|
+
return issues;
|
|
33187
|
+
}
|
|
33188
|
+
function parseSetAggregate(formula) {
|
|
33189
|
+
const m = formula.match(
|
|
33190
|
+
/\b(SUM|COUNT|AVG|MIN|MAX)\s*\(\s*\[([a-z][a-z0-9_]*)\]\s*\.\s*\[([a-z][a-z0-9_]*)\]\s*\)/i
|
|
33191
|
+
);
|
|
33192
|
+
if (!m) return null;
|
|
33193
|
+
return { agg: m[1].toUpperCase(), setField: m[2], childField: m[3] };
|
|
33194
|
+
}
|
|
33195
|
+
|
|
33196
|
+
// src/tools/entity-field.ts
|
|
32818
33197
|
function finalizeField(field) {
|
|
32819
33198
|
const ftc = field.fieldTypeCd;
|
|
32820
33199
|
if (typeof ftc !== "string" || field.dbDatatype !== void 0) return field;
|
|
@@ -32839,37 +33218,11 @@ var fieldSchema = external_exports.object({
|
|
|
32839
33218
|
link: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
32840
33219
|
params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
32841
33220
|
}).passthrough().superRefine((val, ctx) => {
|
|
32842
|
-
const
|
|
32843
|
-
|
|
32844
|
-
ctx.addIssue({
|
|
32845
|
-
code: external_exports.ZodIssueCode.custom,
|
|
32846
|
-
message: `entity fields have no 'defaultValue'/'default' key (settings-only). Set a default with 'formula' (e.g. "formula": "'draft'" or "formula": "TODAY()"), a numberSequence, or DSL logic.`
|
|
32847
|
-
});
|
|
32848
|
-
}
|
|
32849
|
-
if (v.options !== void 0) {
|
|
32850
|
-
ctx.addIssue({
|
|
32851
|
-
code: external_exports.ZodIssueCode.custom,
|
|
32852
|
-
message: 'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
|
|
32853
|
-
});
|
|
32854
|
-
}
|
|
32855
|
-
if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
|
|
33221
|
+
for (const issue2 of checkFieldSpec("field", val)) {
|
|
33222
|
+
if (issue2.level !== "error") continue;
|
|
32856
33223
|
ctx.addIssue({
|
|
32857
33224
|
code: external_exports.ZodIssueCode.custom,
|
|
32858
|
-
message:
|
|
32859
|
-
});
|
|
32860
|
-
}
|
|
32861
|
-
if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
|
|
32862
|
-
const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
|
|
32863
|
-
const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
|
|
32864
|
-
ctx.addIssue({
|
|
32865
|
-
code: external_exports.ZodIssueCode.custom,
|
|
32866
|
-
message: `fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
|
|
32867
|
-
});
|
|
32868
|
-
}
|
|
32869
|
-
if (typeof v.columnType === "string" && v.columnType.length > 0 && !getColumnType(v.columnType)) {
|
|
32870
|
-
ctx.addIssue({
|
|
32871
|
-
code: external_exports.ZodIssueCode.custom,
|
|
32872
|
-
message: `columnType '${v.columnType}' is invalid. Use 'R' (reference), 'S' (set/child list), or 'F' (formula) \u2014 or omit it for a plain data column. (A/L/G exist for register columns.)`
|
|
33225
|
+
message: issue2.message.replace(/^field: /, "")
|
|
32873
33226
|
});
|
|
32874
33227
|
}
|
|
32875
33228
|
}).describe(
|
|
@@ -32935,9 +33288,324 @@ function entityFieldModify(args) {
|
|
|
32935
33288
|
);
|
|
32936
33289
|
}
|
|
32937
33290
|
|
|
33291
|
+
// src/tools/entity-compose.ts
|
|
33292
|
+
var path7 = __toESM(require("path"));
|
|
33293
|
+
function loadEntity(moduleDir, entityName) {
|
|
33294
|
+
const { paths, manifest } = loadManifest(moduleDir);
|
|
33295
|
+
const entityPath = path7.join(paths.entitiesDir, `${entityName}.json`);
|
|
33296
|
+
try {
|
|
33297
|
+
const entity = readJson(entityPath);
|
|
33298
|
+
return { paths, manifest, entity, entityPath };
|
|
33299
|
+
} catch {
|
|
33300
|
+
throw new Error(
|
|
33301
|
+
`Entity '${entityName}' not found at entities/${entityName}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
|
|
33302
|
+
);
|
|
33303
|
+
}
|
|
33304
|
+
}
|
|
33305
|
+
var fieldsOf = (entity) => entity.fields ?? {};
|
|
33306
|
+
function nextOrderNum(fields) {
|
|
33307
|
+
let max = 0;
|
|
33308
|
+
for (const f of Object.values(fields)) {
|
|
33309
|
+
const n = typeof f?.orderNum === "number" ? f.orderNum : 0;
|
|
33310
|
+
if (n > max) max = n;
|
|
33311
|
+
}
|
|
33312
|
+
return Math.ceil((max + 10) / 10) * 10;
|
|
33313
|
+
}
|
|
33314
|
+
var pkOf = (entityCode) => `${entityCode.split(".").pop()}_id`;
|
|
33315
|
+
var pascal = (s) => s.split(/[._]/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
|
|
33316
|
+
var entityReferenceAddSchema = {
|
|
33317
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
33318
|
+
entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The CHILD entity \u2014 the one that gets the FK columns."),
|
|
33319
|
+
targetEntity: external_exports.string().regex(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/).describe(
|
|
33320
|
+
"The PARENT entity being pointed at. Cross-module dotted form ('fin.invoice') is allowed when it's a declared dependency."
|
|
33321
|
+
),
|
|
33322
|
+
name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe(
|
|
33323
|
+
"Name of the VISIBLE reference column, e.g. 'list' or 'customer'. The hidden FK column is named '<name>_id' unless fkField overrides it."
|
|
33324
|
+
),
|
|
33325
|
+
fkField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe("Override the hidden FK column name. Default: '<name>_id'."),
|
|
33326
|
+
label: external_exports.string().optional().describe("Display label for the reference column. Default: derived from name."),
|
|
33327
|
+
required: external_exports.boolean().default(true).describe("true \u2192 flags 'VEM' (required); false \u2192 'VE' (optional). Mirrors the FK's nullability."),
|
|
33328
|
+
targetPk: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
|
|
33329
|
+
"The parent's PK column. Default '{targetEntity}_id' (what the identity trait provides). Never 'id'."
|
|
33330
|
+
),
|
|
33331
|
+
orderNum: external_exports.number().int().optional().describe("Order of the reference column in the UI. Default: appended.")
|
|
33332
|
+
};
|
|
33333
|
+
function entityReferenceAdd(args) {
|
|
33334
|
+
const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
|
|
33335
|
+
const fields = fieldsOf(entity);
|
|
33336
|
+
const fkField = args.fkField ?? `${args.name}_id`;
|
|
33337
|
+
const targetPk = args.targetPk ?? pkOf(args.targetEntity);
|
|
33338
|
+
if (fkField === args.name) {
|
|
33339
|
+
throw new Error(
|
|
33340
|
+
`fkField and name are both '${fkField}' \u2014 an FK+Reference is TWO columns: a hidden FK ('${args.name}_id') plus a visible reference ('${args.name}'). They can't share a name.`
|
|
33341
|
+
);
|
|
33342
|
+
}
|
|
33343
|
+
if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
|
|
33344
|
+
throw new Error(
|
|
33345
|
+
`Column '${args.name}' already exists on '${args.entity}'. Remove it first (dforge_entity_field_remove) or pick a different name.`
|
|
33346
|
+
);
|
|
33347
|
+
}
|
|
33348
|
+
const existingFk = fields[fkField];
|
|
33349
|
+
if (existingFk && typeof existingFk.columnType === "string" && existingFk.columnType !== "D") {
|
|
33350
|
+
throw new Error(
|
|
33351
|
+
`Column '${fkField}' already exists on '${args.entity}' as a '${existingFk.columnType}' column, so it can't serve as the hidden FK. Pass a different fkField.`
|
|
33352
|
+
);
|
|
33353
|
+
}
|
|
33354
|
+
const reusedFk = Boolean(existingFk);
|
|
33355
|
+
const fkFixes = [];
|
|
33356
|
+
if (existingFk) {
|
|
33357
|
+
const flags2 = typeof existingFk.flags === "string" ? existingFk.flags : "";
|
|
33358
|
+
if (existingFk.dbDatatype !== "cuid") {
|
|
33359
|
+
fkFixes.push(
|
|
33360
|
+
`dbDatatype ${existingFk.dbDatatype === void 0 ? "(unset)" : `'${String(existingFk.dbDatatype)}'`} \u2192 'cuid' (the target PK type)`
|
|
33361
|
+
);
|
|
33362
|
+
}
|
|
33363
|
+
if (flags2 !== "EM") {
|
|
33364
|
+
fkFixes.push(`flags '${flags2 || "(unset)"}' \u2192 'EM' (hidden, not user-visible)`);
|
|
33365
|
+
}
|
|
33366
|
+
if (existingFk.fieldTypeCd !== void 0) {
|
|
33367
|
+
fkFixes.push(
|
|
33368
|
+
`dropped fieldTypeCd '${String(existingFk.fieldTypeCd)}' (the hidden FK has no UI control \u2014 the Reference column carries it)`
|
|
33369
|
+
);
|
|
33370
|
+
}
|
|
33371
|
+
if (existingFk.columnType !== void 0) {
|
|
33372
|
+
fkFixes.push(`dropped columnType '${String(existingFk.columnType)}' (a hidden FK is a plain data column)`);
|
|
33373
|
+
}
|
|
33374
|
+
}
|
|
33375
|
+
if (args.targetEntity === args.entity && !args.fkField) {
|
|
33376
|
+
if (fkField === pkOf(args.entity)) {
|
|
33377
|
+
throw new Error(
|
|
33378
|
+
`Derived FK column '${fkField}' collides with '${args.entity}'s own identity PK. Pass an explicit fkField (e.g. 'parent_id').`
|
|
33379
|
+
);
|
|
33380
|
+
}
|
|
33381
|
+
}
|
|
33382
|
+
const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
33383
|
+
const flags = args.required ? "VEM" : "VE";
|
|
33384
|
+
const base = args.orderNum ?? nextOrderNum(fields);
|
|
33385
|
+
if (existingFk) {
|
|
33386
|
+
const { fieldTypeCd: _ft, columnType: _ct, ...keep } = existingFk;
|
|
33387
|
+
fields[fkField] = {
|
|
33388
|
+
...keep,
|
|
33389
|
+
dbDatatype: "cuid",
|
|
33390
|
+
flags: "EM",
|
|
33391
|
+
orderNum: typeof existingFk.orderNum === "number" ? existingFk.orderNum : base,
|
|
33392
|
+
description: typeof existingFk.description === "string" ? existingFk.description : `${label} ID`
|
|
33393
|
+
};
|
|
33394
|
+
} else {
|
|
33395
|
+
fields[fkField] = {
|
|
33396
|
+
dbDatatype: "cuid",
|
|
33397
|
+
flags: "EM",
|
|
33398
|
+
orderNum: base,
|
|
33399
|
+
description: `${label} ID`
|
|
33400
|
+
};
|
|
33401
|
+
}
|
|
33402
|
+
fields[args.name] = {
|
|
33403
|
+
columnType: "R",
|
|
33404
|
+
fieldTypeCd: "lookup",
|
|
33405
|
+
flags,
|
|
33406
|
+
orderNum: base + 5,
|
|
33407
|
+
description: label,
|
|
33408
|
+
link: { entity: args.targetEntity, thisKey: fkField, otherKey: targetPk }
|
|
33409
|
+
};
|
|
33410
|
+
entity.fields = fields;
|
|
33411
|
+
const refs = entity.references ?? {};
|
|
33412
|
+
const refName = `FK_${pascal(args.entity)}_${pascal(args.name)}`;
|
|
33413
|
+
refs[refName] = {
|
|
33414
|
+
from: { field: fkField },
|
|
33415
|
+
to: { entity: args.targetEntity, field: targetPk }
|
|
33416
|
+
};
|
|
33417
|
+
entity.references = refs;
|
|
33418
|
+
const warnings = [];
|
|
33419
|
+
if (fkFixes.length > 0) {
|
|
33420
|
+
warnings.push(
|
|
33421
|
+
`Reused column '${fkField}' was not in the hidden-FK shape and has been normalized: ${fkFixes.join("; ")}. Re-check anything that referenced it as a plain column.`
|
|
33422
|
+
);
|
|
33423
|
+
}
|
|
33424
|
+
if (args.targetEntity.includes(".")) {
|
|
33425
|
+
warnings.push(
|
|
33426
|
+
`'${args.targetEntity}' is cross-module \u2014 make sure '${args.targetEntity.split(".")[0]}' is a declared dependency (dforge_dependency_add).`
|
|
33427
|
+
);
|
|
33428
|
+
}
|
|
33429
|
+
return makeResult(
|
|
33430
|
+
`Added reference '${args.name}' on '${args.entity}' \u2192 '${args.targetEntity}' (${reusedFk ? `reused existing FK '${fkField}'${fkFixes.length ? " (normalized)" : ""}` : `hidden FK '${fkField}'`} + Reference column + references.${refName}, ${args.required ? "required" : "optional"}).`,
|
|
33431
|
+
{
|
|
33432
|
+
[rel(paths.root, entityPath)]: jsonText(entity),
|
|
33433
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
33434
|
+
},
|
|
33435
|
+
warnings.length > 0 ? warnings.join("\n") : void 0
|
|
33436
|
+
);
|
|
33437
|
+
}
|
|
33438
|
+
var entityRollupAddSchema = {
|
|
33439
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
33440
|
+
entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The PARENT entity that gets the roll-up total."),
|
|
33441
|
+
name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Name of the new Generated column, e.g. 'total_amount'."),
|
|
33442
|
+
childEntity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The CHILD entity whose rows are aggregated."),
|
|
33443
|
+
childField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe(
|
|
33444
|
+
"The child column to aggregate. MUST be physical (a plain data column or a same-row Generated one) \u2014 aggregating a virtual F/R/S column fails install."
|
|
33445
|
+
),
|
|
33446
|
+
agg: external_exports.enum(["SUM", "COUNT", "AVG", "MIN", "MAX"]).default("SUM").describe("Aggregate function."),
|
|
33447
|
+
setField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
|
|
33448
|
+
"Name of the Set column on the parent that lists the child rows. Reused if it already exists; created if not. Default: '<childEntity>s'."
|
|
33449
|
+
),
|
|
33450
|
+
childFkField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
|
|
33451
|
+
"The FK column on the child pointing back at the parent \u2014 only needed when the Set column has to be created. Default: '{parentEntity}_id'."
|
|
33452
|
+
),
|
|
33453
|
+
label: external_exports.string().optional().describe("Display label. Default: derived from name."),
|
|
33454
|
+
dbDatatype: external_exports.string().optional().describe("SQL type of the total. Default: copied from the aggregated child column (numeric for AVG, int8 for COUNT)."),
|
|
33455
|
+
orderNum: external_exports.number().int().optional()
|
|
33456
|
+
};
|
|
33457
|
+
function entityRollupAdd(args) {
|
|
33458
|
+
const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
|
|
33459
|
+
const fields = fieldsOf(entity);
|
|
33460
|
+
if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
|
|
33461
|
+
throw new Error(`Column '${args.name}' already exists on '${args.entity}'.`);
|
|
33462
|
+
}
|
|
33463
|
+
const { entity: child } = loadEntity(args.moduleDir, args.childEntity);
|
|
33464
|
+
const childFields = fieldsOf(child);
|
|
33465
|
+
const childCol = childFields[args.childField];
|
|
33466
|
+
if (!childCol) {
|
|
33467
|
+
throw new Error(
|
|
33468
|
+
`'${args.childField}' is not a column on child entity '${args.childEntity}'. Its columns: ${Object.keys(childFields).join(", ") || "(none)"}.`
|
|
33469
|
+
);
|
|
33470
|
+
}
|
|
33471
|
+
const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
|
|
33472
|
+
if (childType === "F" || childType === "R" || childType === "S") {
|
|
33473
|
+
throw new Error(
|
|
33474
|
+
`'${args.childEntity}.${args.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column, so install fails with \`column old.${args.childField} does not exist\`. Aggregate a plain data column (or a same-row Generated one) instead. (See dforge://reference/column-types.)`
|
|
33475
|
+
);
|
|
33476
|
+
}
|
|
33477
|
+
const setField = args.setField ?? `${args.childEntity}s`;
|
|
33478
|
+
const existingSet = fields[setField];
|
|
33479
|
+
let createdSet = false;
|
|
33480
|
+
if (existingSet) {
|
|
33481
|
+
if (existingSet.columnType !== "S") {
|
|
33482
|
+
throw new Error(
|
|
33483
|
+
`Column '${setField}' already exists on '${args.entity}' but is not a set column (columnType 'S'). Pass a different setField.`
|
|
33484
|
+
);
|
|
33485
|
+
}
|
|
33486
|
+
const linked = existingSet.link?.entity;
|
|
33487
|
+
if (linked && linked !== args.childEntity) {
|
|
33488
|
+
throw new Error(
|
|
33489
|
+
`Set column '${setField}' on '${args.entity}' points at '${String(linked)}', not '${args.childEntity}'.`
|
|
33490
|
+
);
|
|
33491
|
+
}
|
|
33492
|
+
} else {
|
|
33493
|
+
const childFk = args.childFkField ?? `${args.entity}_id`;
|
|
33494
|
+
if (!childFields[childFk]) {
|
|
33495
|
+
throw new Error(
|
|
33496
|
+
`Can't create the set column '${setField}': the child '${args.childEntity}' has no FK column '${childFk}' pointing back at '${args.entity}'. Add the relation first with dforge_entity_reference_add({ entity: '${args.childEntity}', targetEntity: '${args.entity}', name: '${args.entity}' }), or pass childFkField explicitly.`
|
|
33497
|
+
);
|
|
33498
|
+
}
|
|
33499
|
+
fields[setField] = {
|
|
33500
|
+
columnType: "S",
|
|
33501
|
+
fieldTypeCd: "grid",
|
|
33502
|
+
flags: "VEM",
|
|
33503
|
+
orderNum: nextOrderNum(fields),
|
|
33504
|
+
description: pascal(setField).replace(/([a-z])([A-Z])/g, "$1 $2"),
|
|
33505
|
+
link: { entity: args.childEntity, thisKey: pkOf(args.entity), otherKey: childFk }
|
|
33506
|
+
};
|
|
33507
|
+
createdSet = true;
|
|
33508
|
+
}
|
|
33509
|
+
const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
33510
|
+
let dbDatatype = args.dbDatatype;
|
|
33511
|
+
if (!dbDatatype) {
|
|
33512
|
+
if (args.agg === "COUNT") dbDatatype = "int8";
|
|
33513
|
+
else if (args.agg === "AVG") dbDatatype = "numeric(18,4)";
|
|
33514
|
+
else if (typeof childCol.dbDatatype === "string" && childCol.dbDatatype) dbDatatype = childCol.dbDatatype;
|
|
33515
|
+
else if (typeof childCol.fieldTypeCd === "string") {
|
|
33516
|
+
dbDatatype = deriveDbDatatype(childCol.fieldTypeCd, {}) ?? "numeric(18,2)";
|
|
33517
|
+
} else dbDatatype = "numeric(18,2)";
|
|
33518
|
+
}
|
|
33519
|
+
fields[args.name] = {
|
|
33520
|
+
columnType: "G",
|
|
33521
|
+
dbDatatype,
|
|
33522
|
+
flags: "V",
|
|
33523
|
+
orderNum: args.orderNum ?? nextOrderNum(fields),
|
|
33524
|
+
description: label,
|
|
33525
|
+
formula: `${args.agg}([${setField}].[${args.childField}])`
|
|
33526
|
+
};
|
|
33527
|
+
entity.fields = fields;
|
|
33528
|
+
return makeResult(
|
|
33529
|
+
`Added roll-up '${args.name}' on '${args.entity}' = ${args.agg}([${setField}].[${args.childField}]) as a Generated column (${dbDatatype})${createdSet ? `, plus the set column '${setField}'` : ""}.`,
|
|
33530
|
+
{
|
|
33531
|
+
[rel(paths.root, entityPath)]: jsonText(entity),
|
|
33532
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
33533
|
+
}
|
|
33534
|
+
);
|
|
33535
|
+
}
|
|
33536
|
+
var statusOption = external_exports.union([
|
|
33537
|
+
external_exports.string(),
|
|
33538
|
+
external_exports.object({
|
|
33539
|
+
value: external_exports.string(),
|
|
33540
|
+
label: external_exports.string().optional(),
|
|
33541
|
+
color: external_exports.string().optional(),
|
|
33542
|
+
icon: external_exports.string().optional()
|
|
33543
|
+
}).passthrough()
|
|
33544
|
+
]);
|
|
33545
|
+
var entityStatusAddSchema = {
|
|
33546
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
33547
|
+
entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity to add the status column to."),
|
|
33548
|
+
name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).default("status").describe("Column name. Default 'status'."),
|
|
33549
|
+
options: external_exports.array(statusOption).min(2).describe(
|
|
33550
|
+
"The status values, in workflow order. Either bare strings ('draft') or objects ({ value, label?, color?, icon? }). A single-value status should be a checkbox instead."
|
|
33551
|
+
),
|
|
33552
|
+
initial: external_exports.string().optional().describe(
|
|
33553
|
+
"The value new records start at \u2014 emitted as a `formula` (entity fields have NO defaultValue key). Must be one of options. Default: the first option."
|
|
33554
|
+
),
|
|
33555
|
+
label: external_exports.string().optional().describe("Display label. Default: derived from name."),
|
|
33556
|
+
required: external_exports.boolean().default(true).describe("true \u2192 flags 'VEM'; false \u2192 'VE'."),
|
|
33557
|
+
orderNum: external_exports.number().int().optional()
|
|
33558
|
+
};
|
|
33559
|
+
function entityStatusAdd(args) {
|
|
33560
|
+
const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
|
|
33561
|
+
const fields = fieldsOf(entity);
|
|
33562
|
+
if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
|
|
33563
|
+
throw new Error(
|
|
33564
|
+
`Column '${args.name}' already exists on '${args.entity}'. Use dforge_entity_field_modify to change it.`
|
|
33565
|
+
);
|
|
33566
|
+
}
|
|
33567
|
+
const options = args.options.map(
|
|
33568
|
+
(o) => typeof o === "string" ? { value: o, label: o.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) } : { label: o.value.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), ...o }
|
|
33569
|
+
);
|
|
33570
|
+
const values = options.map((o) => o.value);
|
|
33571
|
+
const dupes = values.filter((v, i) => values.indexOf(v) !== i);
|
|
33572
|
+
if (dupes.length > 0) {
|
|
33573
|
+
throw new Error(`Duplicate status value(s): ${[...new Set(dupes)].join(", ")}.`);
|
|
33574
|
+
}
|
|
33575
|
+
const initial = args.initial ?? values[0];
|
|
33576
|
+
if (!values.includes(initial)) {
|
|
33577
|
+
throw new Error(
|
|
33578
|
+
`initial '${initial}' is not one of the declared options (${values.join(", ")}).`
|
|
33579
|
+
);
|
|
33580
|
+
}
|
|
33581
|
+
const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
33582
|
+
const maxLen = Math.max(20, ...values.map((v) => v.length));
|
|
33583
|
+
fields[args.name] = {
|
|
33584
|
+
dbDatatype: "varchar",
|
|
33585
|
+
fieldTypeCd: "dropdown",
|
|
33586
|
+
flags: args.required ? "VEM" : "VE",
|
|
33587
|
+
maxLen,
|
|
33588
|
+
orderNum: args.orderNum ?? nextOrderNum(fields),
|
|
33589
|
+
description: label,
|
|
33590
|
+
// Entity fields have no `defaultValue` key — the initial value is a
|
|
33591
|
+
// quoted literal formula.
|
|
33592
|
+
formula: `'${initial}'`,
|
|
33593
|
+
params: { options }
|
|
33594
|
+
};
|
|
33595
|
+
entity.fields = fields;
|
|
33596
|
+
return makeResult(
|
|
33597
|
+
`Added status column '${args.name}' on '${args.entity}' with ${values.length} options (${values.join(" \u2192 ")}), starting at '${initial}'.`,
|
|
33598
|
+
{
|
|
33599
|
+
[rel(paths.root, entityPath)]: jsonText(entity),
|
|
33600
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
33601
|
+
},
|
|
33602
|
+
`Status values are now fixed in the entity. Any action's canExecute: guard must reference one of: ${values.join(", ")}.`
|
|
33603
|
+
);
|
|
33604
|
+
}
|
|
33605
|
+
|
|
32938
33606
|
// src/tools/refactor.ts
|
|
32939
33607
|
var fs6 = __toESM(require("fs"));
|
|
32940
|
-
var
|
|
33608
|
+
var path8 = __toESM(require("path"));
|
|
32941
33609
|
var code = external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
|
|
32942
33610
|
var entityFieldRenameSchema = {
|
|
32943
33611
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
@@ -32956,7 +33624,7 @@ function entityFieldRename(args) {
|
|
|
32956
33624
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
32957
33625
|
const entityRel = (manifest.entities ?? {})[entityName];
|
|
32958
33626
|
if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
|
|
32959
|
-
const entityPath =
|
|
33627
|
+
const entityPath = path8.join(paths.root, entityRel.replace(/^\.\//, ""));
|
|
32960
33628
|
const entity = readJson(entityPath);
|
|
32961
33629
|
const fields = entity.fields ?? {};
|
|
32962
33630
|
if (!(oldName in fields)) throw new Error(`Field '${oldName}' not found on entity '${entityName}'.`);
|
|
@@ -32989,7 +33657,7 @@ function entityFieldRename(args) {
|
|
|
32989
33657
|
files[rel(paths.root, entityPath)] = jsonText(entity);
|
|
32990
33658
|
for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
|
|
32991
33659
|
if (oname === entityName || oname.includes(".")) continue;
|
|
32992
|
-
const op =
|
|
33660
|
+
const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
|
|
32993
33661
|
if (!fs6.existsSync(op)) continue;
|
|
32994
33662
|
const oe = readJson(op);
|
|
32995
33663
|
let touched = false;
|
|
@@ -33043,7 +33711,7 @@ function entityFieldRename(args) {
|
|
|
33043
33711
|
}
|
|
33044
33712
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
33045
33713
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
33046
|
-
const fp =
|
|
33714
|
+
const fp = path8.join(paths.seedDataDir, file2);
|
|
33047
33715
|
let data;
|
|
33048
33716
|
try {
|
|
33049
33717
|
data = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
@@ -33082,7 +33750,7 @@ function entityFieldRemove(args) {
|
|
|
33082
33750
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
33083
33751
|
const entityRel = (manifest.entities ?? {})[entityName];
|
|
33084
33752
|
if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
|
|
33085
|
-
const entityPath =
|
|
33753
|
+
const entityPath = path8.join(paths.root, entityRel.replace(/^\.\//, ""));
|
|
33086
33754
|
const entity = readJson(entityPath);
|
|
33087
33755
|
const fields = entity.fields ?? {};
|
|
33088
33756
|
if (!(fieldName in fields)) throw new Error(`Field '${fieldName}' not found on entity '${entityName}'.`);
|
|
@@ -33147,7 +33815,7 @@ function entityFieldRemove(args) {
|
|
|
33147
33815
|
}
|
|
33148
33816
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
33149
33817
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
33150
|
-
const fp =
|
|
33818
|
+
const fp = path8.join(paths.seedDataDir, file2);
|
|
33151
33819
|
let data;
|
|
33152
33820
|
try {
|
|
33153
33821
|
data = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
@@ -33174,7 +33842,7 @@ function entityFieldRemove(args) {
|
|
|
33174
33842
|
}
|
|
33175
33843
|
for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
|
|
33176
33844
|
if (oname === entityName || oname.includes(".")) continue;
|
|
33177
|
-
const op =
|
|
33845
|
+
const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
|
|
33178
33846
|
if (!fs6.existsSync(op)) continue;
|
|
33179
33847
|
let oe;
|
|
33180
33848
|
try {
|
|
@@ -33217,7 +33885,7 @@ function entityRename(args) {
|
|
|
33217
33885
|
const files = {};
|
|
33218
33886
|
const deletes = [];
|
|
33219
33887
|
const changes = [];
|
|
33220
|
-
const oldPath =
|
|
33888
|
+
const oldPath = path8.join(paths.root, entityMap[oldE].replace(/^\.\//, ""));
|
|
33221
33889
|
const entity = readJson(oldPath);
|
|
33222
33890
|
const usesIdentity = (entity.traits ?? []).includes("identity");
|
|
33223
33891
|
const oldPk = `${oldE}_id`;
|
|
@@ -33226,7 +33894,7 @@ function entityRename(args) {
|
|
|
33226
33894
|
entity.dbObject = newE;
|
|
33227
33895
|
changes.push("entity.dbObject");
|
|
33228
33896
|
}
|
|
33229
|
-
const newPath =
|
|
33897
|
+
const newPath = path8.join(paths.root, "entities", `${newE}.json`);
|
|
33230
33898
|
files[rel(paths.root, newPath)] = jsonText(entity);
|
|
33231
33899
|
deletes.push(rel(paths.root, oldPath));
|
|
33232
33900
|
changes.push(`entity file \u2192 entities/${newE}.json`);
|
|
@@ -33234,7 +33902,7 @@ function entityRename(args) {
|
|
|
33234
33902
|
manifest.entities[newE] = `./entities/${newE}.json`;
|
|
33235
33903
|
for (const [oname, orel] of Object.entries(entityMap)) {
|
|
33236
33904
|
if (oname === oldE || oname.includes(".")) continue;
|
|
33237
|
-
const op =
|
|
33905
|
+
const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
|
|
33238
33906
|
if (!fs6.existsSync(op)) continue;
|
|
33239
33907
|
const oe = readJson(op);
|
|
33240
33908
|
let touched = false;
|
|
@@ -33303,7 +33971,7 @@ function entityRename(args) {
|
|
|
33303
33971
|
}
|
|
33304
33972
|
const folders = readJsonOrDefault(paths.folders, {});
|
|
33305
33973
|
let foldersTouched = false;
|
|
33306
|
-
const
|
|
33974
|
+
const walkFolders2 = (node) => {
|
|
33307
33975
|
if (!node || typeof node !== "object") return;
|
|
33308
33976
|
const rec = node;
|
|
33309
33977
|
const ents = rec.entities;
|
|
@@ -33312,16 +33980,16 @@ function entityRename(args) {
|
|
|
33312
33980
|
foldersTouched = true;
|
|
33313
33981
|
}
|
|
33314
33982
|
const children = rec.children;
|
|
33315
|
-
if (children) for (const c of Object.values(children))
|
|
33983
|
+
if (children) for (const c of Object.values(children)) walkFolders2(c);
|
|
33316
33984
|
};
|
|
33317
|
-
|
|
33985
|
+
walkFolders2(folders);
|
|
33318
33986
|
if (foldersTouched) {
|
|
33319
33987
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
33320
33988
|
changes.push("folders.json (entity bindings)");
|
|
33321
33989
|
}
|
|
33322
33990
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
33323
33991
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
33324
|
-
const fp =
|
|
33992
|
+
const fp = path8.join(paths.seedDataDir, file2);
|
|
33325
33993
|
let data;
|
|
33326
33994
|
try {
|
|
33327
33995
|
data = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
@@ -33358,7 +34026,7 @@ function entityDelete(args) {
|
|
|
33358
34026
|
const deletes = [];
|
|
33359
34027
|
const changes = [];
|
|
33360
34028
|
const warnings = [];
|
|
33361
|
-
const ePath =
|
|
34029
|
+
const ePath = path8.join(paths.root, entityMap[target].replace(/^\.\//, ""));
|
|
33362
34030
|
if (fs6.existsSync(ePath)) deletes.push(rel(paths.root, ePath));
|
|
33363
34031
|
const { [target]: _dropped, ...restEntities } = entityMap;
|
|
33364
34032
|
void _dropped;
|
|
@@ -33366,7 +34034,7 @@ function entityDelete(args) {
|
|
|
33366
34034
|
changes.push("manifest.entities entry");
|
|
33367
34035
|
for (const [oname, orel] of Object.entries(entityMap)) {
|
|
33368
34036
|
if (oname === target || oname.includes(".")) continue;
|
|
33369
|
-
const op =
|
|
34037
|
+
const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
|
|
33370
34038
|
if (!fs6.existsSync(op)) continue;
|
|
33371
34039
|
let oe;
|
|
33372
34040
|
try {
|
|
@@ -33396,7 +34064,7 @@ function entityDelete(args) {
|
|
|
33396
34064
|
}
|
|
33397
34065
|
const folders = readJsonOrDefault(paths.folders, {});
|
|
33398
34066
|
let foldersTouched = false;
|
|
33399
|
-
const
|
|
34067
|
+
const walkFolders2 = (node) => {
|
|
33400
34068
|
if (!node || typeof node !== "object") return;
|
|
33401
34069
|
const rec = node;
|
|
33402
34070
|
const ents = rec.entities;
|
|
@@ -33405,9 +34073,9 @@ function entityDelete(args) {
|
|
|
33405
34073
|
foldersTouched = true;
|
|
33406
34074
|
}
|
|
33407
34075
|
const children = rec.children;
|
|
33408
|
-
if (children) for (const c of Object.values(children))
|
|
34076
|
+
if (children) for (const c of Object.values(children)) walkFolders2(c);
|
|
33409
34077
|
};
|
|
33410
|
-
|
|
34078
|
+
walkFolders2(folders);
|
|
33411
34079
|
if (foldersTouched) {
|
|
33412
34080
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
33413
34081
|
changes.push("folders.json (binding)");
|
|
@@ -33443,7 +34111,7 @@ function entityDelete(args) {
|
|
|
33443
34111
|
}
|
|
33444
34112
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
33445
34113
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
33446
|
-
const fp =
|
|
34114
|
+
const fp = path8.join(paths.seedDataDir, file2);
|
|
33447
34115
|
let data;
|
|
33448
34116
|
try {
|
|
33449
34117
|
data = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
@@ -33470,7 +34138,294 @@ function entityDelete(args) {
|
|
|
33470
34138
|
}
|
|
33471
34139
|
|
|
33472
34140
|
// src/tools/action-add.ts
|
|
33473
|
-
var
|
|
34141
|
+
var path9 = __toESM(require("path"));
|
|
34142
|
+
|
|
34143
|
+
// src/tools/dsl-check.ts
|
|
34144
|
+
var BLOCKS = ["params", "canExecute", "onBeforeStart", "execute"];
|
|
34145
|
+
var DSL_BUILTINS = /* @__PURE__ */ new Set([
|
|
34146
|
+
"addDays",
|
|
34147
|
+
"addSeconds",
|
|
34148
|
+
"callApi",
|
|
34149
|
+
"callProc",
|
|
34150
|
+
"callService",
|
|
34151
|
+
"delete",
|
|
34152
|
+
"detectDocument",
|
|
34153
|
+
"download",
|
|
34154
|
+
"entityLink",
|
|
34155
|
+
"error",
|
|
34156
|
+
"exit",
|
|
34157
|
+
"flush",
|
|
34158
|
+
"getFileInfo",
|
|
34159
|
+
"getFileUrl",
|
|
34160
|
+
"getRecord",
|
|
34161
|
+
"getRecordOrNull",
|
|
34162
|
+
"getSecret",
|
|
34163
|
+
"getSetting",
|
|
34164
|
+
"info",
|
|
34165
|
+
"insert",
|
|
34166
|
+
"nextNumber",
|
|
34167
|
+
"notify",
|
|
34168
|
+
"now",
|
|
34169
|
+
"ocrExtract",
|
|
34170
|
+
"preloadRef",
|
|
34171
|
+
"query",
|
|
34172
|
+
"select",
|
|
34173
|
+
"sendEmail",
|
|
34174
|
+
"tryParseJson",
|
|
34175
|
+
"update",
|
|
34176
|
+
"warn"
|
|
34177
|
+
]);
|
|
34178
|
+
var JS_RESERVED = /* @__PURE__ */ new Set([
|
|
34179
|
+
"if",
|
|
34180
|
+
"for",
|
|
34181
|
+
"while",
|
|
34182
|
+
"switch",
|
|
34183
|
+
"catch",
|
|
34184
|
+
"return",
|
|
34185
|
+
"typeof",
|
|
34186
|
+
"function",
|
|
34187
|
+
"do",
|
|
34188
|
+
"else",
|
|
34189
|
+
"in",
|
|
34190
|
+
"of",
|
|
34191
|
+
"new",
|
|
34192
|
+
"delete",
|
|
34193
|
+
"void",
|
|
34194
|
+
"throw",
|
|
34195
|
+
"try",
|
|
34196
|
+
"parseInt",
|
|
34197
|
+
"parseFloat",
|
|
34198
|
+
"isNaN",
|
|
34199
|
+
"isFinite",
|
|
34200
|
+
"String",
|
|
34201
|
+
"Number",
|
|
34202
|
+
"Boolean",
|
|
34203
|
+
"Array",
|
|
34204
|
+
"Object",
|
|
34205
|
+
"Date",
|
|
34206
|
+
"Math",
|
|
34207
|
+
"JSON",
|
|
34208
|
+
"RegExp",
|
|
34209
|
+
"Error"
|
|
34210
|
+
]);
|
|
34211
|
+
var FORMULA_ONLY = ["TODAY", "NOW"];
|
|
34212
|
+
function blank(src) {
|
|
34213
|
+
let out = "";
|
|
34214
|
+
let i = 0;
|
|
34215
|
+
while (i < src.length) {
|
|
34216
|
+
const c = src[i];
|
|
34217
|
+
if (c === "'" || c === '"') {
|
|
34218
|
+
const quote = c;
|
|
34219
|
+
out += " ";
|
|
34220
|
+
i++;
|
|
34221
|
+
while (i < src.length && src[i] !== quote) {
|
|
34222
|
+
if (src[i] === "\\") {
|
|
34223
|
+
out += " ";
|
|
34224
|
+
i += 2;
|
|
34225
|
+
continue;
|
|
34226
|
+
}
|
|
34227
|
+
out += src[i] === "\n" ? "\n" : " ";
|
|
34228
|
+
i++;
|
|
34229
|
+
}
|
|
34230
|
+
if (i < src.length) {
|
|
34231
|
+
out += " ";
|
|
34232
|
+
i++;
|
|
34233
|
+
}
|
|
34234
|
+
continue;
|
|
34235
|
+
}
|
|
34236
|
+
if (c === "#" || c === "/" && src[i + 1] === "/") {
|
|
34237
|
+
while (i < src.length && src[i] !== "\n") {
|
|
34238
|
+
out += " ";
|
|
34239
|
+
i++;
|
|
34240
|
+
}
|
|
34241
|
+
continue;
|
|
34242
|
+
}
|
|
34243
|
+
out += c;
|
|
34244
|
+
i++;
|
|
34245
|
+
}
|
|
34246
|
+
return out;
|
|
34247
|
+
}
|
|
34248
|
+
var lineOf = (src, index) => src.slice(0, index).split("\n").length;
|
|
34249
|
+
function readStringLiteral(src, start) {
|
|
34250
|
+
const quote = src[start];
|
|
34251
|
+
if (quote !== "'" && quote !== '"') return null;
|
|
34252
|
+
let value = "";
|
|
34253
|
+
let i = start + 1;
|
|
34254
|
+
while (i < src.length) {
|
|
34255
|
+
const c = src[i];
|
|
34256
|
+
if (c === "\\") {
|
|
34257
|
+
if (i + 1 < src.length) value += src[i + 1];
|
|
34258
|
+
i += 2;
|
|
34259
|
+
continue;
|
|
34260
|
+
}
|
|
34261
|
+
if (c === quote) return { value, end: i + 1 };
|
|
34262
|
+
value += c;
|
|
34263
|
+
i++;
|
|
34264
|
+
}
|
|
34265
|
+
return null;
|
|
34266
|
+
}
|
|
34267
|
+
function sqlLiterals(src) {
|
|
34268
|
+
const out = [];
|
|
34269
|
+
const callRe = /\b(query|callProc)\s*\(\s*/g;
|
|
34270
|
+
for (const m of src.matchAll(callRe)) {
|
|
34271
|
+
const argStart = (m.index ?? 0) + m[0].length;
|
|
34272
|
+
const lit = readStringLiteral(src, argStart);
|
|
34273
|
+
if (!lit) continue;
|
|
34274
|
+
out.push({ sql: lit.value, index: m.index ?? 0, end: lit.end });
|
|
34275
|
+
}
|
|
34276
|
+
return out;
|
|
34277
|
+
}
|
|
34278
|
+
function parseBlocks(src) {
|
|
34279
|
+
const lines = src.split("\n");
|
|
34280
|
+
const found = [];
|
|
34281
|
+
let offset = 0;
|
|
34282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34283
|
+
const m = lines[i].match(/^([A-Za-z][A-Za-z0-9_]*)\s*:\s*$/);
|
|
34284
|
+
if (m) found.push({ block: m[1], start: offset + m[0].length, end: src.length, line: i + 1 });
|
|
34285
|
+
offset += lines[i].length + 1;
|
|
34286
|
+
}
|
|
34287
|
+
for (let i = 0; i < found.length - 1; i++) {
|
|
34288
|
+
found[i].end = src.lastIndexOf("\n", found[i + 1].start - found[i + 1].block.length - 2) + 1;
|
|
34289
|
+
}
|
|
34290
|
+
return found;
|
|
34291
|
+
}
|
|
34292
|
+
function checkDsl(src, opts = {}) {
|
|
34293
|
+
const issues = [];
|
|
34294
|
+
const add = (level, rule, message, line) => issues.push({ level, rule, message, ...line ? { line } : {} });
|
|
34295
|
+
const code2 = blank(src);
|
|
34296
|
+
const blocks = parseBlocks(src);
|
|
34297
|
+
const names = blocks.map((b) => b.block);
|
|
34298
|
+
if (src.trim() === "") {
|
|
34299
|
+
add("error", "dsl-empty", "DSL body is empty \u2014 at minimum it needs an `execute:` block.");
|
|
34300
|
+
return issues;
|
|
34301
|
+
}
|
|
34302
|
+
if (!names.includes("execute")) {
|
|
34303
|
+
add(
|
|
34304
|
+
"error",
|
|
34305
|
+
"missing-execute",
|
|
34306
|
+
"no `execute:` block \u2014 it is the only required block in a .dsl file."
|
|
34307
|
+
);
|
|
34308
|
+
}
|
|
34309
|
+
for (const b of blocks) {
|
|
34310
|
+
if (!BLOCKS.includes(b.block)) {
|
|
34311
|
+
add(
|
|
34312
|
+
"warning",
|
|
34313
|
+
"unknown-block",
|
|
34314
|
+
`'${b.block}:' is not a DSL block marker \u2014 expected one of ${BLOCKS.join(", ")}. (A stray label at column 0 is parsed as a block.)`,
|
|
34315
|
+
b.line
|
|
34316
|
+
);
|
|
34317
|
+
}
|
|
34318
|
+
}
|
|
34319
|
+
const known = names.filter((n) => BLOCKS.includes(n));
|
|
34320
|
+
const order = known.map((n) => BLOCKS.indexOf(n));
|
|
34321
|
+
for (let i = 1; i < order.length; i++) {
|
|
34322
|
+
if (order[i] < order[i - 1]) {
|
|
34323
|
+
add(
|
|
34324
|
+
"error",
|
|
34325
|
+
"block-order",
|
|
34326
|
+
`blocks must appear in the order ${BLOCKS.join(" \u2192 ")}; found '${known[i - 1]}:' before '${known[i]}:'.`
|
|
34327
|
+
);
|
|
34328
|
+
break;
|
|
34329
|
+
}
|
|
34330
|
+
}
|
|
34331
|
+
for (const b of BLOCKS) {
|
|
34332
|
+
if (known.filter((n) => n === b).length > 1) {
|
|
34333
|
+
add("error", "duplicate-block", `'${b}:' is declared more than once.`);
|
|
34334
|
+
}
|
|
34335
|
+
}
|
|
34336
|
+
const execBlock = blocks.find((b) => b.block === "execute");
|
|
34337
|
+
const execCode = execBlock ? code2.slice(execBlock.start, execBlock.end) : "";
|
|
34338
|
+
const execOffset = execBlock ? execBlock.start : 0;
|
|
34339
|
+
if (execBlock) {
|
|
34340
|
+
for (const fn of FORMULA_ONLY) {
|
|
34341
|
+
const re = new RegExp(`\\b${fn}\\s*\\(\\s*\\)`, "g");
|
|
34342
|
+
for (const m of execCode.matchAll(re)) {
|
|
34343
|
+
add(
|
|
34344
|
+
"error",
|
|
34345
|
+
"execute-formula-date",
|
|
34346
|
+
`the execute: block calls ${fn}(), which is undefined at runtime \u2014 install fails with "'${fn}' is not defined". Use lowercase now() in execute:. ${fn}() is formula-only (canExecute:, formula columns).`,
|
|
34347
|
+
lineOf(src, execOffset + (m.index ?? 0))
|
|
34348
|
+
);
|
|
34349
|
+
}
|
|
34350
|
+
}
|
|
34351
|
+
}
|
|
34352
|
+
const recordRefs = [...code2.matchAll(/(?<![\w\]$])\[([a-z][a-z0-9_]*)\]/g)];
|
|
34353
|
+
const mode = (opts.executionMode ?? "").toLowerCase();
|
|
34354
|
+
if (opts.viaJob && recordRefs.length > 0) {
|
|
34355
|
+
const first = recordRefs[0];
|
|
34356
|
+
add(
|
|
34357
|
+
"error",
|
|
34358
|
+
"job-record-context",
|
|
34359
|
+
`uses record-context syntax [${first[1]}] but is invoked by a scheduled job \u2014 jobs run as the system user with NO current record, so [field] is a hard error. Fetch rows with select() (or query()) inside the job action instead.`,
|
|
34360
|
+
lineOf(src, first.index ?? 0)
|
|
34361
|
+
);
|
|
34362
|
+
}
|
|
34363
|
+
if (mode === "batch" && execBlock) {
|
|
34364
|
+
const inExec = recordRefs.filter(
|
|
34365
|
+
(m) => (m.index ?? 0) >= execBlock.start && (m.index ?? 0) < execBlock.end
|
|
34366
|
+
);
|
|
34367
|
+
if (inExec.length > 0) {
|
|
34368
|
+
add(
|
|
34369
|
+
"error",
|
|
34370
|
+
"batch-record-context",
|
|
34371
|
+
`executionMode is 'batch' but execute: uses record-context [${inExec[0][1]}] \u2014 in batch mode there is no current record. Iterate the selection instead: \`for rec in records { rec[field] }\`.`,
|
|
34372
|
+
lineOf(src, inExec[0].index ?? 0)
|
|
34373
|
+
);
|
|
34374
|
+
}
|
|
34375
|
+
}
|
|
34376
|
+
for (const { sql, index, end } of sqlLiterals(src)) {
|
|
34377
|
+
const bad = sql.match(/(?<![:\w]):([a-z][a-z0-9_]*)/i);
|
|
34378
|
+
if (bad) {
|
|
34379
|
+
add(
|
|
34380
|
+
"error",
|
|
34381
|
+
"sql-placeholder",
|
|
34382
|
+
`SQL uses ':${bad[1]}' as a placeholder \u2014 dForge binds '@${bad[1]}'. Rewrite as '@${bad[1]}' and pass { ${bad[1]}: value } as the params argument.`,
|
|
34383
|
+
lineOf(src, index)
|
|
34384
|
+
);
|
|
34385
|
+
}
|
|
34386
|
+
const after = src.slice(end, end + 40);
|
|
34387
|
+
if (/^\s*\+/.test(after)) {
|
|
34388
|
+
add(
|
|
34389
|
+
"warning",
|
|
34390
|
+
"sql-concat",
|
|
34391
|
+
"SQL string looks concatenated \u2014 build queries with @placeholders + a params object, never by string concatenation.",
|
|
34392
|
+
lineOf(src, index)
|
|
34393
|
+
);
|
|
34394
|
+
}
|
|
34395
|
+
}
|
|
34396
|
+
if (execBlock) {
|
|
34397
|
+
const declaresFunction = /\bfunction\b/.test(execCode);
|
|
34398
|
+
const returnRe = declaresFunction ? /^[ \t]{0,8}return\b/gm : /\breturn\b/g;
|
|
34399
|
+
for (const m of execCode.matchAll(returnRe)) {
|
|
34400
|
+
add(
|
|
34401
|
+
"error",
|
|
34402
|
+
"top-level-return",
|
|
34403
|
+
"top-level `return` is invalid in a DSL body. Use exit('msg', 'info') to stop and commit, or error('msg') to stop and roll back.",
|
|
34404
|
+
lineOf(src, execOffset + (m.index ?? 0))
|
|
34405
|
+
);
|
|
34406
|
+
}
|
|
34407
|
+
}
|
|
34408
|
+
const local = /* @__PURE__ */ new Set();
|
|
34409
|
+
for (const m of code2.matchAll(/\bfunction\s+([A-Za-z_$][\w$]*)/g)) local.add(m[1]);
|
|
34410
|
+
for (const m of code2.matchAll(/\bvar\s+([A-Za-z_$][\w$]*)/g)) local.add(m[1]);
|
|
34411
|
+
const reported = /* @__PURE__ */ new Set();
|
|
34412
|
+
for (const m of code2.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
34413
|
+
const fn = m[1];
|
|
34414
|
+
if (DSL_BUILTINS.has(fn) || JS_RESERVED.has(fn) || local.has(fn) || reported.has(fn) || FORMULA_ONLY.includes(fn)) {
|
|
34415
|
+
continue;
|
|
34416
|
+
}
|
|
34417
|
+
reported.add(fn);
|
|
34418
|
+
add(
|
|
34419
|
+
"warning",
|
|
34420
|
+
"unknown-builtin",
|
|
34421
|
+
`calls '${fn}()', which is not a DSL host function or a locally declared one. Check the name against dforge://docs/dsl (Built-in functions).`,
|
|
34422
|
+
lineOf(src, m.index ?? 0)
|
|
34423
|
+
);
|
|
34424
|
+
}
|
|
34425
|
+
return issues;
|
|
34426
|
+
}
|
|
34427
|
+
|
|
34428
|
+
// src/tools/action-add.ts
|
|
33474
34429
|
var actionAddSchema = {
|
|
33475
34430
|
moduleDir: external_exports.string(),
|
|
33476
34431
|
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Action code (becomes the DSL filename and registry key)."),
|
|
@@ -33490,18 +34445,16 @@ var actionAddSchema = {
|
|
|
33490
34445
|
"Full DSL source. Should contain `params:`, optional `canExecute:`, optional `onBeforeStart:` (async only), and required `execute:` blocks."
|
|
33491
34446
|
)
|
|
33492
34447
|
};
|
|
33493
|
-
function
|
|
33494
|
-
const
|
|
33495
|
-
|
|
33496
|
-
|
|
33497
|
-
if (/\bTODAY\s*\(\s*\)/.test(execPart)) {
|
|
34448
|
+
function actionAdd(args) {
|
|
34449
|
+
const dslIssues = checkDsl(args.dslBody, { executionMode: args.executionMode });
|
|
34450
|
+
const dslErrors = dslIssues.filter((i) => i.level === "error");
|
|
34451
|
+
if (dslErrors.length > 0) {
|
|
33498
34452
|
throw new Error(
|
|
33499
|
-
`Action '${
|
|
34453
|
+
`Action '${args.code}' has ${dslErrors.length} DSL error(s):
|
|
34454
|
+
` + dslErrors.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.message}`).join("\n") + "\n\nFix the DSL body and call again. (Load dforge://docs/dsl for the full grammar.)"
|
|
33500
34455
|
);
|
|
33501
34456
|
}
|
|
33502
|
-
|
|
33503
|
-
function actionAdd(args) {
|
|
33504
|
-
assertNoFormulaDateInExecute(args.dslBody, args.code);
|
|
34457
|
+
const dslWarnings = dslIssues.filter((i) => i.level === "warning");
|
|
33505
34458
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
33506
34459
|
const actionsJson = readJsonOrDefault(paths.actions, {});
|
|
33507
34460
|
if (Object.prototype.hasOwnProperty.call(actionsJson, args.code)) {
|
|
@@ -33521,7 +34474,7 @@ function actionAdd(args) {
|
|
|
33521
34474
|
actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
|
|
33522
34475
|
}
|
|
33523
34476
|
actionsJson[args.code] = actionEntry;
|
|
33524
|
-
const dslPath =
|
|
34477
|
+
const dslPath = path9.join(paths.logicDir, "actions", `${args.code}.dsl`);
|
|
33525
34478
|
const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
|
|
33526
34479
|
return makeResult(
|
|
33527
34480
|
`Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
|
|
@@ -33529,10 +34482,82 @@ function actionAdd(args) {
|
|
|
33529
34482
|
[rel(paths.root, paths.actions)]: jsonText(actionsJson),
|
|
33530
34483
|
[rel(paths.root, dslPath)]: dslBody,
|
|
33531
34484
|
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
33532
|
-
}
|
|
34485
|
+
},
|
|
34486
|
+
dslWarnings.length > 0 ? `DSL warnings (not blocking):
|
|
34487
|
+
${dslWarnings.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.message}`).join("\n")}` : void 0
|
|
33533
34488
|
);
|
|
33534
34489
|
}
|
|
33535
34490
|
|
|
34491
|
+
// src/tools/action-check.ts
|
|
34492
|
+
var fs7 = __toESM(require("fs"));
|
|
34493
|
+
var path10 = __toESM(require("path"));
|
|
34494
|
+
var actionCheckSchema = {
|
|
34495
|
+
moduleDir: external_exports.string().optional().describe(
|
|
34496
|
+
"Module root. Required when checking an on-disk action (actionCode); optional when checking a raw dslBody."
|
|
34497
|
+
),
|
|
34498
|
+
actionCode: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
|
|
34499
|
+
"Check an action already registered in ui/actions.json \u2014 its DSL file, executionMode, and any job binding are read from disk. Omit to check a raw dslBody instead."
|
|
34500
|
+
),
|
|
34501
|
+
dslBody: external_exports.string().optional().describe("Raw DSL source to check. Use this to validate a draft BEFORE calling dforge_action_add."),
|
|
34502
|
+
executionMode: external_exports.enum(["single", "each", "batch"]).optional().describe(
|
|
34503
|
+
"Execution mode the body will run under. Gates the record-context rules ('[field]' is invalid in batch mode). Read from ui/actions.json when actionCode is used."
|
|
34504
|
+
),
|
|
34505
|
+
viaJob: external_exports.boolean().optional().describe(
|
|
34506
|
+
"True when a scheduled job will fire this action \u2014 jobs have NO current record, so '[field]' becomes a hard error. Detected automatically from logic/jobs.json when actionCode is used."
|
|
34507
|
+
)
|
|
34508
|
+
};
|
|
34509
|
+
function actionCheck(args) {
|
|
34510
|
+
let body = args.dslBody;
|
|
34511
|
+
let mode = args.executionMode;
|
|
34512
|
+
let viaJob = args.viaJob ?? false;
|
|
34513
|
+
let label = "draft DSL body";
|
|
34514
|
+
if (args.actionCode) {
|
|
34515
|
+
if (!args.moduleDir) {
|
|
34516
|
+
throw new Error("moduleDir is required when checking an on-disk action (actionCode).");
|
|
34517
|
+
}
|
|
34518
|
+
const { paths } = loadManifest(args.moduleDir);
|
|
34519
|
+
const actions = readJsonOrDefault(paths.actions, {});
|
|
34520
|
+
const entry = actions[args.actionCode];
|
|
34521
|
+
if (!entry) {
|
|
34522
|
+
throw new Error(
|
|
34523
|
+
`Action '${args.actionCode}' is not in ui/actions.json. Registered actions: ${Object.keys(actions).join(", ") || "(none)"}.`
|
|
34524
|
+
);
|
|
34525
|
+
}
|
|
34526
|
+
const script = entry.script ?? args.actionCode;
|
|
34527
|
+
const dslPath = path10.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
34528
|
+
if (!fs7.existsSync(dslPath)) {
|
|
34529
|
+
throw new Error(
|
|
34530
|
+
`Action '${args.actionCode}' declares script '${script}' but logic/actions/${script}.dsl does not exist.`
|
|
34531
|
+
);
|
|
34532
|
+
}
|
|
34533
|
+
body = fs7.readFileSync(dslPath, "utf8");
|
|
34534
|
+
mode = mode ?? (entry.executionMode ?? entry.mode);
|
|
34535
|
+
label = `logic/actions/${script}.dsl`;
|
|
34536
|
+
if (args.viaJob === void 0) {
|
|
34537
|
+
const jobFile = readJsonOrDefault(paths.jobs, {});
|
|
34538
|
+
viaJob = (jobFile.jobs ?? []).some((j) => j.action === args.actionCode);
|
|
34539
|
+
}
|
|
34540
|
+
}
|
|
34541
|
+
if (body === void 0) {
|
|
34542
|
+
throw new Error("Pass either dslBody (a draft) or actionCode (an action already on disk).");
|
|
34543
|
+
}
|
|
34544
|
+
const issues = checkDsl(body, { executionMode: mode, viaJob });
|
|
34545
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
34546
|
+
const warnings = issues.filter((i) => i.level === "warning");
|
|
34547
|
+
const summary = errors.length === 0 && warnings.length === 0 ? `\u2713 ${label}: no DSL issues${mode ? ` (executionMode '${mode}'${viaJob ? ", job-invoked" : ""})` : ""}.` : `${label}: ${errors.length} error(s), ${warnings.length} warning(s).${errors.length ? ` First: ${errors[0].message}` : ""}`;
|
|
34548
|
+
return {
|
|
34549
|
+
summary,
|
|
34550
|
+
files: {
|
|
34551
|
+
"_action_check.json": JSON.stringify(
|
|
34552
|
+
{ ok: errors.length === 0, executionMode: mode ?? null, viaJob, errors, warnings },
|
|
34553
|
+
null,
|
|
34554
|
+
" "
|
|
34555
|
+
) + "\n"
|
|
34556
|
+
},
|
|
34557
|
+
warning: errors.length ? `${errors.length} DSL error(s) \u2014 each is a documented install failure. Fix before dforge_action_add / dforge_module_pack.` : void 0
|
|
34558
|
+
};
|
|
34559
|
+
}
|
|
34560
|
+
|
|
33536
34561
|
// src/tools/view.ts
|
|
33537
34562
|
var dataSourceSchema = external_exports.object({
|
|
33538
34563
|
entityCode: external_exports.string(),
|
|
@@ -33608,6 +34633,431 @@ function viewModify(args) {
|
|
|
33608
34633
|
});
|
|
33609
34634
|
}
|
|
33610
34635
|
|
|
34636
|
+
// src/tools/menu.ts
|
|
34637
|
+
var menuAddSchema = {
|
|
34638
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34639
|
+
menu: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Root menu key in ui/menus.json, e.g. 'crm_menu'. Created if it doesn't exist yet."),
|
|
34640
|
+
menuLabel: external_exports.string().optional().describe("Label for the root menu \u2014 only used when the root is being created."),
|
|
34641
|
+
parentPath: external_exports.string().default("").describe(
|
|
34642
|
+
"Slash-separated item codes under the root, e.g. 'lists/archive'. Empty = add directly under the root menu."
|
|
34643
|
+
),
|
|
34644
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Code of the new item (its key under the parent's items/children)."),
|
|
34645
|
+
label: external_exports.string().min(1).describe("Display label."),
|
|
34646
|
+
dataViewCode: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
|
|
34647
|
+
"For a LEAF item: the view code from ui/data_views.json this item opens. Omit to create a SECTION node (a container for children). Note the key is dataViewCode, never viewCode."
|
|
34648
|
+
),
|
|
34649
|
+
icon: external_exports.string().optional().describe(
|
|
34650
|
+
"Bootstrap icon name. A leading 'bi-' is STRIPPED \u2014 menu icons are bare names ('list-ul'), unlike action icons which keep the prefix."
|
|
34651
|
+
),
|
|
34652
|
+
orderNum: external_exports.number().int().optional().describe("Sort position among siblings. Default: appended after the current highest.")
|
|
34653
|
+
};
|
|
34654
|
+
function childKey(node, isRoot) {
|
|
34655
|
+
if (isRoot) return "items";
|
|
34656
|
+
return Object.prototype.hasOwnProperty.call(node, "items") ? "items" : "children";
|
|
34657
|
+
}
|
|
34658
|
+
function nextOrder(children) {
|
|
34659
|
+
let max = 0;
|
|
34660
|
+
for (const c of Object.values(children)) {
|
|
34661
|
+
const n = typeof c?.orderNum === "number" ? c.orderNum : 0;
|
|
34662
|
+
if (n > max) max = n;
|
|
34663
|
+
}
|
|
34664
|
+
return max + 1;
|
|
34665
|
+
}
|
|
34666
|
+
function menuAdd(args) {
|
|
34667
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
34668
|
+
const menus = readJsonOrDefault(paths.menus, {});
|
|
34669
|
+
if (args.dataViewCode) {
|
|
34670
|
+
const views = readJsonOrDefault(paths.dataViews, {});
|
|
34671
|
+
if (!Object.prototype.hasOwnProperty.call(views, args.dataViewCode)) {
|
|
34672
|
+
throw new Error(
|
|
34673
|
+
`dataViewCode '${args.dataViewCode}' has no matching view in ui/data_views.json. Existing views: ${Object.keys(views).join(", ") || "(none)"}. Add the view first with dforge_view_add.`
|
|
34674
|
+
);
|
|
34675
|
+
}
|
|
34676
|
+
}
|
|
34677
|
+
let root = menus[args.menu];
|
|
34678
|
+
if (!root) {
|
|
34679
|
+
root = { label: args.menuLabel ?? manifest.displayName, items: {} };
|
|
34680
|
+
menus[args.menu] = root;
|
|
34681
|
+
}
|
|
34682
|
+
let cursor = root;
|
|
34683
|
+
let isRoot = true;
|
|
34684
|
+
const segments = args.parentPath.split("/").filter(Boolean);
|
|
34685
|
+
const walked = [];
|
|
34686
|
+
for (const seg of segments) {
|
|
34687
|
+
const key2 = childKey(cursor, isRoot);
|
|
34688
|
+
const children2 = cursor[key2] ?? {};
|
|
34689
|
+
if (!children2[seg]) {
|
|
34690
|
+
throw new Error(
|
|
34691
|
+
`Menu path segment '${seg}' not found under '${walked.join("/") || args.menu}'. Available: ${Object.keys(children2).join(", ") || "(none)"}.`
|
|
34692
|
+
);
|
|
34693
|
+
}
|
|
34694
|
+
cursor = children2[seg];
|
|
34695
|
+
walked.push(seg);
|
|
34696
|
+
isRoot = false;
|
|
34697
|
+
}
|
|
34698
|
+
if (!isRoot && cursor.dataViewCode) {
|
|
34699
|
+
throw new Error(
|
|
34700
|
+
`'${walked.join("/")}' is a leaf item (it has a dataViewCode) \u2014 it can't contain children. Nest under a section node instead (a node with no dataViewCode).`
|
|
34701
|
+
);
|
|
34702
|
+
}
|
|
34703
|
+
const key = childKey(cursor, isRoot);
|
|
34704
|
+
const children = cursor[key] ?? {};
|
|
34705
|
+
if (children[args.code]) {
|
|
34706
|
+
throw new Error(
|
|
34707
|
+
`Menu item '${args.code}' already exists under '${walked.join("/") || args.menu}'.`
|
|
34708
|
+
);
|
|
34709
|
+
}
|
|
34710
|
+
const node = { label: args.label, orderNum: args.orderNum ?? nextOrder(children) };
|
|
34711
|
+
if (args.icon) {
|
|
34712
|
+
node.icon = args.icon.replace(/^bi-/, "");
|
|
34713
|
+
}
|
|
34714
|
+
if (args.dataViewCode) {
|
|
34715
|
+
node.itemType = "V";
|
|
34716
|
+
node.dataViewCode = args.dataViewCode;
|
|
34717
|
+
} else {
|
|
34718
|
+
node.children = {};
|
|
34719
|
+
}
|
|
34720
|
+
children[args.code] = node;
|
|
34721
|
+
cursor[key] = children;
|
|
34722
|
+
const kind = args.dataViewCode ? `leaf \u2192 view '${args.dataViewCode}'` : "section";
|
|
34723
|
+
return makeResult(
|
|
34724
|
+
`Added menu ${kind} '${args.code}' under '${args.menu}${walked.length ? "/" + walked.join("/") : ""}'.`,
|
|
34725
|
+
{
|
|
34726
|
+
[rel(paths.root, paths.menus)]: jsonText(menus),
|
|
34727
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
34728
|
+
}
|
|
34729
|
+
);
|
|
34730
|
+
}
|
|
34731
|
+
|
|
34732
|
+
// src/tools/translations.ts
|
|
34733
|
+
var fs8 = __toESM(require("fs"));
|
|
34734
|
+
var path11 = __toESM(require("path"));
|
|
34735
|
+
function titleize(code2) {
|
|
34736
|
+
return code2.split(/[_-]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
34737
|
+
}
|
|
34738
|
+
function str(obj, key) {
|
|
34739
|
+
const v = obj?.[key];
|
|
34740
|
+
return typeof v === "string" && v.trim() !== "" ? v : void 0;
|
|
34741
|
+
}
|
|
34742
|
+
var translationSyncSchema = {
|
|
34743
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34744
|
+
locales: external_exports.array(external_exports.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/)).optional().describe(
|
|
34745
|
+
"Locales to sync. Default: 'en-US' plus every entry in manifest.supportedLocales \u2014 i.e. exactly the set install requires."
|
|
34746
|
+
),
|
|
34747
|
+
prune: external_exports.boolean().default(false).describe(
|
|
34748
|
+
"Remove keys that no longer correspond to anything in the module (e.g. after an entity delete). Off by default \u2014 pruning discards translated text."
|
|
34749
|
+
)
|
|
34750
|
+
};
|
|
34751
|
+
function buildSkeleton(moduleDir) {
|
|
34752
|
+
const { paths, manifest } = loadManifest(moduleDir);
|
|
34753
|
+
const entries = [];
|
|
34754
|
+
const counts = {};
|
|
34755
|
+
const push = (section, pathParts, english, required2 = false) => {
|
|
34756
|
+
entries.push({ pathParts, english, required: required2 });
|
|
34757
|
+
counts[section] = (counts[section] ?? 0) + 1;
|
|
34758
|
+
};
|
|
34759
|
+
for (const [name, relPath] of Object.entries(manifest.entities ?? {})) {
|
|
34760
|
+
if (name.includes(".")) continue;
|
|
34761
|
+
const abs = path11.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
34762
|
+
if (!fs8.existsSync(abs)) {
|
|
34763
|
+
throw new Error(
|
|
34764
|
+
`manifest.entities.${name} points at '${relPath}', which does not exist on disk. Fix the manifest or restore the file \u2014 syncing now would silently omit that entity's translation keys and the gap would only surface at install.`
|
|
34765
|
+
);
|
|
34766
|
+
}
|
|
34767
|
+
let e;
|
|
34768
|
+
try {
|
|
34769
|
+
e = JSON.parse(fs8.readFileSync(abs, "utf8"));
|
|
34770
|
+
} catch (ex) {
|
|
34771
|
+
throw new Error(`${relPath} is not valid JSON: ${ex.message}`);
|
|
34772
|
+
}
|
|
34773
|
+
push("entities", ["entities", name, "label"], str(e, "description") ?? titleize(name));
|
|
34774
|
+
const authored = e.fields ?? {};
|
|
34775
|
+
const traitCodes = e.traits ?? [];
|
|
34776
|
+
assertKnownTraits(traitCodes, name);
|
|
34777
|
+
const traitFields = expandTraits(traitCodes, name);
|
|
34778
|
+
const all = { ...traitFields, ...authored };
|
|
34779
|
+
for (const [fname, f] of Object.entries(all)) {
|
|
34780
|
+
push(
|
|
34781
|
+
"fields",
|
|
34782
|
+
["entities", name, "fields", fname, "label"],
|
|
34783
|
+
str(f, "description") ?? str(f, "label") ?? titleize(fname)
|
|
34784
|
+
);
|
|
34785
|
+
}
|
|
34786
|
+
}
|
|
34787
|
+
const views = readJsonOrDefault(paths.dataViews, {});
|
|
34788
|
+
for (const [code2, v] of Object.entries(views)) {
|
|
34789
|
+
push("views", ["views", code2, "label"], str(v, "label") ?? titleize(code2));
|
|
34790
|
+
}
|
|
34791
|
+
const menus = readJsonOrDefault(paths.menus, {});
|
|
34792
|
+
const walkMenu = (node, trail) => {
|
|
34793
|
+
const children = node.items ?? node.children ?? {};
|
|
34794
|
+
for (const [code2, child] of Object.entries(children)) {
|
|
34795
|
+
const next = [...trail, "items", code2];
|
|
34796
|
+
push("menus", [...next, "label"], str(child, "label") ?? titleize(code2));
|
|
34797
|
+
walkMenu(child, next);
|
|
34798
|
+
}
|
|
34799
|
+
};
|
|
34800
|
+
for (const [code2, m] of Object.entries(menus)) {
|
|
34801
|
+
push("menus", ["menus", code2, "label"], str(m, "label") ?? titleize(code2));
|
|
34802
|
+
walkMenu(m, ["menus", code2]);
|
|
34803
|
+
}
|
|
34804
|
+
const roles = readJsonOrDefault(paths.roles, {});
|
|
34805
|
+
for (const [code2, r] of Object.entries(roles)) {
|
|
34806
|
+
push(
|
|
34807
|
+
"roles",
|
|
34808
|
+
["roles", code2, "label"],
|
|
34809
|
+
str(r, "description") ?? titleize(code2.split(".").pop() ?? code2),
|
|
34810
|
+
true
|
|
34811
|
+
);
|
|
34812
|
+
}
|
|
34813
|
+
const actions = readJsonOrDefault(paths.actions, {});
|
|
34814
|
+
for (const [code2, a] of Object.entries(actions)) {
|
|
34815
|
+
push("actions", ["actions", code2, "label"], str(a, "label") ?? titleize(code2));
|
|
34816
|
+
}
|
|
34817
|
+
const folders = readJsonOrDefault(paths.folders, {});
|
|
34818
|
+
if (Object.keys(folders).length > 0) {
|
|
34819
|
+
const dupes = duplicateFolderCodes(folders);
|
|
34820
|
+
if (dupes.size > 0) {
|
|
34821
|
+
const detail = [...dupes].map(([code2, paths_]) => `'${code2}' (${paths_.join(", ")})`).join("; ");
|
|
34822
|
+
throw new Error(
|
|
34823
|
+
`ui/folders.json reuses folder code(s) across branches: ${detail}. Translation keys are flat (\`folders.<code>.label\`), so duplicates would overwrite each other \u2014 and role rights (\`folder:<code>\`) can't tell them apart either. Rename them to unique codes, then re-run.`
|
|
34824
|
+
);
|
|
34825
|
+
}
|
|
34826
|
+
push("folders", ["folders", manifest.code, "label"], str(folders, "label") ?? titleize(manifest.code));
|
|
34827
|
+
for (const f of walkFolders(folders)) {
|
|
34828
|
+
push("folders", ["folders", f.code, "label"], str(f.node, "label") ?? titleize(f.code));
|
|
34829
|
+
}
|
|
34830
|
+
}
|
|
34831
|
+
const settings = readJsonOrDefault(paths.settings, {});
|
|
34832
|
+
for (const [code2, s] of Object.entries(settings)) {
|
|
34833
|
+
push("settings", ["settings", code2, "label"], str(s, "label") ?? titleize(code2));
|
|
34834
|
+
}
|
|
34835
|
+
return { entries, counts };
|
|
34836
|
+
}
|
|
34837
|
+
var getIn = (root, parts) => parts.reduce((acc, p) => acc && typeof acc === "object" ? acc[p] : void 0, root);
|
|
34838
|
+
function setIn(root, parts, value) {
|
|
34839
|
+
let cursor = root;
|
|
34840
|
+
for (const p of parts.slice(0, -1)) {
|
|
34841
|
+
if (!cursor[p] || typeof cursor[p] !== "object") cursor[p] = {};
|
|
34842
|
+
cursor = cursor[p];
|
|
34843
|
+
}
|
|
34844
|
+
cursor[parts[parts.length - 1]] = value;
|
|
34845
|
+
}
|
|
34846
|
+
function pruneTo(existing, keep, trail = []) {
|
|
34847
|
+
const out = {};
|
|
34848
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
34849
|
+
const parts = [...trail, k];
|
|
34850
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
34851
|
+
const sub = pruneTo(v, keep, parts);
|
|
34852
|
+
if (Object.keys(sub).length > 0) out[k] = sub;
|
|
34853
|
+
continue;
|
|
34854
|
+
}
|
|
34855
|
+
if (keep.has(compositeKey(...parts))) out[k] = v;
|
|
34856
|
+
}
|
|
34857
|
+
return out;
|
|
34858
|
+
}
|
|
34859
|
+
function translationSync(args) {
|
|
34860
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
34861
|
+
const declared = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
|
|
34862
|
+
const locales = args.locales ?? ["en-US", ...declared];
|
|
34863
|
+
if (locales.length === 0) {
|
|
34864
|
+
throw new Error("No locales to sync. Pass `locales`, or declare them in manifest.supportedLocales.");
|
|
34865
|
+
}
|
|
34866
|
+
const { entries, counts } = buildSkeleton(args.moduleDir);
|
|
34867
|
+
if (entries.length === 0) {
|
|
34868
|
+
throw new Error(
|
|
34869
|
+
"Nothing translatable found \u2014 the module has no entities, views, menus, or roles yet. Author them first."
|
|
34870
|
+
);
|
|
34871
|
+
}
|
|
34872
|
+
const keep = new Set(entries.map((e) => compositeKey(...e.pathParts)));
|
|
34873
|
+
const files = {};
|
|
34874
|
+
const report = [];
|
|
34875
|
+
let totalAdded = 0;
|
|
34876
|
+
let missingRequired = 0;
|
|
34877
|
+
for (const locale of locales) {
|
|
34878
|
+
const abs = path11.join(paths.translationsDir, `${locale}.json`);
|
|
34879
|
+
let existing = {};
|
|
34880
|
+
if (fs8.existsSync(abs)) {
|
|
34881
|
+
try {
|
|
34882
|
+
existing = JSON.parse(fs8.readFileSync(abs, "utf8"));
|
|
34883
|
+
} catch (e) {
|
|
34884
|
+
throw new Error(`translations/${locale}.json is not valid JSON: ${e.message}`);
|
|
34885
|
+
}
|
|
34886
|
+
}
|
|
34887
|
+
const next = args.prune ? pruneTo(existing, keep) : JSON.parse(JSON.stringify(existing));
|
|
34888
|
+
let added = 0;
|
|
34889
|
+
for (const entry of entries) {
|
|
34890
|
+
const current = getIn(next, entry.pathParts);
|
|
34891
|
+
if (typeof current === "string" && current.trim() !== "") continue;
|
|
34892
|
+
setIn(next, entry.pathParts, entry.english);
|
|
34893
|
+
added++;
|
|
34894
|
+
if (entry.required) missingRequired++;
|
|
34895
|
+
}
|
|
34896
|
+
files[rel(paths.root, abs)] = jsonText(next);
|
|
34897
|
+
totalAdded += added;
|
|
34898
|
+
report.push(`${locale}: +${added} key(s)${added === 0 ? " (already complete)" : ""}`);
|
|
34899
|
+
}
|
|
34900
|
+
const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
34901
|
+
const nonEnglish = locales.filter((l) => !isEnglish(l));
|
|
34902
|
+
const warning = nonEnglish.length > 0 && totalAdded > 0 ? `Newly-added keys in ${nonEnglish.join(", ")} are seeded with the ENGLISH source text so the file is installable immediately \u2014 they still need real translation. (Untranslated values fall back to en-US at runtime, so this never breaks; it just shows English.)` : void 0;
|
|
34903
|
+
const sections = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
34904
|
+
return makeResult(
|
|
34905
|
+
`Synced ${locales.length} locale file(s) from ${entries.length} translatable keys (${sections}). ${report.join("; ")}.${missingRequired > 0 ? ` Filled ${missingRequired} install-blocking role label(s).` : ""}`,
|
|
34906
|
+
files,
|
|
34907
|
+
warning
|
|
34908
|
+
);
|
|
34909
|
+
}
|
|
34910
|
+
|
|
34911
|
+
// src/tools/seed.ts
|
|
34912
|
+
var fs9 = __toESM(require("fs"));
|
|
34913
|
+
var path12 = __toESM(require("path"));
|
|
34914
|
+
var SYSTEM_USER = 0;
|
|
34915
|
+
var seedAddSchema = {
|
|
34916
|
+
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34917
|
+
entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity these records belong to."),
|
|
34918
|
+
records: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).min(1).describe(
|
|
34919
|
+
"The rows. Each MUST carry an explicit numeric PK under '{entity}_id' (cuid is int8, not a UUID). FK values must match a PK of an already-seeded parent."
|
|
34920
|
+
),
|
|
34921
|
+
order: external_exports.number().int().min(1).max(99).optional().describe(
|
|
34922
|
+
"Load order \u2014 becomes the filename prefix (seed-data/NN-<entity>.json). Parents need a LOWER number than their children. Default: next free slot."
|
|
34923
|
+
),
|
|
34924
|
+
fileName: external_exports.string().optional().describe("Override the generated filename (without directory). Rarely needed.")
|
|
34925
|
+
};
|
|
34926
|
+
function seedAdd(args) {
|
|
34927
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
34928
|
+
const entityPath = path12.join(paths.entitiesDir, `${args.entity}.json`);
|
|
34929
|
+
if (!fs9.existsSync(entityPath)) {
|
|
34930
|
+
throw new Error(
|
|
34931
|
+
`Entity '${args.entity}' not found at entities/${args.entity}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
|
|
34932
|
+
);
|
|
34933
|
+
}
|
|
34934
|
+
const entity = readJson(entityPath);
|
|
34935
|
+
const fields = entity.fields ?? {};
|
|
34936
|
+
const traits2 = entity.traits ?? [];
|
|
34937
|
+
const pk = `${args.entity}_id`;
|
|
34938
|
+
const warnings = [];
|
|
34939
|
+
const seenPks = /* @__PURE__ */ new Set();
|
|
34940
|
+
args.records.forEach((r, i) => {
|
|
34941
|
+
if (!Object.prototype.hasOwnProperty.call(r, pk)) {
|
|
34942
|
+
const wrong = ["id", "ID", `${args.entity}Id`].find(
|
|
34943
|
+
(k) => Object.prototype.hasOwnProperty.call(r, k)
|
|
34944
|
+
);
|
|
34945
|
+
throw new Error(
|
|
34946
|
+
`Record ${i} has no '${pk}'${wrong ? ` (found '${wrong}' instead)` : ""}. Seed records need an explicit numeric PK under '{entity}_id' \u2014 the identity trait names it '${pk}', never 'id'.`
|
|
34947
|
+
);
|
|
34948
|
+
}
|
|
34949
|
+
const v = r[pk];
|
|
34950
|
+
if (typeof v !== "number" || !Number.isInteger(v)) {
|
|
34951
|
+
throw new Error(
|
|
34952
|
+
`Record ${i}'s '${pk}' is ${JSON.stringify(v)} \u2014 it must be an INTEGER. The cuid type is int8, not a UUID or string.`
|
|
34953
|
+
);
|
|
34954
|
+
}
|
|
34955
|
+
if (seenPks.has(v)) throw new Error(`Duplicate '${pk}' value ${v} across the seed records.`);
|
|
34956
|
+
seenPks.add(v);
|
|
34957
|
+
});
|
|
34958
|
+
if (traits2.includes("audit-full")) {
|
|
34959
|
+
const missing = args.records.map((r, i) => ({ i, r })).filter(({ r }) => r.created_by === void 0 || r.last_updated_by === void 0).map(({ i }) => i);
|
|
34960
|
+
if (missing.length > 0) {
|
|
34961
|
+
throw new Error(
|
|
34962
|
+
`'${args.entity}' uses the 'audit-full' trait, whose created_by / last_updated_by columns are REQUIRED with no default \u2014 records [${missing.join(", ")}] set neither. Install fails with "required column 'created_by' \u2026 is not present in seed records". Set both to ${SYSTEM_USER} (the System user) on every record, switch the entity to the 'audit' trait, or don't seed it.`
|
|
34963
|
+
);
|
|
34964
|
+
}
|
|
34965
|
+
}
|
|
34966
|
+
assertKnownTraits(traits2, args.entity);
|
|
34967
|
+
const traitFields = expandTraits(traits2, args.entity);
|
|
34968
|
+
const known = /* @__PURE__ */ new Set([...Object.keys(fields), ...Object.keys(traitFields)]);
|
|
34969
|
+
known.add(pk);
|
|
34970
|
+
const unknown2 = /* @__PURE__ */ new Set();
|
|
34971
|
+
for (const r of args.records) {
|
|
34972
|
+
for (const k of Object.keys(r)) if (!known.has(k)) unknown2.add(k);
|
|
34973
|
+
}
|
|
34974
|
+
if (unknown2.size > 0) {
|
|
34975
|
+
const traitCols = Object.keys(traitFields);
|
|
34976
|
+
throw new Error(
|
|
34977
|
+
`Seed records set column(s) not defined on '${args.entity}': ${[...unknown2].join(", ")}. Its columns are: ${Object.keys(fields).join(", ") || "(none authored)"}` + (traitCols.length > 0 ? `, plus these from traits [${traits2.join(", ")}]: ${traitCols.join(", ")}.` : ".")
|
|
34978
|
+
);
|
|
34979
|
+
}
|
|
34980
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
34981
|
+
if (f?.columnType !== "R") continue;
|
|
34982
|
+
const used = args.records.some((r) => r[fname] !== void 0);
|
|
34983
|
+
if (used) {
|
|
34984
|
+
const thisKey = f.link?.thisKey;
|
|
34985
|
+
throw new Error(
|
|
34986
|
+
`'${fname}' is a Reference column (virtual \u2014 it owns no physical column), so it can't be seeded. Set the hidden FK '${String(thisKey ?? `${fname}_id`)}' to the parent's numeric PK instead.`
|
|
34987
|
+
);
|
|
34988
|
+
}
|
|
34989
|
+
}
|
|
34990
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
34991
|
+
const flags = typeof f?.flags === "string" ? f.flags : "";
|
|
34992
|
+
const isVirtual = f?.columnType === "R" || f?.columnType === "S" || f?.columnType === "F" || f?.columnType === "G";
|
|
34993
|
+
if (isVirtual || !flags.includes("M") || f?.formula !== void 0) continue;
|
|
34994
|
+
const missing = args.records.map((r, i) => r[fname] === void 0 ? i : -1).filter((i) => i >= 0);
|
|
34995
|
+
if (missing.length > 0) {
|
|
34996
|
+
warnings.push(
|
|
34997
|
+
`required column '${fname}' is unset on record(s) [${missing.join(", ")}] \u2014 install may reject the row.`
|
|
34998
|
+
);
|
|
34999
|
+
}
|
|
35000
|
+
}
|
|
35001
|
+
const seedFiles = fs9.existsSync(paths.seedDataDir) ? fs9.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
35002
|
+
const seededPks = {};
|
|
35003
|
+
for (const f of seedFiles) {
|
|
35004
|
+
try {
|
|
35005
|
+
const data = JSON.parse(fs9.readFileSync(path12.join(paths.seedDataDir, f), "utf8"));
|
|
35006
|
+
const ec = data?.entityCode;
|
|
35007
|
+
if (typeof ec !== "string" || !Array.isArray(data.records)) continue;
|
|
35008
|
+
const set2 = seededPks[ec] ?? (seededPks[ec] = /* @__PURE__ */ new Set());
|
|
35009
|
+
for (const r of data.records) {
|
|
35010
|
+
const v = r?.[`${ec}_id`];
|
|
35011
|
+
if (typeof v === "number") set2.add(v);
|
|
35012
|
+
}
|
|
35013
|
+
} catch {
|
|
35014
|
+
}
|
|
35015
|
+
}
|
|
35016
|
+
seededPks[args.entity] = seenPks;
|
|
35017
|
+
const refs = entity.references ?? {};
|
|
35018
|
+
for (const r of Object.values(refs)) {
|
|
35019
|
+
const fromField = r?.from?.field;
|
|
35020
|
+
const toEntity = r?.to?.entity;
|
|
35021
|
+
if (!fromField || !toEntity || toEntity.includes(".")) continue;
|
|
35022
|
+
const parentPks = seededPks[toEntity];
|
|
35023
|
+
for (const [i, rec] of args.records.entries()) {
|
|
35024
|
+
const v = rec[fromField];
|
|
35025
|
+
if (v === void 0 || v === null) continue;
|
|
35026
|
+
if (typeof v !== "number") {
|
|
35027
|
+
warnings.push(`record ${i}: FK '${fromField}' is ${JSON.stringify(v)} \u2014 expected a numeric PK.`);
|
|
35028
|
+
continue;
|
|
35029
|
+
}
|
|
35030
|
+
if (!parentPks || !parentPks.has(v)) {
|
|
35031
|
+
warnings.push(
|
|
35032
|
+
`record ${i}: FK '${fromField}' = ${v}, but no seeded '${toEntity}' record has that PK \u2014 seed '${toEntity}' first (with a LOWER order prefix) or the install hits an FK violation.`
|
|
35033
|
+
);
|
|
35034
|
+
}
|
|
35035
|
+
}
|
|
35036
|
+
}
|
|
35037
|
+
let order = args.order;
|
|
35038
|
+
if (order === void 0) {
|
|
35039
|
+
const mine = seedFiles.find((f) => f.replace(/^\d+-/, "") === `${args.entity}.json`);
|
|
35040
|
+
if (mine) {
|
|
35041
|
+
order = parseInt(mine.slice(0, 2), 10);
|
|
35042
|
+
} else {
|
|
35043
|
+
const used = seedFiles.map((f) => parseInt(f.slice(0, 2), 10)).filter((n) => !Number.isNaN(n));
|
|
35044
|
+
order = (used.length ? Math.max(...used) : 0) + 1;
|
|
35045
|
+
}
|
|
35046
|
+
}
|
|
35047
|
+
const fileName = args.fileName ?? `${String(order).padStart(2, "0")}-${args.entity}.json`;
|
|
35048
|
+
const seedPath = path12.join(paths.seedDataDir, fileName);
|
|
35049
|
+
const payload = { entityCode: args.entity, records: args.records };
|
|
35050
|
+
return makeResult(
|
|
35051
|
+
`Seeded ${args.records.length} '${args.entity}' record(s) \u2192 seed-data/${fileName} (load order ${order}).`,
|
|
35052
|
+
{
|
|
35053
|
+
[rel(paths.root, seedPath)]: jsonText(payload),
|
|
35054
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
35055
|
+
},
|
|
35056
|
+
warnings.length > 0 ? `Seed warnings:
|
|
35057
|
+
${warnings.map((w) => ` \u2022 ${w}`).join("\n")}` : void 0
|
|
35058
|
+
);
|
|
35059
|
+
}
|
|
35060
|
+
|
|
33611
35061
|
// src/tools/adds.ts
|
|
33612
35062
|
var reportAddSchema = {
|
|
33613
35063
|
moduleDir: external_exports.string(),
|
|
@@ -33725,10 +35175,13 @@ function folderAdd(args) {
|
|
|
33725
35175
|
}
|
|
33726
35176
|
cursor = children2[seg];
|
|
33727
35177
|
}
|
|
33728
|
-
const
|
|
33729
|
-
if (
|
|
33730
|
-
throw new Error(
|
|
35178
|
+
const clash = walkFolders(root).find((f) => f.code === args.code);
|
|
35179
|
+
if (clash) {
|
|
35180
|
+
throw new Error(
|
|
35181
|
+
`Folder code '${args.code}' is already used at '${clash.path}'. Folder codes must be unique across the whole tree \u2014 role rights reference a folder as 'folder:${args.code}' with no path, so a duplicate is ambiguous. Pick a distinct code (e.g. 'east_central').`
|
|
35182
|
+
);
|
|
33731
35183
|
}
|
|
35184
|
+
const children = cursor.children ?? {};
|
|
33732
35185
|
children[args.code] = args.folder;
|
|
33733
35186
|
cursor.children = children;
|
|
33734
35187
|
return makeResult(
|
|
@@ -33809,8 +35262,8 @@ function roleRightSet(args) {
|
|
|
33809
35262
|
}
|
|
33810
35263
|
|
|
33811
35264
|
// src/tools/module-inspect.ts
|
|
33812
|
-
var
|
|
33813
|
-
var
|
|
35265
|
+
var fs10 = __toESM(require("fs"));
|
|
35266
|
+
var path13 = __toESM(require("path"));
|
|
33814
35267
|
var moduleInspectSchema = {
|
|
33815
35268
|
moduleDir: external_exports.string()
|
|
33816
35269
|
};
|
|
@@ -33818,7 +35271,7 @@ function moduleInspect(args) {
|
|
|
33818
35271
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
33819
35272
|
const entities = manifest.entities ?? {};
|
|
33820
35273
|
const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
|
|
33821
|
-
const abs =
|
|
35274
|
+
const abs = path13.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
33822
35275
|
const e = readJsonOrDefault(abs, {});
|
|
33823
35276
|
const fields = e.fields ?? {};
|
|
33824
35277
|
return {
|
|
@@ -33828,7 +35281,11 @@ function moduleInspect(args) {
|
|
|
33828
35281
|
fieldCount: Object.keys(fields).length,
|
|
33829
35282
|
fields: Object.keys(fields),
|
|
33830
35283
|
hasNumberSequence: Boolean(e.numberSequence),
|
|
33831
|
-
toString
|
|
35284
|
+
// Read as an OWN property — `toString` is inherited from
|
|
35285
|
+
// Object.prototype, so a plain `e.toString` reports the built-in
|
|
35286
|
+
// function (which JSON.stringify then silently drops) for every
|
|
35287
|
+
// entity that doesn't declare a template.
|
|
35288
|
+
toString: Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : null
|
|
33832
35289
|
};
|
|
33833
35290
|
});
|
|
33834
35291
|
const views = readJsonOrDefault(paths.dataViews, {});
|
|
@@ -33854,15 +35311,47 @@ function moduleInspect(args) {
|
|
|
33854
35311
|
const actions = readJsonOrDefault(paths.actions, {});
|
|
33855
35312
|
const actionSummaries = Object.entries(actions).map(([code2, a]) => ({
|
|
33856
35313
|
code: code2,
|
|
33857
|
-
|
|
33858
|
-
|
|
33859
|
-
|
|
35314
|
+
entityCode: a.entityCode ?? a.entity ?? "?",
|
|
35315
|
+
executionMode: a.executionMode ?? a.mode ?? "single",
|
|
35316
|
+
isAsync: Boolean(a.isAsync ?? a.background),
|
|
35317
|
+
script: a.script ?? code2
|
|
35318
|
+
}));
|
|
35319
|
+
const triggerFile = readJsonOrDefault(
|
|
35320
|
+
paths.triggers,
|
|
35321
|
+
{}
|
|
35322
|
+
);
|
|
35323
|
+
const triggerSummaries = (triggerFile.triggers ?? []).map((t) => ({
|
|
35324
|
+
code: t.code ?? "?",
|
|
35325
|
+
entity: t.entity ?? "?",
|
|
35326
|
+
event: t.event ?? "?",
|
|
35327
|
+
action: t.action ?? "?",
|
|
35328
|
+
async: Boolean(t.async)
|
|
35329
|
+
}));
|
|
35330
|
+
const webhookFile = readJsonOrDefault(
|
|
35331
|
+
paths.webhooks,
|
|
35332
|
+
{}
|
|
35333
|
+
);
|
|
35334
|
+
const webhookSummaries = (webhookFile.subscriptions ?? []).map((w) => ({
|
|
35335
|
+
code: w.code ?? "?",
|
|
35336
|
+
entity: w.entity ?? "?",
|
|
35337
|
+
event: w.event ?? "?",
|
|
35338
|
+
endpointUrl: w.endpointUrl ?? "?"
|
|
33860
35339
|
}));
|
|
33861
35340
|
const reports = Object.keys(readJsonOrDefault(paths.reports, {}));
|
|
33862
35341
|
const settings = Object.keys(readJsonOrDefault(paths.settings, {}));
|
|
33863
|
-
const
|
|
33864
|
-
const
|
|
33865
|
-
|
|
35342
|
+
const jobsFile = readJsonOrDefault(paths.jobs, {});
|
|
35343
|
+
const jobSummaries = (jobsFile.jobs ?? []).map((j) => ({
|
|
35344
|
+
code: j.code ?? "?",
|
|
35345
|
+
action: j.action ?? "?",
|
|
35346
|
+
schedule: j.schedule ?? "?"
|
|
35347
|
+
}));
|
|
35348
|
+
const queries = Object.keys(readJsonOrDefault(paths.queries, {}));
|
|
35349
|
+
const printTemplates = Object.keys(
|
|
35350
|
+
readJsonOrDefault(paths.printTemplates, {})
|
|
35351
|
+
);
|
|
35352
|
+
const domains = Object.keys(readJsonOrDefault(paths.domains, {}));
|
|
35353
|
+
const seedFiles = fs10.existsSync(paths.seedDataDir) ? fs10.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
35354
|
+
const translations = fs10.existsSync(paths.translationsDir) ? fs10.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
33866
35355
|
const summary = {
|
|
33867
35356
|
module: {
|
|
33868
35357
|
code: manifest.code,
|
|
@@ -33879,14 +35368,20 @@ function moduleInspect(args) {
|
|
|
33879
35368
|
menus: menuSummaries,
|
|
33880
35369
|
roles: roleSummaries,
|
|
33881
35370
|
actions: actionSummaries,
|
|
35371
|
+
triggers: triggerSummaries,
|
|
35372
|
+
webhooks: webhookSummaries,
|
|
33882
35373
|
reports,
|
|
33883
35374
|
settings,
|
|
33884
|
-
jobs,
|
|
35375
|
+
jobs: jobSummaries,
|
|
35376
|
+
queries,
|
|
35377
|
+
printTemplates,
|
|
35378
|
+
domains,
|
|
33885
35379
|
seedFiles,
|
|
33886
|
-
translations
|
|
35380
|
+
translations,
|
|
35381
|
+
supportedLocales: Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : []
|
|
33887
35382
|
};
|
|
33888
35383
|
return {
|
|
33889
|
-
summary: `Module '${manifest.code}' v${manifest.version}: ${entitySummaries.length} entities, ${viewSummaries.length} views, ${roleSummaries.length} roles, ${actionSummaries.length} actions, ${reports.length} reports.`,
|
|
35384
|
+
summary: `Module '${manifest.code}' v${manifest.version}: ${entitySummaries.length} entities, ${viewSummaries.length} views, ${roleSummaries.length} roles, ${actionSummaries.length} actions, ${triggerSummaries.length} triggers, ${jobSummaries.length} jobs, ${reports.length} reports.`,
|
|
33890
35385
|
files: { "_inspect.json": JSON.stringify(summary, null, " ") + "\n" }
|
|
33891
35386
|
};
|
|
33892
35387
|
}
|
|
@@ -33900,8 +35395,8 @@ function computeDepth(folder, current = 1) {
|
|
|
33900
35395
|
}
|
|
33901
35396
|
|
|
33902
35397
|
// src/tools/module-validate.ts
|
|
33903
|
-
var
|
|
33904
|
-
var
|
|
35398
|
+
var fs11 = __toESM(require("fs"));
|
|
35399
|
+
var path14 = __toESM(require("path"));
|
|
33905
35400
|
var moduleValidateSchema = {
|
|
33906
35401
|
moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
|
|
33907
35402
|
};
|
|
@@ -33921,12 +35416,12 @@ function hasVisibleScalarColumn(fields) {
|
|
|
33921
35416
|
return false;
|
|
33922
35417
|
}
|
|
33923
35418
|
function resolveTranslationFile(translationsDir, locale) {
|
|
33924
|
-
const exact =
|
|
33925
|
-
if (
|
|
33926
|
-
if (!
|
|
35419
|
+
const exact = path14.join(translationsDir, `${locale}.json`);
|
|
35420
|
+
if (fs11.existsSync(exact)) return exact;
|
|
35421
|
+
if (!fs11.existsSync(translationsDir)) return void 0;
|
|
33927
35422
|
const want = `${locale}.json`.toLowerCase();
|
|
33928
|
-
for (const f of
|
|
33929
|
-
if (f.toLowerCase() === want) return
|
|
35423
|
+
for (const f of fs11.readdirSync(translationsDir)) {
|
|
35424
|
+
if (f.toLowerCase() === want) return path14.join(translationsDir, f);
|
|
33930
35425
|
}
|
|
33931
35426
|
return void 0;
|
|
33932
35427
|
}
|
|
@@ -33954,14 +35449,14 @@ function moduleValidate(args) {
|
|
|
33954
35449
|
const fieldDefsOf = {};
|
|
33955
35450
|
for (const [name, relPath] of Object.entries(entityMap)) {
|
|
33956
35451
|
if (name.includes(".")) continue;
|
|
33957
|
-
const abs =
|
|
33958
|
-
if (!
|
|
35452
|
+
const abs = path14.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
35453
|
+
if (!fs11.existsSync(abs)) {
|
|
33959
35454
|
err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
|
|
33960
35455
|
continue;
|
|
33961
35456
|
}
|
|
33962
35457
|
let e;
|
|
33963
35458
|
try {
|
|
33964
|
-
e = JSON.parse(
|
|
35459
|
+
e = JSON.parse(fs11.readFileSync(abs, "utf8"));
|
|
33965
35460
|
} catch (ex) {
|
|
33966
35461
|
err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
|
|
33967
35462
|
continue;
|
|
@@ -33969,13 +35464,16 @@ function moduleValidate(args) {
|
|
|
33969
35464
|
entities[name] = e;
|
|
33970
35465
|
const fields = e.fields ?? {};
|
|
33971
35466
|
const cols = new Set(Object.keys(fields));
|
|
33972
|
-
let traitFields = {};
|
|
33973
35467
|
const traits2 = e.traits ?? [];
|
|
33974
|
-
|
|
33975
|
-
|
|
33976
|
-
|
|
33977
|
-
|
|
35468
|
+
const badTraits = unknownTraits(traits2);
|
|
35469
|
+
if (badTraits.length > 0) {
|
|
35470
|
+
err(
|
|
35471
|
+
`entities/${name}.json`,
|
|
35472
|
+
`declares unknown trait(s): ${badTraits.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is ignored when columns are expanded, so its columns are missing from this entity \u2014 expect knock-on 'not a column' errors below.`
|
|
35473
|
+
);
|
|
33978
35474
|
}
|
|
35475
|
+
const traitFields = expandTraits(traits2, name);
|
|
35476
|
+
for (const c of Object.keys(traitFields)) cols.add(c);
|
|
33979
35477
|
columnsOf[name] = cols;
|
|
33980
35478
|
fieldDefsOf[name] = { ...traitFields, ...fields };
|
|
33981
35479
|
}
|
|
@@ -33989,7 +35487,7 @@ function moduleValidate(args) {
|
|
|
33989
35487
|
}
|
|
33990
35488
|
return false;
|
|
33991
35489
|
};
|
|
33992
|
-
const
|
|
35490
|
+
const pkOf2 = (code2) => {
|
|
33993
35491
|
if (code2 in SYSTEM_ENTITY_PK) return SYSTEM_ENTITY_PK[code2];
|
|
33994
35492
|
const e = entities[code2];
|
|
33995
35493
|
if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
|
|
@@ -34009,7 +35507,7 @@ function moduleValidate(args) {
|
|
|
34009
35507
|
if (thisKey && !columnsOf[name].has(thisKey)) {
|
|
34010
35508
|
err(where, `link.thisKey '${thisKey}' is not a column on '${name}' \u2014 the hidden FK column is missing (FK+Reference is two columns)`);
|
|
34011
35509
|
}
|
|
34012
|
-
const pk = target ?
|
|
35510
|
+
const pk = target ? pkOf2(target) : void 0;
|
|
34013
35511
|
if (pk && link.otherKey && link.otherKey !== pk) {
|
|
34014
35512
|
warn(where, `link.otherKey '${link.otherKey}' \u2014 expected '${pk}' (the target entity's PK)`);
|
|
34015
35513
|
}
|
|
@@ -34058,7 +35556,7 @@ function moduleValidate(args) {
|
|
|
34058
35556
|
const defs = fieldDefsOf[ent];
|
|
34059
35557
|
if (!defs) continue;
|
|
34060
35558
|
if (hasVisibleScalarColumn(defs)) continue;
|
|
34061
|
-
const key =
|
|
35559
|
+
const key = compositeKey(vcode, ent);
|
|
34062
35560
|
if (vcSeen.has(key)) continue;
|
|
34063
35561
|
vcSeen.add(key);
|
|
34064
35562
|
err(
|
|
@@ -34133,7 +35631,7 @@ function moduleValidate(args) {
|
|
|
34133
35631
|
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
34134
35632
|
if (abs) {
|
|
34135
35633
|
try {
|
|
34136
|
-
tx = JSON.parse(
|
|
35634
|
+
tx = JSON.parse(fs11.readFileSync(abs, "utf8"));
|
|
34137
35635
|
} catch {
|
|
34138
35636
|
tx = null;
|
|
34139
35637
|
}
|
|
@@ -34149,6 +35647,236 @@ function moduleValidate(args) {
|
|
|
34149
35647
|
}
|
|
34150
35648
|
}
|
|
34151
35649
|
}
|
|
35650
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
35651
|
+
const fields = e.fields ?? {};
|
|
35652
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
35653
|
+
for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
|
|
35654
|
+
issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
|
|
35655
|
+
}
|
|
35656
|
+
}
|
|
35657
|
+
}
|
|
35658
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
35659
|
+
const isExtension = typeof e.extends === "string" && e.extends.length > 0;
|
|
35660
|
+
const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
|
|
35661
|
+
if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
|
|
35662
|
+
if (!isExtension) {
|
|
35663
|
+
warn(
|
|
35664
|
+
`entities/${name}.json`,
|
|
35665
|
+
`has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
|
|
35666
|
+
);
|
|
35667
|
+
}
|
|
35668
|
+
continue;
|
|
35669
|
+
}
|
|
35670
|
+
if (typeof ts !== "string") {
|
|
35671
|
+
err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
|
|
35672
|
+
continue;
|
|
35673
|
+
}
|
|
35674
|
+
const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
|
|
35675
|
+
if (braces.length === 0) {
|
|
35676
|
+
warn(
|
|
35677
|
+
`entities/${name}.json`,
|
|
35678
|
+
`'toString' is "${ts}" with no {column} placeholder \u2014 every record will render the same label.`
|
|
35679
|
+
);
|
|
35680
|
+
}
|
|
35681
|
+
for (const b of braces) {
|
|
35682
|
+
if (!columnsOf[name].has(b)) {
|
|
35683
|
+
err(
|
|
35684
|
+
`entities/${name}.json`,
|
|
35685
|
+
`'toString' references {${b}}, which is not a column on '${name}'.`
|
|
35686
|
+
);
|
|
35687
|
+
}
|
|
35688
|
+
}
|
|
35689
|
+
}
|
|
35690
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
35691
|
+
const fields = e.fields ?? {};
|
|
35692
|
+
for (const [fname, f] of Object.entries(fields)) {
|
|
35693
|
+
const formula = typeof f?.formula === "string" ? f.formula : "";
|
|
35694
|
+
if (!formula) continue;
|
|
35695
|
+
const agg = parseSetAggregate(formula);
|
|
35696
|
+
if (!agg) continue;
|
|
35697
|
+
const where = `entities/${name}.json \u2192 ${fname}`;
|
|
35698
|
+
if (f.columnType === "F") {
|
|
35699
|
+
err(
|
|
35700
|
+
where,
|
|
35701
|
+
`is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
|
|
35702
|
+
);
|
|
35703
|
+
continue;
|
|
35704
|
+
}
|
|
35705
|
+
if (f.columnType !== "G") continue;
|
|
35706
|
+
const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
|
|
35707
|
+
if (!setCol) {
|
|
35708
|
+
err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
|
|
35709
|
+
continue;
|
|
35710
|
+
}
|
|
35711
|
+
if (setCol.columnType !== "S") {
|
|
35712
|
+
warn(
|
|
35713
|
+
where,
|
|
35714
|
+
`aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
|
|
35715
|
+
);
|
|
35716
|
+
continue;
|
|
35717
|
+
}
|
|
35718
|
+
const childEntity = setCol.link?.entity;
|
|
35719
|
+
if (!childEntity) continue;
|
|
35720
|
+
const childDefs = fieldDefsOf[childEntity];
|
|
35721
|
+
if (!childDefs) continue;
|
|
35722
|
+
const childCol = childDefs[agg.childField];
|
|
35723
|
+
if (!childCol) {
|
|
35724
|
+
err(
|
|
35725
|
+
where,
|
|
35726
|
+
`aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
|
|
35727
|
+
);
|
|
35728
|
+
continue;
|
|
35729
|
+
}
|
|
35730
|
+
const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
|
|
35731
|
+
if (childType === "F" || childType === "R" || childType === "S") {
|
|
35732
|
+
err(
|
|
35733
|
+
where,
|
|
35734
|
+
`aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
|
|
35735
|
+
);
|
|
35736
|
+
}
|
|
35737
|
+
}
|
|
35738
|
+
}
|
|
35739
|
+
for (const [acode, a] of Object.entries(actions)) {
|
|
35740
|
+
const act = a ?? {};
|
|
35741
|
+
const where = `actions \u2192 ${acode}`;
|
|
35742
|
+
const script = typeof act.script === "string" ? act.script : "";
|
|
35743
|
+
if (!script) {
|
|
35744
|
+
err(where, "has no 'script' \u2014 it must be the bare DSL filename (no path, no .dsl extension).");
|
|
35745
|
+
} else if (script.includes("/") || script.includes("\\") || script.endsWith(".dsl")) {
|
|
35746
|
+
err(
|
|
35747
|
+
where,
|
|
35748
|
+
`script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
|
|
35749
|
+
);
|
|
35750
|
+
} else if (!fs11.existsSync(path14.join(paths.logicDir, "actions", `${script}.dsl`))) {
|
|
35751
|
+
err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
|
|
35752
|
+
}
|
|
35753
|
+
const ent = act.entityCode ?? act.entity;
|
|
35754
|
+
if (ent && !isKnownEntity(ent)) {
|
|
35755
|
+
err(where, `targets entity '${ent}', which is not a known entity.`);
|
|
35756
|
+
}
|
|
35757
|
+
}
|
|
35758
|
+
const isKnownAction = (code2) => {
|
|
35759
|
+
if (code2 in actions) return true;
|
|
35760
|
+
const dot = code2.indexOf(".");
|
|
35761
|
+
if (dot > 0) {
|
|
35762
|
+
const mod = code2.slice(0, dot);
|
|
35763
|
+
return deps.has(mod) || mod === manifest.code;
|
|
35764
|
+
}
|
|
35765
|
+
return false;
|
|
35766
|
+
};
|
|
35767
|
+
const triggerFile = readJsonOrDefault(
|
|
35768
|
+
paths.triggers,
|
|
35769
|
+
{}
|
|
35770
|
+
);
|
|
35771
|
+
for (const t of triggerFile.triggers ?? []) {
|
|
35772
|
+
const where = `triggers \u2192 ${String(t.code ?? "?")}`;
|
|
35773
|
+
const act = t.action;
|
|
35774
|
+
if (act && !isKnownAction(act)) {
|
|
35775
|
+
err(where, `fires action '${act}', which is not in ui/actions.json (add it with dforge_action_add first).`);
|
|
35776
|
+
}
|
|
35777
|
+
const ent = t.entity;
|
|
35778
|
+
if (ent && !isKnownEntity(ent)) err(where, `is bound to entity '${ent}', which is not a known entity.`);
|
|
35779
|
+
}
|
|
35780
|
+
const jobFile = readJsonOrDefault(paths.jobs, {});
|
|
35781
|
+
for (const j of jobFile.jobs ?? []) {
|
|
35782
|
+
const where = `jobs \u2192 ${String(j.code ?? "?")}`;
|
|
35783
|
+
const act = j.action;
|
|
35784
|
+
if (act && !isKnownAction(act)) {
|
|
35785
|
+
err(where, `schedules action '${act}', which is not in ui/actions.json.`);
|
|
35786
|
+
}
|
|
35787
|
+
if (act && act in actions) {
|
|
35788
|
+
const script = actions[act]?.script;
|
|
35789
|
+
if (typeof script === "string") {
|
|
35790
|
+
const dslPath = path14.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
35791
|
+
if (fs11.existsSync(dslPath)) {
|
|
35792
|
+
let body = "";
|
|
35793
|
+
try {
|
|
35794
|
+
body = fs11.readFileSync(dslPath, "utf8");
|
|
35795
|
+
} catch {
|
|
35796
|
+
}
|
|
35797
|
+
for (const issue2 of checkDsl(body, { viaJob: true })) {
|
|
35798
|
+
if (issue2.rule !== "job-record-context") continue;
|
|
35799
|
+
issues.push({ level: issue2.level, where, message: issue2.message });
|
|
35800
|
+
}
|
|
35801
|
+
}
|
|
35802
|
+
}
|
|
35803
|
+
}
|
|
35804
|
+
}
|
|
35805
|
+
const webhookFile = readJsonOrDefault(
|
|
35806
|
+
paths.webhooks,
|
|
35807
|
+
{}
|
|
35808
|
+
);
|
|
35809
|
+
for (const w of webhookFile.subscriptions ?? []) {
|
|
35810
|
+
const ent = w.entity;
|
|
35811
|
+
if (ent && !isKnownEntity(ent)) {
|
|
35812
|
+
err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
|
|
35813
|
+
}
|
|
35814
|
+
}
|
|
35815
|
+
for (const [acode, a] of Object.entries(actions)) {
|
|
35816
|
+
const script = a?.script;
|
|
35817
|
+
if (typeof script !== "string" || !script) continue;
|
|
35818
|
+
const dslPath = path14.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
35819
|
+
if (!fs11.existsSync(dslPath)) continue;
|
|
35820
|
+
let body;
|
|
35821
|
+
try {
|
|
35822
|
+
body = fs11.readFileSync(dslPath, "utf8");
|
|
35823
|
+
} catch {
|
|
35824
|
+
continue;
|
|
35825
|
+
}
|
|
35826
|
+
const mode = a.executionMode ?? a.mode;
|
|
35827
|
+
for (const issue2 of checkDsl(body, { executionMode: mode })) {
|
|
35828
|
+
issues.push({
|
|
35829
|
+
level: issue2.level,
|
|
35830
|
+
where: `logic/actions/${script}.dsl`,
|
|
35831
|
+
message: `[${acode}] ${issue2.message}`
|
|
35832
|
+
});
|
|
35833
|
+
}
|
|
35834
|
+
}
|
|
35835
|
+
const folderRoot = readJsonOrDefault(paths.folders, {});
|
|
35836
|
+
for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
|
|
35837
|
+
err(
|
|
35838
|
+
"ui/folders.json",
|
|
35839
|
+
`folder code '${code2}' is used ${dupPaths.length} times (${dupPaths.join(", ")}). Codes must be unique across the whole tree: role rights say 'folder:${code2}' with no path, and translations key on 'folders.${code2}.label', so duplicates are ambiguous and silently collide.`
|
|
35840
|
+
);
|
|
35841
|
+
}
|
|
35842
|
+
const roleCodes = Object.keys(roles);
|
|
35843
|
+
const localeFiles = fs11.existsSync(paths.translationsDir) ? fs11.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
|
|
35844
|
+
for (const raw of supportedLocales) {
|
|
35845
|
+
const locale = raw.trim();
|
|
35846
|
+
if (!locale) continue;
|
|
35847
|
+
if (!resolveTranslationFile(paths.translationsDir, locale)) {
|
|
35848
|
+
err(
|
|
35849
|
+
"translations",
|
|
35850
|
+
`manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
|
|
35851
|
+
);
|
|
35852
|
+
}
|
|
35853
|
+
}
|
|
35854
|
+
if (roleCodes.length > 0 && localeFiles.length === 0) {
|
|
35855
|
+
warn(
|
|
35856
|
+
"translations",
|
|
35857
|
+
`no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
|
|
35858
|
+
);
|
|
35859
|
+
}
|
|
35860
|
+
for (const file2 of localeFiles) {
|
|
35861
|
+
let tx;
|
|
35862
|
+
try {
|
|
35863
|
+
tx = JSON.parse(fs11.readFileSync(path14.join(paths.translationsDir, file2), "utf8"));
|
|
35864
|
+
} catch (ex) {
|
|
35865
|
+
err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
|
|
35866
|
+
continue;
|
|
35867
|
+
}
|
|
35868
|
+
const txRoles = tx.roles ?? {};
|
|
35869
|
+
const missing = roleCodes.filter((rc) => {
|
|
35870
|
+
const entry = txRoles[rc];
|
|
35871
|
+
return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
|
|
35872
|
+
});
|
|
35873
|
+
if (missing.length > 0) {
|
|
35874
|
+
err(
|
|
35875
|
+
`translations/${file2}`,
|
|
35876
|
+
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
35877
|
+
);
|
|
35878
|
+
}
|
|
35879
|
+
}
|
|
34152
35880
|
const errors = issues.filter((i) => i.level === "error");
|
|
34153
35881
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
34154
35882
|
const clean = errors.length === 0 && warnings.length === 0;
|
|
@@ -34314,11 +36042,72 @@ function webhookAdd(args) {
|
|
|
34314
36042
|
);
|
|
34315
36043
|
}
|
|
34316
36044
|
|
|
36045
|
+
// src/tools/apply.ts
|
|
36046
|
+
var fs12 = __toESM(require("fs"));
|
|
36047
|
+
var path15 = __toESM(require("path"));
|
|
36048
|
+
var applyInput = {
|
|
36049
|
+
apply: external_exports.boolean().default(false).describe(
|
|
36050
|
+
"When true, WRITE the resulting files to disk (and remove any `deletes`) instead of returning their contents for the client to write. The response then lists the paths touched. Use it for routine patches the user isn't reviewing line-by-line; leave it false when you want to preview a diff first."
|
|
36051
|
+
)
|
|
36052
|
+
};
|
|
36053
|
+
var isReportPayload = (relPath) => path15.basename(relPath).startsWith("_");
|
|
36054
|
+
function resolveInside(root, relPath) {
|
|
36055
|
+
const abs = path15.resolve(root, relPath);
|
|
36056
|
+
const rel2 = path15.relative(root, abs);
|
|
36057
|
+
if (rel2.startsWith("..") || path15.isAbsolute(rel2)) {
|
|
36058
|
+
throw new Error(
|
|
36059
|
+
`Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
|
|
36060
|
+
);
|
|
36061
|
+
}
|
|
36062
|
+
return abs;
|
|
36063
|
+
}
|
|
36064
|
+
function applyToDisk(moduleDir, result) {
|
|
36065
|
+
const root = path15.resolve(moduleDir);
|
|
36066
|
+
const written = [];
|
|
36067
|
+
const skipped = [];
|
|
36068
|
+
for (const [relPath, contents] of Object.entries(result.files ?? {})) {
|
|
36069
|
+
if (isReportPayload(relPath)) {
|
|
36070
|
+
skipped.push(relPath);
|
|
36071
|
+
continue;
|
|
36072
|
+
}
|
|
36073
|
+
const abs = resolveInside(root, relPath);
|
|
36074
|
+
fs12.mkdirSync(path15.dirname(abs), { recursive: true });
|
|
36075
|
+
fs12.writeFileSync(abs, contents, "utf8");
|
|
36076
|
+
written.push(relPath);
|
|
36077
|
+
}
|
|
36078
|
+
const deleted = [];
|
|
36079
|
+
for (const relPath of result.deletes ?? []) {
|
|
36080
|
+
const abs = resolveInside(root, relPath);
|
|
36081
|
+
let stat;
|
|
36082
|
+
try {
|
|
36083
|
+
stat = fs12.lstatSync(abs);
|
|
36084
|
+
} catch {
|
|
36085
|
+
continue;
|
|
36086
|
+
}
|
|
36087
|
+
if (stat.isDirectory()) {
|
|
36088
|
+
throw new Error(
|
|
36089
|
+
`Refusing to delete '${relPath}': it is a directory, and \`deletes\` is a file-only contract (it exists to remove a renamed entity's old JSON or an orphaned seed file). Remove the directory by hand if that is really what you want.`
|
|
36090
|
+
);
|
|
36091
|
+
}
|
|
36092
|
+
fs12.rmSync(abs, { force: true });
|
|
36093
|
+
deleted.push(relPath);
|
|
36094
|
+
}
|
|
36095
|
+
const out = {
|
|
36096
|
+
summary: result.summary,
|
|
36097
|
+
applied: true,
|
|
36098
|
+
written: written.sort(),
|
|
36099
|
+
deleted: deleted.sort()
|
|
36100
|
+
};
|
|
36101
|
+
if (skipped.length > 0) out.skipped = skipped.sort();
|
|
36102
|
+
if (result.warning) out.warning = result.warning;
|
|
36103
|
+
return out;
|
|
36104
|
+
}
|
|
36105
|
+
|
|
34317
36106
|
// src/resources/index.ts
|
|
34318
|
-
var
|
|
34319
|
-
var
|
|
34320
|
-
var RESOURCES_DIR =
|
|
34321
|
-
var SKILL_DIR =
|
|
36107
|
+
var fs13 = __toESM(require("fs"));
|
|
36108
|
+
var path16 = __toESM(require("path"));
|
|
36109
|
+
var RESOURCES_DIR = path16.resolve(__dirname, "..", "resources");
|
|
36110
|
+
var SKILL_DIR = path16.resolve(__dirname, "..", "skills", "dforge-mcp-author");
|
|
34322
36111
|
function schema(name, label, description) {
|
|
34323
36112
|
return {
|
|
34324
36113
|
uri: `dforge://schema/${name}`,
|
|
@@ -34366,6 +36155,11 @@ var resources = [
|
|
|
34366
36155
|
"Entity JSON schema",
|
|
34367
36156
|
"JSON Schema for entity files under entities/*.json (description, dbObject, toString, traits, fields)."
|
|
34368
36157
|
),
|
|
36158
|
+
schema(
|
|
36159
|
+
"domains",
|
|
36160
|
+
"Column domains JSON schema",
|
|
36161
|
+
'JSON Schema for domains.json \u2014 reusable column domains (base datatype + control + sizing + shared option list) referenced from a field via "domain": "module_cd.domain_cd".'
|
|
36162
|
+
),
|
|
34369
36163
|
schema(
|
|
34370
36164
|
"data-views",
|
|
34371
36165
|
"Data view JSON schema",
|
|
@@ -34444,6 +36238,7 @@ var resources = [
|
|
|
34444
36238
|
reference("field-types", "Field types \u2014 fieldTypeCd (UI control) vs dbDatatype (SQL type), and the correct value for each. Load before adding any field."),
|
|
34445
36239
|
reference("flags", "Column flags \u2014 only V/I/E/M/H; valid combos (VEM/VE/V/EM/I) and why 'VEMHI' is invalid. Load before setting any flags."),
|
|
34446
36240
|
reference("column-types", "The FK + Reference two-column pattern (the #1 source of broken modules) and Set columns. Load before any relation."),
|
|
36241
|
+
reference("column-domains", "Column domains: a reusable field type (datatype + control + shared option list) referenced via a single `domain` key. Use when the same enum/type repeats across columns. Load before authoring domains.json or a domain-backed column."),
|
|
34447
36242
|
reference("formulas", "Formula columns (columnType 'F'): baseDatatypeCd, no dbDatatype, flags 'V', and the formula expression grammar."),
|
|
34448
36243
|
reference("traits", "Entity traits (identity, audit, audit-full, ...). identity \u2192 PK is '{entity}_id'; don't redefine trait columns."),
|
|
34449
36244
|
reference("data-views", "Data views: dataSources array, columns, the order string-array ('order': ['-col','col']), and specialized view configs."),
|
|
@@ -34455,6 +36250,7 @@ var resources = [
|
|
|
34455
36250
|
reference("settings", "Module settings shape."),
|
|
34456
36251
|
reference("jobs", "Scheduled jobs: cron, timeout, jobClass, and the no-record-context constraint."),
|
|
34457
36252
|
reference("number-sequences", "Number sequences for reference numbers / codes."),
|
|
36253
|
+
reference("document-extraction", "OCR / document extraction: ocrExtract() (v2 schema-driven + profile forms), logic/extraction_profiles.json, and the detectDocument() doc-type auto-detect built-in. Load before wiring any OCR/intake action."),
|
|
34458
36254
|
reference("print-templates", "Liquid HTML print templates."),
|
|
34459
36255
|
reference("translations", "Translation files: locale-keyed, every trait-provided field needs an entry."),
|
|
34460
36256
|
reference("queries", "Pre-built saved queries."),
|
|
@@ -34489,53 +36285,53 @@ var resources = [
|
|
|
34489
36285
|
matrixExample("seed-data/01-categories.json", "Matrix example seed: row-axis category records.")
|
|
34490
36286
|
];
|
|
34491
36287
|
function readVendored(rel2) {
|
|
34492
|
-
const p =
|
|
34493
|
-
if (!
|
|
36288
|
+
const p = path16.join(RESOURCES_DIR, rel2);
|
|
36289
|
+
if (!fs13.existsSync(p)) {
|
|
34494
36290
|
return `// Resource missing at build time: ${rel2}
|
|
34495
36291
|
// Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
|
|
34496
36292
|
}
|
|
34497
|
-
return
|
|
36293
|
+
return fs13.readFileSync(p, "utf8");
|
|
34498
36294
|
}
|
|
34499
36295
|
function readSkill(rel2) {
|
|
34500
|
-
const p =
|
|
34501
|
-
if (!
|
|
36296
|
+
const p = path16.join(SKILL_DIR, rel2);
|
|
36297
|
+
if (!fs13.existsSync(p)) {
|
|
34502
36298
|
return `// Skill resource missing: ${rel2}
|
|
34503
36299
|
// Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
|
|
34504
36300
|
}
|
|
34505
|
-
return
|
|
36301
|
+
return fs13.readFileSync(p, "utf8");
|
|
34506
36302
|
}
|
|
34507
36303
|
|
|
34508
36304
|
// src/server.ts
|
|
34509
36305
|
var server = new McpServer({
|
|
34510
36306
|
name: "dforge-mcp",
|
|
34511
|
-
version: "0.
|
|
36307
|
+
version: "0.2.0"
|
|
34512
36308
|
});
|
|
34513
36309
|
function envelope(fn) {
|
|
34514
36310
|
return async (args) => {
|
|
34515
36311
|
try {
|
|
34516
|
-
|
|
36312
|
+
if (args.apply && !args.moduleDir) {
|
|
36313
|
+
throw new Error(
|
|
36314
|
+
"apply: true requires `moduleDir` \u2014 it's the root the file map is written relative to. Pass moduleDir, or omit `apply` to get the file map back for the client to write."
|
|
36315
|
+
);
|
|
36316
|
+
}
|
|
36317
|
+
const result = fn(args);
|
|
36318
|
+
const payload = args.apply ? applyToDisk(args.moduleDir, result) : result;
|
|
34517
36319
|
return {
|
|
34518
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
36320
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
34519
36321
|
};
|
|
34520
36322
|
} catch (e) {
|
|
34521
36323
|
return {
|
|
34522
|
-
content: [
|
|
34523
|
-
{ type: "text", text: `Error: ${e.message}` }
|
|
34524
|
-
],
|
|
36324
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
34525
36325
|
isError: true
|
|
34526
36326
|
};
|
|
34527
36327
|
}
|
|
34528
36328
|
};
|
|
34529
36329
|
}
|
|
34530
|
-
|
|
34531
|
-
|
|
34532
|
-
"Phase 0 owner \u2014 CALL THIS FIRST for any new or resumed module task. Drives the full Phase 0 workflow: 'check' returns current state and exact next steps; 'write_identity' (0a) writes CLAUDE.md; 'write_requirements' (0b) confirms docs/REQUIREMENTS.md (already written to disk by the agent) after user confirms YES and ticks CLAUDE.md; 'write_design' (0c) confirms docs/DESIGN.md (already written to disk by the agent) after user confirms YES and ticks CLAUDE.md; 'validate' (0d) runs pre-scaffold checks and writes docs/VALIDATION.md when all pass. dforge_module_create is blocked until this tool reports readyToScaffold: true.",
|
|
34533
|
-
planModuleSchema,
|
|
34534
|
-
async (args) => {
|
|
36330
|
+
function serialize(fn) {
|
|
36331
|
+
return async (args) => {
|
|
34535
36332
|
try {
|
|
34536
|
-
const result = planModule(args);
|
|
34537
36333
|
return {
|
|
34538
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
36334
|
+
content: [{ type: "text", text: JSON.stringify(fn(args), null, 2) }]
|
|
34539
36335
|
};
|
|
34540
36336
|
} catch (e) {
|
|
34541
36337
|
return {
|
|
@@ -34543,218 +36339,356 @@ server.tool(
|
|
|
34543
36339
|
isError: true
|
|
34544
36340
|
};
|
|
34545
36341
|
}
|
|
34546
|
-
}
|
|
36342
|
+
};
|
|
36343
|
+
}
|
|
36344
|
+
var READ_ONLY = { readOnlyHint: true, idempotentHint: true };
|
|
36345
|
+
var DESTRUCTIVE = { destructiveHint: true };
|
|
36346
|
+
var EXTERNAL = { openWorldHint: true };
|
|
36347
|
+
server.registerTool(
|
|
36348
|
+
"dforge_module_plan",
|
|
36349
|
+
{
|
|
36350
|
+
title: "Plan module (lifecycle orchestrator)",
|
|
36351
|
+
description: "Lifecycle owner \u2014 CALL THIS FIRST for any new or resumed module task. 'check' returns the current phase, the exact next steps, and which authoring skill to run next (design \u2192 build \u2192 ship). Phase 0: 'write_identity' (0a) writes CLAUDE.md; 'write_requirements' (0b) confirms docs/REQUIREMENTS.md (already written to disk by the agent) after the user confirms YES and ticks CLAUDE.md; 'write_design' (0c) confirms docs/DESIGN.md the same way; 'validate' (0d) runs pre-scaffold checks and writes docs/VALIDATION.md when all pass. Phases 1-6: 'complete_phase' records a phase as done or deliberately skipped, so a resumed session never re-proposes finished work. dforge_module_create is blocked until this tool reports readyToScaffold: true.",
|
|
36352
|
+
inputSchema: planModuleSchema
|
|
36353
|
+
},
|
|
36354
|
+
serialize(planModule)
|
|
34547
36355
|
);
|
|
34548
|
-
server.
|
|
36356
|
+
server.registerTool(
|
|
34549
36357
|
"dforge_module_create",
|
|
34550
|
-
|
|
34551
|
-
|
|
34552
|
-
|
|
36358
|
+
{
|
|
36359
|
+
title: "Scaffold a new module",
|
|
36360
|
+
description: "Scaffold a new dForge module (Phase 1). \u26D4 REQUIRES Phase 0 complete \u2014 call dforge_module_plan first. Blocked until CLAUDE.md, docs/REQUIREMENTS.md, docs/DESIGN.md, and docs/VALIDATION.md (all-pass) exist in moduleDir. Returns { files: { '<relPath>': '<contents>' } } \u2014 preview with the user before writing, or pass apply: true to write directly.",
|
|
36361
|
+
inputSchema: { ...createModuleSchema, ...applyInput }
|
|
36362
|
+
},
|
|
36363
|
+
envelope((args) => {
|
|
34553
36364
|
const files = createModuleFiles(args);
|
|
34554
|
-
return
|
|
34555
|
-
|
|
34556
|
-
|
|
34557
|
-
|
|
34558
|
-
|
|
34559
|
-
{
|
|
34560
|
-
summary: `Generated ${Object.keys(files).length} files for module '${args.code}' (preset: ${args.preset}, ${args.entities.length} entit${args.entities.length === 1 ? "y" : "ies"}).`,
|
|
34561
|
-
files
|
|
34562
|
-
},
|
|
34563
|
-
null,
|
|
34564
|
-
2
|
|
34565
|
-
)
|
|
34566
|
-
}
|
|
34567
|
-
]
|
|
34568
|
-
};
|
|
34569
|
-
}
|
|
36365
|
+
return makeResult(
|
|
36366
|
+
`Generated ${Object.keys(files).length} files for module '${args.code}' (preset: ${args.preset}, ${args.entities.length} entit${args.entities.length === 1 ? "y" : "ies"}).`,
|
|
36367
|
+
files
|
|
36368
|
+
);
|
|
36369
|
+
})
|
|
34570
36370
|
);
|
|
34571
|
-
server.
|
|
36371
|
+
server.registerTool(
|
|
34572
36372
|
"dforge_module_inspect",
|
|
34573
|
-
|
|
34574
|
-
|
|
34575
|
-
|
|
36373
|
+
{
|
|
36374
|
+
title: "Inspect module state",
|
|
36375
|
+
description: "Read the current state of an existing module from disk and return a structured summary \u2014 entities + fields, views + sources, roles + rights, actions, triggers, jobs, webhooks, reports, settings, queries, print templates, domains, seed files, translations. Call this BEFORE any patch tool so you know what already exists. The full structured state is in files['_inspect.json']; `summary` is one-line stats.",
|
|
36376
|
+
inputSchema: moduleInspectSchema,
|
|
36377
|
+
annotations: READ_ONLY
|
|
36378
|
+
},
|
|
36379
|
+
serialize(moduleInspect)
|
|
34576
36380
|
);
|
|
34577
|
-
server.
|
|
36381
|
+
server.registerTool(
|
|
34578
36382
|
"dforge_module_validate",
|
|
34579
|
-
|
|
34580
|
-
|
|
34581
|
-
|
|
36383
|
+
{
|
|
36384
|
+
title: "Validate module (offline)",
|
|
36385
|
+
description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks, and translation completeness (role labels are install-blocking). Returns errors + warnings in _validate.json. Fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
|
|
36386
|
+
inputSchema: moduleValidateSchema,
|
|
36387
|
+
annotations: READ_ONLY
|
|
36388
|
+
},
|
|
36389
|
+
serialize(moduleValidate)
|
|
34582
36390
|
);
|
|
34583
|
-
server.
|
|
36391
|
+
server.registerTool(
|
|
34584
36392
|
"dforge_module_pack",
|
|
34585
|
-
|
|
34586
|
-
|
|
34587
|
-
|
|
34588
|
-
|
|
34589
|
-
|
|
34590
|
-
}
|
|
36393
|
+
{
|
|
36394
|
+
title: "Pack module to .dforge",
|
|
36395
|
+
description: "Pack a module directory into a .dforge tarball. Uses the bundled dforge-cli package, PATH fallback, or DFORGE_CLI_BINARY override. Refuses to build if any entity lacks a role granting Select (the Phase 5a gate).",
|
|
36396
|
+
inputSchema: packModuleSchema,
|
|
36397
|
+
annotations: EXTERNAL
|
|
36398
|
+
},
|
|
36399
|
+
serialize(packModule)
|
|
34591
36400
|
);
|
|
34592
|
-
server.
|
|
36401
|
+
server.registerTool(
|
|
34593
36402
|
"dforge_module_install",
|
|
34594
|
-
|
|
34595
|
-
|
|
36403
|
+
{
|
|
36404
|
+
title: "Install module to a tenant",
|
|
36405
|
+
description: "PHASE 6: Install a module (directory or .dforge tarball) to a running tenant. Runs the FULL server-side validator \u2014 the only real validator. Reads DFORGE_URL / DFORGE_TOKEN env if not passed as args. Always returns raw CLI output, exitCode, and command so the agent can fix module defects and retry.",
|
|
36406
|
+
inputSchema: installModuleSchema,
|
|
36407
|
+
annotations: { ...EXTERNAL, ...DESTRUCTIVE }
|
|
36408
|
+
},
|
|
34596
36409
|
async (args) => {
|
|
34597
|
-
|
|
34598
|
-
|
|
34599
|
-
|
|
34600
|
-
|
|
34601
|
-
|
|
36410
|
+
try {
|
|
36411
|
+
const result = installModule(args);
|
|
36412
|
+
return {
|
|
36413
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
36414
|
+
isError: !result.ok
|
|
36415
|
+
};
|
|
36416
|
+
} catch (e) {
|
|
36417
|
+
return {
|
|
36418
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
36419
|
+
isError: true
|
|
36420
|
+
};
|
|
36421
|
+
}
|
|
34602
36422
|
}
|
|
34603
36423
|
);
|
|
34604
|
-
server.
|
|
36424
|
+
server.registerTool(
|
|
34605
36425
|
"dforge_entity_add",
|
|
34606
|
-
|
|
34607
|
-
|
|
34608
|
-
|
|
36426
|
+
{
|
|
36427
|
+
title: "Add an entity",
|
|
36428
|
+
description: "PHASE 1: Add an entity to an existing module. Reads manifest from disk, returns the updated file map (manifest + new entity + regenerated UI/security). Other files on disk are NOT touched.",
|
|
36429
|
+
inputSchema: { ...addEntitySchema, ...applyInput }
|
|
36430
|
+
},
|
|
36431
|
+
envelope((args) => {
|
|
34609
36432
|
const { files, warning } = addEntityFiles(args);
|
|
34610
|
-
return {
|
|
34611
|
-
|
|
34612
|
-
{
|
|
34613
|
-
type: "text",
|
|
34614
|
-
text: JSON.stringify(
|
|
34615
|
-
{
|
|
34616
|
-
summary: `Adds entity '${args.entity.name}' to ${args.moduleDir}.`,
|
|
34617
|
-
warning,
|
|
34618
|
-
files
|
|
34619
|
-
},
|
|
34620
|
-
null,
|
|
34621
|
-
2
|
|
34622
|
-
)
|
|
34623
|
-
}
|
|
34624
|
-
]
|
|
34625
|
-
};
|
|
34626
|
-
}
|
|
36433
|
+
return makeResult(`Adds entity '${args.entity.name}' to ${args.moduleDir}.`, files, warning);
|
|
36434
|
+
})
|
|
34627
36435
|
);
|
|
34628
|
-
server.
|
|
36436
|
+
server.registerTool(
|
|
34629
36437
|
"dforge_module_import",
|
|
34630
|
-
|
|
34631
|
-
|
|
36438
|
+
{
|
|
36439
|
+
title: "Import a table-spec as entities",
|
|
36440
|
+
description: "Import a normalized table-spec (tables \u2192 columns \u2192 relationships) into an existing module as entities. Infers each column's fieldTypeCd from an explicit code, a source SQL type (sqlType), sample values, and name heuristics (validated against the metadata registry; dbDatatype derived), and generates the FK+Reference two-column pattern for each relationship. The front-end that produces the table-spec can be DBML/SQL, an Excel/CSV upload, or hand-authored. Scaffold a minimal module first; this ADDS entities and regenerates default views/menus/roles. Review inferred types and run dforge_module_validate after.",
|
|
36441
|
+
inputSchema: { ...moduleImportSchema, ...applyInput }
|
|
36442
|
+
},
|
|
34632
36443
|
envelope(moduleImport)
|
|
34633
36444
|
);
|
|
34634
|
-
server.
|
|
36445
|
+
server.registerTool(
|
|
34635
36446
|
"dforge_entity_rename",
|
|
34636
|
-
|
|
34637
|
-
|
|
36447
|
+
{
|
|
36448
|
+
title: "Rename an entity (refactor-safe)",
|
|
36449
|
+
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
36450
|
+
inputSchema: { ...entityRenameSchema, ...applyInput },
|
|
36451
|
+
annotations: DESTRUCTIVE
|
|
36452
|
+
},
|
|
34638
36453
|
envelope(entityRename)
|
|
34639
36454
|
);
|
|
34640
|
-
server.
|
|
36455
|
+
server.registerTool(
|
|
34641
36456
|
"dforge_entity_delete",
|
|
34642
|
-
|
|
34643
|
-
|
|
36457
|
+
{
|
|
36458
|
+
title: "Delete an entity (refactor-safe)",
|
|
36459
|
+
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
36460
|
+
inputSchema: { ...entityDeleteSchema, ...applyInput },
|
|
36461
|
+
annotations: DESTRUCTIVE
|
|
36462
|
+
},
|
|
34644
36463
|
envelope(entityDelete)
|
|
34645
36464
|
);
|
|
34646
|
-
server.
|
|
36465
|
+
server.registerTool(
|
|
34647
36466
|
"dforge_entity_field_add",
|
|
34648
|
-
|
|
34649
|
-
|
|
36467
|
+
{
|
|
36468
|
+
title: "Add a field",
|
|
36469
|
+
description: "PHASE 1 / backtrack: Add a single new field to an existing entity. Use this (not entity_add) when refining an existing entity \u2014 it preserves the rest of the definition. For a RELATION, a ROLL-UP total, or a STATUS column, prefer the composite tools (dforge_entity_reference_add / _rollup_add / _status_add): each of those concepts spans several coordinated keys, and the composite tools emit the whole shape so the broken variants aren't representable.",
|
|
36470
|
+
inputSchema: { ...entityFieldAddSchema, ...applyInput }
|
|
36471
|
+
},
|
|
34650
36472
|
envelope(entityFieldAdd)
|
|
34651
36473
|
);
|
|
34652
|
-
server.
|
|
36474
|
+
server.registerTool(
|
|
34653
36475
|
"dforge_entity_field_modify",
|
|
34654
|
-
|
|
34655
|
-
|
|
36476
|
+
{
|
|
36477
|
+
title: "Modify a field",
|
|
36478
|
+
description: "PHASE 1 / backtrack: Replace an existing field's spec on an entity. Pass the full desired field shape, not a partial diff.",
|
|
36479
|
+
inputSchema: { ...entityFieldModifySchema, ...applyInput }
|
|
36480
|
+
},
|
|
34656
36481
|
envelope(entityFieldModify)
|
|
34657
36482
|
);
|
|
34658
|
-
server.
|
|
36483
|
+
server.registerTool(
|
|
34659
36484
|
"dforge_entity_field_remove",
|
|
34660
|
-
|
|
34661
|
-
|
|
36485
|
+
{
|
|
36486
|
+
title: "Remove a field (refactor-safe)",
|
|
36487
|
+
description: "Refactor-safe field removal. Removes the field AND cascade-cleans the safe dependents: the paired Reference column when you remove its hidden FK, the references entry, view columns + order, and seed-data keys. Formula references and other entities' FKs pointing at the field are surfaced as warnings (not auto-deleted). Run dforge_module_validate after.",
|
|
36488
|
+
inputSchema: { ...entityFieldRemoveSchema, ...applyInput },
|
|
36489
|
+
annotations: DESTRUCTIVE
|
|
36490
|
+
},
|
|
34662
36491
|
envelope(entityFieldRemove)
|
|
34663
36492
|
);
|
|
34664
|
-
server.
|
|
36493
|
+
server.registerTool(
|
|
34665
36494
|
"dforge_entity_field_rename",
|
|
34666
|
-
|
|
34667
|
-
|
|
36495
|
+
{
|
|
36496
|
+
title: "Rename a field (refactor-safe)",
|
|
36497
|
+
description: "Refactor-safe rename of a field. Unlike field_modify, this PROPAGATES the new name to every reference: the paired Reference column's link.thisKey + references block, same-entity formula columns ([oldName] \u2192 [newName]), data view columns + order arrays, seed-data records for the entity, and OTHER entities' FKs that target this field. Returns the full set of changed files; review then write, and run dforge_module_validate after to confirm nothing dangles.",
|
|
36498
|
+
inputSchema: { ...entityFieldRenameSchema, ...applyInput }
|
|
36499
|
+
},
|
|
34668
36500
|
envelope(entityFieldRename)
|
|
34669
36501
|
);
|
|
34670
|
-
server.
|
|
36502
|
+
server.registerTool(
|
|
36503
|
+
"dforge_entity_reference_add",
|
|
36504
|
+
{
|
|
36505
|
+
title: "Add a relation (FK + Reference)",
|
|
36506
|
+
description: "PHASE 1: Add a relation between two entities in ONE call. Emits the complete FK+Reference pattern \u2014 the documented #1 source of broken modules \u2014 as all three of its parts: the hidden FK column (dbDatatype 'cuid', flags 'EM', no fieldTypeCd), the visible Reference column (columnType 'R', fieldTypeCd 'lookup', flags VEM/VE, link{entity,thisKey,otherKey}), and the `references` block entry. Use this instead of two dforge_entity_field_add calls \u2014 it can't emit the one-column form that fails install.",
|
|
36507
|
+
inputSchema: { ...entityReferenceAddSchema, ...applyInput }
|
|
36508
|
+
},
|
|
36509
|
+
envelope(entityReferenceAdd)
|
|
36510
|
+
);
|
|
36511
|
+
server.registerTool(
|
|
36512
|
+
"dforge_entity_rollup_add",
|
|
36513
|
+
{
|
|
36514
|
+
title: "Add a roll-up total",
|
|
36515
|
+
description: "PHASE 1: Add a roll-up total over child rows (SUM/COUNT/AVG/MIN/MAX) in ONE call. Emits a GENERATED ('G') column \u2014 never a Formula ('F'), whose set-aggregates silently render empty \u2014 and creates the parent's Set column if it doesn't exist yet. Refuses to aggregate a virtual F/R/S child column, the documented `column old.<field> does not exist` install failure.",
|
|
36516
|
+
inputSchema: { ...entityRollupAddSchema, ...applyInput }
|
|
36517
|
+
},
|
|
36518
|
+
envelope(entityRollupAdd)
|
|
36519
|
+
);
|
|
36520
|
+
server.registerTool(
|
|
36521
|
+
"dforge_entity_status_add",
|
|
36522
|
+
{
|
|
36523
|
+
title: "Add a status column",
|
|
36524
|
+
description: "PHASE 1: Add a status/stage column in ONE call. Emits a dropdown with params.options ({value,label} objects, never bare strings at the field root) and the initial value as a `formula` \u2014 entity fields have NO defaultValue key, which the entity schema rejects outright. An entity with a 3+ value status is also the objective trigger for a kanban view in Phase 3.",
|
|
36525
|
+
inputSchema: { ...entityStatusAddSchema, ...applyInput }
|
|
36526
|
+
},
|
|
36527
|
+
envelope(entityStatusAdd)
|
|
36528
|
+
);
|
|
36529
|
+
server.registerTool(
|
|
34671
36530
|
"dforge_action_add",
|
|
34672
|
-
|
|
34673
|
-
|
|
36531
|
+
{
|
|
36532
|
+
title: "Add a DSL action",
|
|
36533
|
+
description: "PHASE 2: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json, and runs the DSL static checker first \u2014 errors reject the call. **Load dforge://docs/dsl before authoring.**",
|
|
36534
|
+
inputSchema: { ...actionAddSchema, ...applyInput }
|
|
36535
|
+
},
|
|
34674
36536
|
envelope(actionAdd)
|
|
34675
36537
|
);
|
|
34676
|
-
server.
|
|
36538
|
+
server.registerTool(
|
|
36539
|
+
"dforge_action_check",
|
|
36540
|
+
{
|
|
36541
|
+
title: "Check action DSL",
|
|
36542
|
+
description: "PHASE 2: Statically check an action's DSL \u2014 either a draft `dslBody` BEFORE calling dforge_action_add, or an `actionCode` already on disk. Catches the documented install/runtime failures without a tenant round trip: TODAY()/NOW() inside execute: ('TODAY is not defined'), [field] record-context in batch mode or a scheduled job, block order/duplication, top-level return, ':param' SQL placeholders (dForge binds '@param'), and unknown host functions. Read files['_action_check.json'].",
|
|
36543
|
+
inputSchema: actionCheckSchema,
|
|
36544
|
+
annotations: READ_ONLY
|
|
36545
|
+
},
|
|
36546
|
+
serialize(actionCheck)
|
|
36547
|
+
);
|
|
36548
|
+
server.registerTool(
|
|
34677
36549
|
"dforge_trigger_add",
|
|
34678
|
-
|
|
34679
|
-
|
|
36550
|
+
{
|
|
36551
|
+
title: "Add a DB-event trigger",
|
|
36552
|
+
description: "PHASE 2: Add a trigger that fires an action on a DB event (insert/update/delete/status_change/any) optionally gated by a condition formula. Appends to logic/triggers.json. **Use a trigger when the platform should react to data changes WITHOUT user interaction**; use jobs for cron-driven; use webhooks for outbound HTTP. The target action must already exist in ui/actions.json.",
|
|
36553
|
+
inputSchema: { ...triggerAddSchema, ...applyInput }
|
|
36554
|
+
},
|
|
34680
36555
|
envelope(triggerAdd)
|
|
34681
36556
|
);
|
|
34682
|
-
server.
|
|
36557
|
+
server.registerTool(
|
|
34683
36558
|
"dforge_job_add",
|
|
34684
|
-
|
|
34685
|
-
|
|
36559
|
+
{
|
|
36560
|
+
title: "Schedule a job",
|
|
36561
|
+
description: "PHASE 2: Schedule an existing action to fire on a 5-field cron. Appends to logic/jobs.json. The action must NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as the system user with no current record (dforge_module_validate flags this).",
|
|
36562
|
+
inputSchema: { ...jobAddSchema, ...applyInput }
|
|
36563
|
+
},
|
|
34686
36564
|
envelope(jobAdd)
|
|
34687
36565
|
);
|
|
34688
|
-
server.
|
|
36566
|
+
server.registerTool(
|
|
34689
36567
|
"dforge_webhook_add",
|
|
34690
|
-
|
|
34691
|
-
|
|
36568
|
+
{
|
|
36569
|
+
title: "Add an outbound webhook",
|
|
36570
|
+
description: "PHASE 2: Subscribe an outbound HTTP endpoint to a DB event. Appends to logic/webhooks.json. Use for integrations with external systems (Slack, Zapier, custom dashboards).",
|
|
36571
|
+
inputSchema: { ...webhookAddSchema, ...applyInput }
|
|
36572
|
+
},
|
|
34692
36573
|
envelope(webhookAdd)
|
|
34693
36574
|
);
|
|
34694
|
-
server.
|
|
36575
|
+
server.registerTool(
|
|
34695
36576
|
"dforge_view_add",
|
|
34696
|
-
|
|
34697
|
-
|
|
36577
|
+
{
|
|
36578
|
+
title: "Add a data view",
|
|
36579
|
+
description: "PHASE 3: Add a data view to ui/data_views.json. viewType-specific viewConfig is supplied verbatim \u2014 pull dforge://schema/data-views first to know the shape. Every entity needs a default grid before any specialized view.",
|
|
36580
|
+
inputSchema: { ...viewAddSchema, ...applyInput }
|
|
36581
|
+
},
|
|
34698
36582
|
envelope(viewAdd)
|
|
34699
36583
|
);
|
|
34700
|
-
server.
|
|
36584
|
+
server.registerTool(
|
|
34701
36585
|
"dforge_view_modify",
|
|
34702
|
-
|
|
34703
|
-
|
|
36586
|
+
{
|
|
36587
|
+
title: "Modify a data view",
|
|
36588
|
+
description: "PHASE 3 / backtrack: Replace an existing view spec. Pass the full desired shape.",
|
|
36589
|
+
inputSchema: { ...viewModifySchema, ...applyInput }
|
|
36590
|
+
},
|
|
34704
36591
|
envelope(viewModify)
|
|
34705
36592
|
);
|
|
34706
|
-
server.
|
|
36593
|
+
server.registerTool(
|
|
36594
|
+
"dforge_menu_add",
|
|
36595
|
+
{
|
|
36596
|
+
title: "Add a menu item",
|
|
36597
|
+
description: "PHASE 3: Add a leaf or section to ui/menus.json. Pass dataViewCode for a leaf (validated against ui/data_views.json, and emitted with itemType 'V'); omit it for a section node (which correctly omits itemType). Bootstrap icon names are normalized to the bare form menus require \u2014 a leading 'bi-' is stripped, unlike action icons which keep it.",
|
|
36598
|
+
inputSchema: { ...menuAddSchema, ...applyInput }
|
|
36599
|
+
},
|
|
36600
|
+
envelope(menuAdd)
|
|
36601
|
+
);
|
|
36602
|
+
server.registerTool(
|
|
34707
36603
|
"dforge_report_add",
|
|
34708
|
-
|
|
34709
|
-
|
|
36604
|
+
{
|
|
36605
|
+
title: "Add a report",
|
|
36606
|
+
description: "PHASE 3: Add a report to ui/reports.json. Read dforge://schema/reports for the layout/datasets/parameters shape.",
|
|
36607
|
+
inputSchema: { ...reportAddSchema, ...applyInput }
|
|
36608
|
+
},
|
|
34710
36609
|
envelope(reportAdd)
|
|
34711
36610
|
);
|
|
34712
|
-
server.
|
|
36611
|
+
server.registerTool(
|
|
34713
36612
|
"dforge_setting_add",
|
|
34714
|
-
|
|
34715
|
-
|
|
36613
|
+
{
|
|
36614
|
+
title: "Add a module setting",
|
|
36615
|
+
description: "PHASE 4 (polish): Add a configurable module-level setting to settings.json. Settings are folder-scoped at runtime \u2014 values inherit through the folder tree.",
|
|
36616
|
+
inputSchema: { ...settingAddSchema, ...applyInput }
|
|
36617
|
+
},
|
|
34716
36618
|
envelope(settingAdd)
|
|
34717
36619
|
);
|
|
34718
|
-
server.
|
|
36620
|
+
server.registerTool(
|
|
36621
|
+
"dforge_translation_sync",
|
|
36622
|
+
{
|
|
36623
|
+
title: "Sync translation files",
|
|
36624
|
+
description: "PHASE 4: Generate or refresh translations/<locale>.json from what the module actually contains \u2014 entity + field labels (including trait-provided columns), views, menus, roles, actions, folders, settings. Existing translated text is NEVER overwritten; missing keys are seeded with the English source so the file is immediately installable. Defaults to en-US plus every manifest.supportedLocales entry \u2014 exactly the set install requires. Run it after any entity/view/role change: role labels are completeness-enforced and a missing one FAILS install.",
|
|
36625
|
+
inputSchema: { ...translationSyncSchema, ...applyInput }
|
|
36626
|
+
},
|
|
36627
|
+
envelope(translationSync)
|
|
36628
|
+
);
|
|
36629
|
+
server.registerTool(
|
|
36630
|
+
"dforge_seed_add",
|
|
36631
|
+
{
|
|
36632
|
+
title: "Add seed data",
|
|
36633
|
+
description: "PHASE 4: Write a seed-data file for one entity. Enforces the four documented seed traps: an explicit NUMERIC PK under '{entity}_id' (cuid is int8, not a UUID), parent-before-child load order via the NN- filename prefix, created_by/last_updated_by = 0 (System user) on every record of an audit-full entity, and FK values that point at an already-seeded parent. Also rejects seeding a virtual Reference column instead of its hidden FK.",
|
|
36634
|
+
inputSchema: { ...seedAddSchema, ...applyInput }
|
|
36635
|
+
},
|
|
36636
|
+
envelope(seedAdd)
|
|
36637
|
+
);
|
|
36638
|
+
server.registerTool(
|
|
34719
36639
|
"dforge_role_add",
|
|
34720
|
-
|
|
34721
|
-
|
|
36640
|
+
{
|
|
36641
|
+
title: "Add a role",
|
|
36642
|
+
description: "PHASE 5: Add a role to security/roles.json. Roles are namespaced (e.g. 'crm.admin'). Rights are S/I/U/D/C for entities, E for actions/reports/folders (which use a COLON prefix). Derive roles from the intake's user types \u2014 never a generic admin/contributor/viewer taxonomy the user didn't ask for.",
|
|
36643
|
+
inputSchema: { ...roleAddSchema, ...applyInput }
|
|
36644
|
+
},
|
|
34722
36645
|
envelope(roleAdd)
|
|
34723
36646
|
);
|
|
34724
|
-
server.
|
|
36647
|
+
server.registerTool(
|
|
34725
36648
|
"dforge_role_right_set",
|
|
34726
|
-
|
|
34727
|
-
|
|
36649
|
+
{
|
|
36650
|
+
title: "Grant or revoke one right",
|
|
36651
|
+
description: "PHASE 5 / backtrack: Grant or revoke a single right on a single object for a role. Cheaper than role_add when iterating on the rights matrix, and the correct tool for amending the scaffolded admin role (role_add fails on an existing code).",
|
|
36652
|
+
inputSchema: { ...roleRightSetSchema, ...applyInput }
|
|
36653
|
+
},
|
|
34728
36654
|
envelope(roleRightSet)
|
|
34729
36655
|
);
|
|
34730
|
-
server.
|
|
36656
|
+
server.registerTool(
|
|
34731
36657
|
"dforge_folder_add",
|
|
34732
|
-
|
|
34733
|
-
|
|
36658
|
+
{
|
|
36659
|
+
title: "Add a security folder",
|
|
36660
|
+
description: "PHASE 5 (optional): Add a sub-folder to ui/folders.json. Folders are SECURITY boundaries (row-level filters + per-folder role mappings). Most modules don't need any beyond the root \u2014 only use when intake said data must be separated per folder.",
|
|
36661
|
+
inputSchema: { ...folderAddSchema, ...applyInput }
|
|
36662
|
+
},
|
|
34734
36663
|
envelope(folderAdd)
|
|
34735
36664
|
);
|
|
34736
|
-
server.
|
|
36665
|
+
server.registerTool(
|
|
34737
36666
|
"dforge_dependency_add",
|
|
34738
|
-
|
|
34739
|
-
|
|
36667
|
+
{
|
|
36668
|
+
title: "Add a module dependency",
|
|
36669
|
+
description: "Add a dependency on another dForge module to manifest.json. Use the entities list form when only specific entities are imported (smaller coupling).",
|
|
36670
|
+
inputSchema: { ...dependencyAddSchema, ...applyInput }
|
|
36671
|
+
},
|
|
34740
36672
|
envelope(dependencyAdd)
|
|
34741
36673
|
);
|
|
34742
|
-
server.
|
|
36674
|
+
server.registerTool(
|
|
34743
36675
|
"dforge_dbml_import",
|
|
34744
|
-
|
|
34745
|
-
|
|
36676
|
+
{
|
|
36677
|
+
title: "Import entities from DBML",
|
|
36678
|
+
description: "Generate entities from DBML schema text (a front-end to dforge_module_import). Parses Table blocks, typed columns with [settings], inline [ref: > t.c] and top-level Ref: lines; drops the source PK (the identity trait provides {entity}_id), infers field types via the metadata registry, and builds the FK+Reference pair per relationship. Pass `module` when the dir has no manifest (greenfield). Review inferred types + run dforge_module_validate after.",
|
|
36679
|
+
inputSchema: { ...dbmlImportSchema, ...applyInput }
|
|
36680
|
+
},
|
|
34746
36681
|
envelope(dbmlImport)
|
|
34747
36682
|
);
|
|
34748
36683
|
for (const res of resources) {
|
|
34749
|
-
server.
|
|
34750
|
-
|
|
34751
|
-
|
|
34752
|
-
|
|
34753
|
-
|
|
34754
|
-
|
|
34755
|
-
|
|
34756
|
-
|
|
34757
|
-
}));
|
|
36684
|
+
server.registerResource(
|
|
36685
|
+
res.name,
|
|
36686
|
+
res.uri,
|
|
36687
|
+
{ description: res.description, mimeType: res.mimeType },
|
|
36688
|
+
async (uri) => ({
|
|
36689
|
+
contents: [{ uri: uri.href, mimeType: res.mimeType, text: res.read() }]
|
|
36690
|
+
})
|
|
36691
|
+
);
|
|
34758
36692
|
}
|
|
34759
36693
|
(async () => {
|
|
34760
36694
|
const transport = new StdioServerTransport();
|