@dforge-core/dforge-mcp 0.2.30 → 0.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/server.js +517 -196
- package/package.json +48 -46
- package/resources/docs/dsl-reference.md +12 -7
- package/resources/schemas/diagram.schema.json +8 -5
- package/resources/schemas/entity.schema.json +14 -1
- package/resources/schemas/manifest.schema.json +2 -2
- package/resources/schemas/traits.schema.json +5 -0
- package/resources/schemas/translations.schema.json +267 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +11 -4
- package/skills/dforge-mcp-author/references/formulas.md +11 -4
- package/skills/dforge-mcp-author/references/translations.md +17 -0
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.25/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31211,7 +31211,8 @@ var SCHEMA_BINDINGS = [
|
|
|
31211
31211
|
{ fileMatch: ["logic/webhooks.json"], schema: "webhooks" },
|
|
31212
31212
|
{ fileMatch: ["deps/*.json"], schema: "deps" },
|
|
31213
31213
|
{ fileMatch: ["seed-data/*.json"], schema: "seed-data" },
|
|
31214
|
-
{ fileMatch: ["settings.json"], schema: "settings" }
|
|
31214
|
+
{ fileMatch: ["settings.json"], schema: "settings" },
|
|
31215
|
+
{ fileMatch: ["translations/*.json"], schema: "translations" }
|
|
31215
31216
|
];
|
|
31216
31217
|
function schemaEntries() {
|
|
31217
31218
|
return SCHEMA_BINDINGS.map((b) => ({
|
|
@@ -31284,7 +31285,7 @@ function buildZedTasks() {
|
|
|
31284
31285
|
var fs = __toESM(require("fs"));
|
|
31285
31286
|
var path = __toESM(require("path"));
|
|
31286
31287
|
|
|
31287
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31288
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.32/node_modules/@dforge-core/metadata/dist/index.js
|
|
31288
31289
|
var AggType = {
|
|
31289
31290
|
Sum: "sum",
|
|
31290
31291
|
Count: "count",
|
|
@@ -34590,9 +34591,13 @@ function entityDelete(args) {
|
|
|
34590
34591
|
}
|
|
34591
34592
|
|
|
34592
34593
|
// src/tools/action-add.ts
|
|
34594
|
+
var path10 = __toESM(require("path"));
|
|
34595
|
+
|
|
34596
|
+
// src/tools/validate/translations.ts
|
|
34597
|
+
var fs7 = __toESM(require("fs"));
|
|
34593
34598
|
var path9 = __toESM(require("path"));
|
|
34594
34599
|
|
|
34595
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34600
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.32/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34596
34601
|
var PUNCT3 = ["===", "!=="];
|
|
34597
34602
|
var PUNCT2 = [
|
|
34598
34603
|
"==",
|
|
@@ -34867,24 +34872,48 @@ function stringValue(token) {
|
|
|
34867
34872
|
}
|
|
34868
34873
|
function templateChunkValue(token) {
|
|
34869
34874
|
if (token.kind !== "template") return token.text;
|
|
34870
|
-
const body = token.text.slice(1);
|
|
34875
|
+
const body = token.text.slice(1).replace(/\r\n?/g, "\n");
|
|
34871
34876
|
if (body.endsWith("${")) return unescape2(body.slice(0, -2));
|
|
34872
34877
|
if (body.endsWith("`")) return unescape2(body.slice(0, -1));
|
|
34873
34878
|
return unescape2(body);
|
|
34874
34879
|
}
|
|
34875
34880
|
function unescape2(body) {
|
|
34876
|
-
return body.replace(
|
|
34877
|
-
|
|
34878
|
-
|
|
34879
|
-
|
|
34880
|
-
|
|
34881
|
-
|
|
34882
|
-
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
|
|
34881
|
+
return body.replace(
|
|
34882
|
+
/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])/g,
|
|
34883
|
+
(_, esc2) => {
|
|
34884
|
+
switch (esc2[0]) {
|
|
34885
|
+
case "n":
|
|
34886
|
+
return "\n";
|
|
34887
|
+
case "t":
|
|
34888
|
+
return " ";
|
|
34889
|
+
case "r":
|
|
34890
|
+
return "\r";
|
|
34891
|
+
case "b":
|
|
34892
|
+
return "\b";
|
|
34893
|
+
case "f":
|
|
34894
|
+
return "\f";
|
|
34895
|
+
case "v":
|
|
34896
|
+
return "\v";
|
|
34897
|
+
case "0":
|
|
34898
|
+
return "\0";
|
|
34899
|
+
case "\r":
|
|
34900
|
+
case "\n":
|
|
34901
|
+
case "\u2028":
|
|
34902
|
+
case "\u2029":
|
|
34903
|
+
return "";
|
|
34904
|
+
case "u":
|
|
34905
|
+
case "x":
|
|
34906
|
+
if (esc2.length > 1) {
|
|
34907
|
+
const hex3 = esc2[1] === "{" ? esc2.slice(2, -1) : esc2.slice(1);
|
|
34908
|
+
const cp = parseInt(hex3, 16);
|
|
34909
|
+
return cp <= 1114111 ? String.fromCodePoint(cp) : esc2;
|
|
34910
|
+
}
|
|
34911
|
+
return esc2;
|
|
34912
|
+
default:
|
|
34913
|
+
return esc2;
|
|
34914
|
+
}
|
|
34886
34915
|
}
|
|
34887
|
-
|
|
34916
|
+
);
|
|
34888
34917
|
}
|
|
34889
34918
|
var BLOCK_KINDS = [
|
|
34890
34919
|
"params",
|
|
@@ -35027,10 +35056,10 @@ function templateArg(tokens, start) {
|
|
|
35027
35056
|
for (let j = start; j < tokens.length; j++) {
|
|
35028
35057
|
const t = tokens[j];
|
|
35029
35058
|
if (t.kind !== "template") continue;
|
|
35030
|
-
const
|
|
35031
|
-
if (
|
|
35059
|
+
const opens2 = t.text.startsWith("`");
|
|
35060
|
+
if (opens2) depth++;
|
|
35032
35061
|
if (depth === 1) {
|
|
35033
|
-
if (!
|
|
35062
|
+
if (!opens2) parts.push(" ");
|
|
35034
35063
|
parts.push(templateChunkValue(t));
|
|
35035
35064
|
last = t;
|
|
35036
35065
|
endIndex = j;
|
|
@@ -35351,22 +35380,27 @@ var BUILTINS = [
|
|
|
35351
35380
|
// Messaging
|
|
35352
35381
|
{
|
|
35353
35382
|
name: "error",
|
|
35354
|
-
signature: "error(message)",
|
|
35383
|
+
signature: "error(message, values?)",
|
|
35355
35384
|
returns: "never",
|
|
35356
|
-
doc: "Abort the action and show an error. Rolls back all changes."
|
|
35385
|
+
doc: "Abort the action and show an error. Rolls back all changes. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`."
|
|
35386
|
+
},
|
|
35387
|
+
{
|
|
35388
|
+
name: "warn",
|
|
35389
|
+
signature: "warn(message, values?, opts?)",
|
|
35390
|
+
returns: "void",
|
|
35391
|
+
doc: "Show a warning and continue. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`."
|
|
35357
35392
|
},
|
|
35358
|
-
{ name: "warn", signature: "warn(message)", returns: "void", doc: "Show a warning and continue." },
|
|
35359
35393
|
{
|
|
35360
35394
|
name: "info",
|
|
35361
|
-
signature: "info(message)",
|
|
35395
|
+
signature: "info(message, values?, opts?)",
|
|
35362
35396
|
returns: "void",
|
|
35363
|
-
doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value."
|
|
35397
|
+
doc: "Show an informational message \u2014 a receipt for work the action did, not a way to publish a computed value. The text is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`. `opts.links` adds record links."
|
|
35364
35398
|
},
|
|
35365
35399
|
{
|
|
35366
35400
|
name: "exit",
|
|
35367
|
-
signature: "exit(message?, level?)",
|
|
35401
|
+
signature: "exit(message?, level?, values?)",
|
|
35368
35402
|
returns: "never",
|
|
35369
|
-
doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default) or `'
|
|
35403
|
+
doc: "Stop the script early **without** error, keeping changes made so far. Level `'info'` (default), `'warning'`, `'danger'` or `'success'`. The message is looked up in the module's `messages` translations; `{name}` placeholders are filled from `values`. This is the DSL's early return \u2014 the block compiles to a bare script, so a `return` outside a function is a syntax error wherever it sits."
|
|
35370
35404
|
},
|
|
35371
35405
|
{
|
|
35372
35406
|
name: "notify",
|
|
@@ -35473,6 +35507,170 @@ var BUILTINS = [
|
|
|
35473
35507
|
}
|
|
35474
35508
|
];
|
|
35475
35509
|
var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
|
|
35510
|
+
var VALUES_INDEX = /* @__PURE__ */ new Map([
|
|
35511
|
+
["info", 1],
|
|
35512
|
+
["warn", 1],
|
|
35513
|
+
["error", 1],
|
|
35514
|
+
["exit", 2]
|
|
35515
|
+
]);
|
|
35516
|
+
function extractMessages(doc) {
|
|
35517
|
+
const out = [];
|
|
35518
|
+
const { tokens } = doc;
|
|
35519
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
35520
|
+
const t = tokens[i];
|
|
35521
|
+
const valuesIndex = VALUES_INDEX.get(t.text);
|
|
35522
|
+
if (t.kind !== "ident" || valuesIndex === void 0 || tokens[i + 1].text !== "(") continue;
|
|
35523
|
+
if (tokens[i - 1]?.text === ".") continue;
|
|
35524
|
+
const block = doc.blocks.find((b) => t.start >= b.body.start && t.start < b.body.end)?.kind;
|
|
35525
|
+
if (block !== "execute" && block !== "onBeforeStart") continue;
|
|
35526
|
+
const args = splitArgs(tokens, i + 1);
|
|
35527
|
+
if (!args[0]?.length) continue;
|
|
35528
|
+
const [kind, text] = classify(args[0]);
|
|
35529
|
+
const valuesArg = args[valuesIndex];
|
|
35530
|
+
out.push({
|
|
35531
|
+
builtin: t.text,
|
|
35532
|
+
kind,
|
|
35533
|
+
text,
|
|
35534
|
+
valueNames: valuesArg ? valueNames(valuesArg) : [],
|
|
35535
|
+
span: { start: args[0][0].start, end: args[0][args[0].length - 1].end }
|
|
35536
|
+
});
|
|
35537
|
+
}
|
|
35538
|
+
return out;
|
|
35539
|
+
}
|
|
35540
|
+
function splitArgs(tokens, open) {
|
|
35541
|
+
const args = [];
|
|
35542
|
+
let current = [];
|
|
35543
|
+
let depth = 0;
|
|
35544
|
+
for (let j = open + 1; j < tokens.length; j++) {
|
|
35545
|
+
const t = tokens[j];
|
|
35546
|
+
if (t.kind === "comment") continue;
|
|
35547
|
+
if (depth === 0 && t.kind === "punct" && closes(t)) break;
|
|
35548
|
+
if (depth === 0 && t.kind === "punct" && t.text === ",") {
|
|
35549
|
+
args.push(current);
|
|
35550
|
+
current = [];
|
|
35551
|
+
continue;
|
|
35552
|
+
}
|
|
35553
|
+
depth += depthDelta(t);
|
|
35554
|
+
current.push(t);
|
|
35555
|
+
}
|
|
35556
|
+
if (current.length > 0) args.push(current);
|
|
35557
|
+
return args;
|
|
35558
|
+
}
|
|
35559
|
+
var isWholeTemplate = (t) => t.kind === "template" && t.text.length > 1 && t.text.startsWith("`") && t.text.endsWith("`");
|
|
35560
|
+
function classify(arg) {
|
|
35561
|
+
const t = unwrap(arg);
|
|
35562
|
+
if (t.length === 1 && t[0].kind === "string") return ["literal", stringValue(t[0])];
|
|
35563
|
+
if (t.length === 1 && isWholeTemplate(t[0])) return ["literal", templateChunkValue(t[0])];
|
|
35564
|
+
return [joinsText(t) ? "concatenated" : "dynamic"];
|
|
35565
|
+
}
|
|
35566
|
+
function joinsText(tokens) {
|
|
35567
|
+
const t = unwrap(tokens);
|
|
35568
|
+
if (t.length === 1 && t[0].kind === "string" || isTemplate(t)) return true;
|
|
35569
|
+
let depth = 0;
|
|
35570
|
+
let root = -1;
|
|
35571
|
+
for (let j = 0; j < t.length; j++) {
|
|
35572
|
+
const tok = t[j];
|
|
35573
|
+
if (depth === 0) {
|
|
35574
|
+
if (isLooserThanPlus(t, j)) return false;
|
|
35575
|
+
if (tok.kind === "punct" && (tok.text === "+" || tok.text === "-") && endsOperand(t[j - 1])) root = j;
|
|
35576
|
+
}
|
|
35577
|
+
depth += depthDelta(tok);
|
|
35578
|
+
}
|
|
35579
|
+
if (root < 0 || t[root].text === "-") return false;
|
|
35580
|
+
return joinsText(t.slice(0, root)) || joinsText(t.slice(root + 1));
|
|
35581
|
+
}
|
|
35582
|
+
var LOOSER_THAN_PLUS = /* @__PURE__ */ new Set([
|
|
35583
|
+
"?",
|
|
35584
|
+
":",
|
|
35585
|
+
"&&",
|
|
35586
|
+
"||",
|
|
35587
|
+
"=",
|
|
35588
|
+
"==",
|
|
35589
|
+
"!=",
|
|
35590
|
+
"===",
|
|
35591
|
+
"!==",
|
|
35592
|
+
"<",
|
|
35593
|
+
">",
|
|
35594
|
+
"<=",
|
|
35595
|
+
">=",
|
|
35596
|
+
"&",
|
|
35597
|
+
"|",
|
|
35598
|
+
"^",
|
|
35599
|
+
",",
|
|
35600
|
+
"=>",
|
|
35601
|
+
"+=",
|
|
35602
|
+
"-=",
|
|
35603
|
+
"*=",
|
|
35604
|
+
"/=",
|
|
35605
|
+
"%="
|
|
35606
|
+
]);
|
|
35607
|
+
function isLooserThanPlus(t, j) {
|
|
35608
|
+
const tok = t[j];
|
|
35609
|
+
if (tok.kind === "ident") return tok.text === "in" || tok.text === "instanceof";
|
|
35610
|
+
if (tok.kind !== "punct" || !LOOSER_THAN_PLUS.has(tok.text)) return false;
|
|
35611
|
+
return !(tok.text === "?" && t[j + 1]?.text === ".");
|
|
35612
|
+
}
|
|
35613
|
+
function endsOperand(prev) {
|
|
35614
|
+
if (!prev) return false;
|
|
35615
|
+
switch (prev.kind) {
|
|
35616
|
+
case "number":
|
|
35617
|
+
case "string":
|
|
35618
|
+
case "regex":
|
|
35619
|
+
return true;
|
|
35620
|
+
case "ident":
|
|
35621
|
+
return !["return", "typeof", "void", "delete", "new", "in", "instanceof", "case"].includes(prev.text);
|
|
35622
|
+
case "template":
|
|
35623
|
+
return prev.text.endsWith("`");
|
|
35624
|
+
case "punct":
|
|
35625
|
+
return [")", "]", "}", "++", "--"].includes(prev.text);
|
|
35626
|
+
default:
|
|
35627
|
+
return false;
|
|
35628
|
+
}
|
|
35629
|
+
}
|
|
35630
|
+
function isTemplate(tokens) {
|
|
35631
|
+
if (tokens[0]?.kind !== "template" || !tokens[0].text.startsWith("`")) return false;
|
|
35632
|
+
let depth = 0;
|
|
35633
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
35634
|
+
depth += depthDelta(tokens[j]);
|
|
35635
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
35636
|
+
}
|
|
35637
|
+
return false;
|
|
35638
|
+
}
|
|
35639
|
+
var opens = (t) => t.kind === "punct" ? "([{".includes(t.text) : t.kind === "template" && t.text.endsWith("${");
|
|
35640
|
+
var closes = (t) => t.kind === "punct" ? ")]}".includes(t.text) : t.kind === "template" && t.text.startsWith("}");
|
|
35641
|
+
var depthDelta = (t) => Number(opens(t)) - Number(closes(t));
|
|
35642
|
+
function unwrap(tokens) {
|
|
35643
|
+
while (tokens[0]?.text === "(" && isGroup(tokens)) tokens = tokens.slice(1, -1);
|
|
35644
|
+
return tokens;
|
|
35645
|
+
}
|
|
35646
|
+
function isGroup(tokens) {
|
|
35647
|
+
if (tokens.length < 2 || !opens(tokens[0])) return false;
|
|
35648
|
+
let depth = 0;
|
|
35649
|
+
for (let j = 0; j < tokens.length; j++) {
|
|
35650
|
+
depth += depthDelta(tokens[j]);
|
|
35651
|
+
if (depth === 0) return j === tokens.length - 1;
|
|
35652
|
+
}
|
|
35653
|
+
return false;
|
|
35654
|
+
}
|
|
35655
|
+
function valueNames(arg) {
|
|
35656
|
+
const t = unwrap(arg);
|
|
35657
|
+
if (t[0]?.text !== "{" || !isGroup(t)) return null;
|
|
35658
|
+
const names = [];
|
|
35659
|
+
let depth = 0;
|
|
35660
|
+
for (let j = 0; j < t.length; j++) {
|
|
35661
|
+
const tok = t[j];
|
|
35662
|
+
const at = depth;
|
|
35663
|
+
depth += depthDelta(tok);
|
|
35664
|
+
if (at !== 1 || tok.kind !== "ident" && tok.kind !== "string") continue;
|
|
35665
|
+
const prev = t[j - 1].text;
|
|
35666
|
+
const next = t[j + 1]?.text;
|
|
35667
|
+
if ((prev === "{" || prev === ",") && (next === ":" || next === "," || next === "}")) {
|
|
35668
|
+
const name = tok.kind === "string" ? stringValue(tok) : tok.text;
|
|
35669
|
+
if (name !== "links") names.push(name);
|
|
35670
|
+
}
|
|
35671
|
+
}
|
|
35672
|
+
return names;
|
|
35673
|
+
}
|
|
35476
35674
|
function checkDsl(text, ctx = {}) {
|
|
35477
35675
|
const parsed = parseDsl(text);
|
|
35478
35676
|
const out = [];
|
|
@@ -35812,8 +36010,38 @@ function checkDsl(text, ctx = {}) {
|
|
|
35812
36010
|
}
|
|
35813
36011
|
}
|
|
35814
36012
|
out.push(...inlineAssignmentIssues(parsed, positionAt));
|
|
36013
|
+
const locales = Object.entries(ctx.messageTranslations ?? {});
|
|
36014
|
+
for (const m of extractMessages(parsed)) {
|
|
36015
|
+
if (m.kind === "concatenated" && locales.length > 0) {
|
|
36016
|
+
add(
|
|
36017
|
+
m.span,
|
|
36018
|
+
`This ${m.builtin}() message joins text and values, so it cannot be translated. Put the values in {placeholders}: ${m.builtin}('Only {n} left', ${m.builtin === "exit" ? "'warning', " : ""}{ n: qty }).`,
|
|
36019
|
+
"warning",
|
|
36020
|
+
"dsl/message-concatenated"
|
|
36021
|
+
);
|
|
36022
|
+
}
|
|
36023
|
+
if (m.text === void 0) continue;
|
|
36024
|
+
if (m.valueNames && m.valueNames.length > 0) {
|
|
36025
|
+
for (const name of placeholders(m.text)) {
|
|
36026
|
+
if (!m.valueNames.includes(name))
|
|
36027
|
+
add(m.span, `{${name}} has no value in this call, so it shows as written.`, "warning", "dsl/message-placeholder");
|
|
36028
|
+
}
|
|
36029
|
+
}
|
|
36030
|
+
const missing = locales.filter(([, messages]) => !(Object.hasOwn(messages, m.text) && messages[m.text]?.trim())).map(([locale]) => locale);
|
|
36031
|
+
if (missing.length > 0) {
|
|
36032
|
+
add(
|
|
36033
|
+
m.span,
|
|
36034
|
+
`No ${missing.join(", ")} translation for this message \u2014 add it to "messages" in translations/<locale>.json, keyed by the exact text.`,
|
|
36035
|
+
"warning",
|
|
36036
|
+
"dsl/message-untranslated"
|
|
36037
|
+
);
|
|
36038
|
+
}
|
|
36039
|
+
}
|
|
35815
36040
|
return out;
|
|
35816
36041
|
}
|
|
36042
|
+
function placeholders(text) {
|
|
36043
|
+
return [...new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((x) => x[1]))];
|
|
36044
|
+
}
|
|
35817
36045
|
function columnMatcher(columns) {
|
|
35818
36046
|
if (!columns) return () => true;
|
|
35819
36047
|
if (typeof columns.keys !== "function")
|
|
@@ -36005,6 +36233,190 @@ function lineIndexer(text) {
|
|
|
36005
36233
|
};
|
|
36006
36234
|
}
|
|
36007
36235
|
|
|
36236
|
+
// src/tools/validate/translations.ts
|
|
36237
|
+
function resolveTranslationFile(translationsDir, locale) {
|
|
36238
|
+
const exact = path9.join(translationsDir, `${locale}.json`);
|
|
36239
|
+
if (fs7.existsSync(exact)) return exact;
|
|
36240
|
+
if (!fs7.existsSync(translationsDir)) return void 0;
|
|
36241
|
+
const want = `${locale}.json`.toLowerCase();
|
|
36242
|
+
for (const f of fs7.readdirSync(translationsDir)) {
|
|
36243
|
+
if (f.toLowerCase() === want) return path9.join(translationsDir, f);
|
|
36244
|
+
}
|
|
36245
|
+
return void 0;
|
|
36246
|
+
}
|
|
36247
|
+
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
36248
|
+
if (!root || typeof root !== "object") return false;
|
|
36249
|
+
const entities = root.entities;
|
|
36250
|
+
if (!entities || typeof entities !== "object") return false;
|
|
36251
|
+
const entity = entities[entityCd];
|
|
36252
|
+
if (!entity || typeof entity !== "object") return false;
|
|
36253
|
+
const constraints = entity.constraints;
|
|
36254
|
+
if (!constraints || typeof constraints !== "object") return false;
|
|
36255
|
+
const ck = constraints[constraintCd];
|
|
36256
|
+
if (!ck || typeof ck !== "object") return false;
|
|
36257
|
+
const msg = ck.message;
|
|
36258
|
+
return typeof msg === "string" && msg.trim() !== "";
|
|
36259
|
+
}
|
|
36260
|
+
function checkConstraintTranslations(ctx) {
|
|
36261
|
+
const { paths, warn, entities, supportedLocales } = ctx;
|
|
36262
|
+
if (supportedLocales.length > 0) {
|
|
36263
|
+
const declared = [];
|
|
36264
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
36265
|
+
if (typeof e.extends === "string" && e.extends) continue;
|
|
36266
|
+
const constraints = e.constraints;
|
|
36267
|
+
if (!constraints || typeof constraints !== "object") continue;
|
|
36268
|
+
for (const [cname, c] of Object.entries(constraints)) {
|
|
36269
|
+
if (!c || typeof c !== "object") continue;
|
|
36270
|
+
const msg = c.message;
|
|
36271
|
+
if (typeof msg === "string" && msg.trim() !== "") {
|
|
36272
|
+
declared.push({ entity: name, constraint: cname, message: msg });
|
|
36273
|
+
}
|
|
36274
|
+
}
|
|
36275
|
+
}
|
|
36276
|
+
if (declared.length > 0) {
|
|
36277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36278
|
+
for (const raw of supportedLocales) {
|
|
36279
|
+
const locale = raw.trim();
|
|
36280
|
+
if (!locale) continue;
|
|
36281
|
+
const lc = locale.toLowerCase();
|
|
36282
|
+
if (lc === "en" || lc.startsWith("en-")) continue;
|
|
36283
|
+
if (seen.has(lc)) continue;
|
|
36284
|
+
seen.add(lc);
|
|
36285
|
+
let tx = null;
|
|
36286
|
+
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
36287
|
+
if (abs) {
|
|
36288
|
+
try {
|
|
36289
|
+
tx = JSON.parse(fs7.readFileSync(abs, "utf8"));
|
|
36290
|
+
} catch {
|
|
36291
|
+
tx = null;
|
|
36292
|
+
}
|
|
36293
|
+
}
|
|
36294
|
+
for (const d of declared) {
|
|
36295
|
+
if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
|
|
36296
|
+
warn(
|
|
36297
|
+
`translations/${locale}.json`,
|
|
36298
|
+
`constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
|
|
36299
|
+
);
|
|
36300
|
+
}
|
|
36301
|
+
}
|
|
36302
|
+
}
|
|
36303
|
+
}
|
|
36304
|
+
}
|
|
36305
|
+
}
|
|
36306
|
+
function checkTranslationCompleteness(ctx) {
|
|
36307
|
+
const { paths, err, warn, roles, supportedLocales } = ctx;
|
|
36308
|
+
const roleCodes = Object.keys(roles);
|
|
36309
|
+
const localeFiles = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
|
|
36310
|
+
for (const raw of supportedLocales) {
|
|
36311
|
+
const locale = raw.trim();
|
|
36312
|
+
if (!locale) continue;
|
|
36313
|
+
if (!resolveTranslationFile(paths.translationsDir, locale)) {
|
|
36314
|
+
err(
|
|
36315
|
+
"translations",
|
|
36316
|
+
`manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
|
|
36317
|
+
);
|
|
36318
|
+
}
|
|
36319
|
+
}
|
|
36320
|
+
if (roleCodes.length > 0 && localeFiles.length === 0) {
|
|
36321
|
+
warn(
|
|
36322
|
+
"translations",
|
|
36323
|
+
`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.`
|
|
36324
|
+
);
|
|
36325
|
+
}
|
|
36326
|
+
for (const file2 of localeFiles) {
|
|
36327
|
+
let tx;
|
|
36328
|
+
try {
|
|
36329
|
+
tx = JSON.parse(fs7.readFileSync(path9.join(paths.translationsDir, file2), "utf8"));
|
|
36330
|
+
} catch (ex) {
|
|
36331
|
+
err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
|
|
36332
|
+
continue;
|
|
36333
|
+
}
|
|
36334
|
+
const txRoles = tx.roles ?? {};
|
|
36335
|
+
const missing = roleCodes.filter((rc) => {
|
|
36336
|
+
const entry = txRoles[rc];
|
|
36337
|
+
return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
|
|
36338
|
+
});
|
|
36339
|
+
if (missing.length > 0) {
|
|
36340
|
+
err(
|
|
36341
|
+
`translations/${file2}`,
|
|
36342
|
+
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
36343
|
+
);
|
|
36344
|
+
}
|
|
36345
|
+
}
|
|
36346
|
+
}
|
|
36347
|
+
var isEnglishLocale = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
36348
|
+
function readLocaleFile(translationsDir, locale) {
|
|
36349
|
+
const abs = resolveTranslationFile(translationsDir, locale);
|
|
36350
|
+
if (!abs) return null;
|
|
36351
|
+
try {
|
|
36352
|
+
return JSON.parse(fs7.readFileSync(abs, "utf8"));
|
|
36353
|
+
} catch {
|
|
36354
|
+
return null;
|
|
36355
|
+
}
|
|
36356
|
+
}
|
|
36357
|
+
function messagesBlock(root) {
|
|
36358
|
+
const block = root?.messages;
|
|
36359
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return {};
|
|
36360
|
+
return Object.fromEntries(
|
|
36361
|
+
Object.entries(block).filter((e) => typeof e[1] === "string" && e[1].trim() !== "")
|
|
36362
|
+
);
|
|
36363
|
+
}
|
|
36364
|
+
function readMessageTranslations(translationsDir, supportedLocales) {
|
|
36365
|
+
const declared = Array.isArray(supportedLocales) ? supportedLocales.filter((l) => typeof l === "string") : [];
|
|
36366
|
+
const locales = [...new Set(declared.map((l) => l.trim()).filter((l) => l && !isEnglishLocale(l)))];
|
|
36367
|
+
if (locales.length === 0) return void 0;
|
|
36368
|
+
return Object.fromEntries(locales.map((l) => [l, messagesBlock(readLocaleFile(translationsDir, l))]));
|
|
36369
|
+
}
|
|
36370
|
+
function actionMessageTexts(logicDir, actions) {
|
|
36371
|
+
const texts = /* @__PURE__ */ new Map();
|
|
36372
|
+
for (const [code2, def] of Object.entries(actions)) {
|
|
36373
|
+
const declared = def?.script;
|
|
36374
|
+
const script = typeof declared === "string" && declared ? declared : code2;
|
|
36375
|
+
const file2 = path9.join(logicDir, "actions", `${script}.dsl`);
|
|
36376
|
+
if (!fs7.existsSync(file2)) continue;
|
|
36377
|
+
let body;
|
|
36378
|
+
try {
|
|
36379
|
+
body = fs7.readFileSync(file2, "utf8");
|
|
36380
|
+
} catch {
|
|
36381
|
+
continue;
|
|
36382
|
+
}
|
|
36383
|
+
for (const m of extractMessages(parseDsl(body))) {
|
|
36384
|
+
if (m.text !== void 0 && !texts.has(m.text)) texts.set(m.text, `logic/actions/${script}.dsl`);
|
|
36385
|
+
}
|
|
36386
|
+
}
|
|
36387
|
+
return texts;
|
|
36388
|
+
}
|
|
36389
|
+
var placeholdersOf = (text) => new Set([...text.matchAll(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)].map((m) => m[1]));
|
|
36390
|
+
function checkMessageTranslations(ctx) {
|
|
36391
|
+
const { paths, warn } = ctx;
|
|
36392
|
+
if (!fs7.existsSync(paths.translationsDir)) return;
|
|
36393
|
+
let texts;
|
|
36394
|
+
for (const f of fs7.readdirSync(paths.translationsDir).filter((f2) => f2.toLowerCase().endsWith(".json")).sort()) {
|
|
36395
|
+
const locale = f.slice(0, -".json".length);
|
|
36396
|
+
const block = messagesBlock(readLocaleFile(paths.translationsDir, locale));
|
|
36397
|
+
if (Object.keys(block).length === 0) continue;
|
|
36398
|
+
if (isEnglishLocale(locale)) {
|
|
36399
|
+
warn(`translations/${f}`, "has a `messages` block, which install ignores: English is the text in the .dsl scripts.");
|
|
36400
|
+
continue;
|
|
36401
|
+
}
|
|
36402
|
+
texts ??= actionMessageTexts(paths.logicDir, ctx.actions);
|
|
36403
|
+
for (const [text, translation] of Object.entries(block)) {
|
|
36404
|
+
if (!texts.has(text)) {
|
|
36405
|
+
warn(
|
|
36406
|
+
`translations/${f}`,
|
|
36407
|
+
`messages entry "${text}" matches no info/warn/error/exit text in the action scripts \u2014 it never shows. The key must be the exact English text; an edit to the .dsl wording leaves the old entry behind.`
|
|
36408
|
+
);
|
|
36409
|
+
continue;
|
|
36410
|
+
}
|
|
36411
|
+
const known = placeholdersOf(text);
|
|
36412
|
+
for (const ph of placeholdersOf(translation)) {
|
|
36413
|
+
if (!known.has(ph))
|
|
36414
|
+
warn(`translations/${f}`, `messages entry "${text}": the translation uses {${ph}}, which the English text has no value for \u2014 it shows as written.`);
|
|
36415
|
+
}
|
|
36416
|
+
}
|
|
36417
|
+
}
|
|
36418
|
+
}
|
|
36419
|
+
|
|
36008
36420
|
// src/tools/dsl-check.ts
|
|
36009
36421
|
function checkDsl2(src, opts = {}) {
|
|
36010
36422
|
const raw = opts.executionMode?.trim();
|
|
@@ -36013,7 +36425,8 @@ function checkDsl2(src, opts = {}) {
|
|
|
36013
36425
|
const ctx = {
|
|
36014
36426
|
moduleCode: opts.moduleCode,
|
|
36015
36427
|
action: executionMode || opts.viaJob || opts.actionCode ? { code: opts.actionCode ?? "this action", executionMode, viaJob: opts.viaJob } : void 0,
|
|
36016
|
-
currentEntity: opts.entity ?? null
|
|
36428
|
+
currentEntity: opts.entity ?? null,
|
|
36429
|
+
messageTranslations: opts.messageTranslations
|
|
36017
36430
|
};
|
|
36018
36431
|
const issues = checkDsl(src, ctx).map(toIssue);
|
|
36019
36432
|
if (raw && !executionMode) {
|
|
@@ -36066,7 +36479,8 @@ function actionAdd(args) {
|
|
|
36066
36479
|
executionMode: args.executionMode,
|
|
36067
36480
|
moduleCode: manifest.code,
|
|
36068
36481
|
actionCode: args.code,
|
|
36069
|
-
entity: entityRecordContext(paths, manifest, args.entityCode, localTraits)
|
|
36482
|
+
entity: entityRecordContext(paths, manifest, args.entityCode, localTraits),
|
|
36483
|
+
messageTranslations: readMessageTranslations(paths.translationsDir, manifest.supportedLocales)
|
|
36070
36484
|
});
|
|
36071
36485
|
const dslErrors = dslIssues.filter((i) => i.level === "error");
|
|
36072
36486
|
if (dslErrors.length > 0) {
|
|
@@ -36094,7 +36508,7 @@ function actionAdd(args) {
|
|
|
36094
36508
|
actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
|
|
36095
36509
|
}
|
|
36096
36510
|
actionsJson[args.code] = actionEntry;
|
|
36097
|
-
const dslPath =
|
|
36511
|
+
const dslPath = path10.join(paths.logicDir, "actions", `${args.code}.dsl`);
|
|
36098
36512
|
const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
|
|
36099
36513
|
return makeResult(
|
|
36100
36514
|
`Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
|
|
@@ -36109,8 +36523,8 @@ ${dslWarnings.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.messag
|
|
|
36109
36523
|
}
|
|
36110
36524
|
|
|
36111
36525
|
// src/tools/action-check.ts
|
|
36112
|
-
var
|
|
36113
|
-
var
|
|
36526
|
+
var fs8 = __toESM(require("fs"));
|
|
36527
|
+
var path11 = __toESM(require("path"));
|
|
36114
36528
|
var actionCheckSchema = {
|
|
36115
36529
|
moduleDir: external_exports.string().optional().describe(
|
|
36116
36530
|
"Module root. Required when checking an on-disk action (actionCode); optional when checking a raw dslBody."
|
|
@@ -36137,6 +36551,7 @@ function actionCheck(args) {
|
|
|
36137
36551
|
let entityCode = args.entityCode;
|
|
36138
36552
|
let moduleCode;
|
|
36139
36553
|
let entity;
|
|
36554
|
+
let messageTranslations;
|
|
36140
36555
|
const preIssues = [];
|
|
36141
36556
|
if (args.actionCode && !args.moduleDir) {
|
|
36142
36557
|
throw new Error("moduleDir is required when checking an on-disk action (actionCode).");
|
|
@@ -36144,6 +36559,7 @@ function actionCheck(args) {
|
|
|
36144
36559
|
if (args.moduleDir) {
|
|
36145
36560
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36146
36561
|
moduleCode = manifest.code;
|
|
36562
|
+
messageTranslations = readMessageTranslations(paths.translationsDir, manifest.supportedLocales);
|
|
36147
36563
|
if (args.actionCode) {
|
|
36148
36564
|
const actions = readJsonOrDefault(paths.actions, {});
|
|
36149
36565
|
const entry = actions[args.actionCode];
|
|
@@ -36153,13 +36569,13 @@ function actionCheck(args) {
|
|
|
36153
36569
|
);
|
|
36154
36570
|
}
|
|
36155
36571
|
const script = entry.script ?? args.actionCode;
|
|
36156
|
-
const dslPath =
|
|
36157
|
-
if (!
|
|
36572
|
+
const dslPath = path11.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
36573
|
+
if (!fs8.existsSync(dslPath)) {
|
|
36158
36574
|
throw new Error(
|
|
36159
36575
|
`Action '${args.actionCode}' declares script '${script}' but logic/actions/${script}.dsl does not exist.`
|
|
36160
36576
|
);
|
|
36161
36577
|
}
|
|
36162
|
-
body =
|
|
36578
|
+
body = fs8.readFileSync(dslPath, "utf8");
|
|
36163
36579
|
mode = mode ?? (entry.executionMode ?? entry.mode);
|
|
36164
36580
|
entityCode = entityCode ?? entry.entityCode;
|
|
36165
36581
|
label = `logic/actions/${script}.dsl`;
|
|
@@ -36188,7 +36604,8 @@ function actionCheck(args) {
|
|
|
36188
36604
|
viaJob,
|
|
36189
36605
|
moduleCode,
|
|
36190
36606
|
actionCode: args.actionCode,
|
|
36191
|
-
entity
|
|
36607
|
+
entity,
|
|
36608
|
+
messageTranslations
|
|
36192
36609
|
})
|
|
36193
36610
|
];
|
|
36194
36611
|
const errors = issues.filter((i) => i.level === "error");
|
|
@@ -36379,8 +36796,8 @@ function menuAdd(args) {
|
|
|
36379
36796
|
}
|
|
36380
36797
|
|
|
36381
36798
|
// src/tools/translations.ts
|
|
36382
|
-
var
|
|
36383
|
-
var
|
|
36799
|
+
var fs9 = __toESM(require("fs"));
|
|
36800
|
+
var path12 = __toESM(require("path"));
|
|
36384
36801
|
function titleize(code2) {
|
|
36385
36802
|
return code2.split(/[_-]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
36386
36803
|
}
|
|
@@ -36407,21 +36824,21 @@ function buildSkeleton(moduleDir) {
|
|
|
36407
36824
|
`traits.json \u2014 ${traitsError} Its traits can't be overlaid, so the entities that use one would be skipped as declaring an unknown trait and their columns would go unlabelled \u2014 which fails the install. Fix the file and sync again.`
|
|
36408
36825
|
);
|
|
36409
36826
|
}
|
|
36410
|
-
const push = (section, pathParts, english, required2 = false) => {
|
|
36411
|
-
entries.push({ pathParts, english, required: required2 });
|
|
36827
|
+
const push = (section, pathParts, english, required2 = false, legacyParts, nonEnglishOnly = false) => {
|
|
36828
|
+
entries.push({ pathParts, english, required: required2, legacyParts, nonEnglishOnly });
|
|
36412
36829
|
counts[section] = (counts[section] ?? 0) + 1;
|
|
36413
36830
|
};
|
|
36414
36831
|
for (const [name, relPath] of Object.entries(manifest.entities ?? {})) {
|
|
36415
36832
|
if (name.includes(".")) continue;
|
|
36416
|
-
const abs =
|
|
36417
|
-
if (!
|
|
36833
|
+
const abs = path12.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
36834
|
+
if (!fs9.existsSync(abs)) {
|
|
36418
36835
|
throw new Error(
|
|
36419
36836
|
`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.`
|
|
36420
36837
|
);
|
|
36421
36838
|
}
|
|
36422
36839
|
let e;
|
|
36423
36840
|
try {
|
|
36424
|
-
e = JSON.parse(
|
|
36841
|
+
e = JSON.parse(fs9.readFileSync(abs, "utf8"));
|
|
36425
36842
|
} catch (ex) {
|
|
36426
36843
|
throw new Error(`${relPath} is not valid JSON: ${ex.message}`);
|
|
36427
36844
|
}
|
|
@@ -36475,17 +36892,25 @@ function buildSkeleton(moduleDir) {
|
|
|
36475
36892
|
if (dupes.size > 0) {
|
|
36476
36893
|
const detail = [...dupes].map(([code2, paths_]) => `'${code2}' (${paths_.join(", ")})`).join("; ");
|
|
36477
36894
|
throw new Error(
|
|
36478
|
-
`ui/folders.json reuses folder code(s) across branches: ${detail}.
|
|
36895
|
+
`ui/folders.json reuses folder code(s) across branches: ${detail}. Role rights (\`folder:<code>\`) can't tell them apart. Rename them to unique codes, then re-run.`
|
|
36479
36896
|
);
|
|
36480
36897
|
}
|
|
36481
36898
|
push("folders", ["folders", manifest.code, "label"], str(folders, "label") ?? titleize(manifest.code));
|
|
36482
36899
|
for (const f of walkFolders(folders)) {
|
|
36483
|
-
|
|
36900
|
+
const flatCode = [manifest.code, ...f.path.split("/")].join("_");
|
|
36901
|
+
push("folders", ["folders", flatCode, "label"], str(f.node, "label") ?? titleize(f.code), false, [
|
|
36902
|
+
"folders",
|
|
36903
|
+
f.code,
|
|
36904
|
+
"label"
|
|
36905
|
+
]);
|
|
36484
36906
|
}
|
|
36485
36907
|
}
|
|
36486
36908
|
const settings = readJsonOrDefault(paths.settings, {});
|
|
36487
36909
|
for (const [code2, s] of Object.entries(settings)) {
|
|
36488
|
-
push("settings", ["settings", code2, "label"], str(s, "label") ?? titleize(code2));
|
|
36910
|
+
push("settings", ["settings", code2, "label"], str(s, "label") ?? str(s, "description") ?? str(s, "desc") ?? titleize(code2));
|
|
36911
|
+
}
|
|
36912
|
+
for (const text of actionMessageTexts(paths.logicDir, actions).keys()) {
|
|
36913
|
+
push("messages", ["messages", text], text, false, void 0, true);
|
|
36489
36914
|
}
|
|
36490
36915
|
return { entries, counts };
|
|
36491
36916
|
}
|
|
@@ -36525,16 +36950,17 @@ function translationSync(args) {
|
|
|
36525
36950
|
);
|
|
36526
36951
|
}
|
|
36527
36952
|
const keep = new Set(entries.map((e) => compositeKey(...e.pathParts)));
|
|
36953
|
+
const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
36528
36954
|
const files = {};
|
|
36529
36955
|
const report = [];
|
|
36530
36956
|
let totalAdded = 0;
|
|
36531
36957
|
let missingRequired = 0;
|
|
36532
36958
|
for (const locale of locales) {
|
|
36533
|
-
const abs =
|
|
36959
|
+
const abs = path12.join(paths.translationsDir, `${locale}.json`);
|
|
36534
36960
|
let existing = {};
|
|
36535
|
-
if (
|
|
36961
|
+
if (fs9.existsSync(abs)) {
|
|
36536
36962
|
try {
|
|
36537
|
-
existing = JSON.parse(
|
|
36963
|
+
existing = JSON.parse(fs9.readFileSync(abs, "utf8"));
|
|
36538
36964
|
} catch (e) {
|
|
36539
36965
|
throw new Error(`translations/${locale}.json is not valid JSON: ${e.message}`);
|
|
36540
36966
|
}
|
|
@@ -36542,9 +36968,11 @@ function translationSync(args) {
|
|
|
36542
36968
|
const next = args.prune ? pruneTo(existing, keep) : JSON.parse(JSON.stringify(existing));
|
|
36543
36969
|
let added = 0;
|
|
36544
36970
|
for (const entry of entries) {
|
|
36971
|
+
if (entry.nonEnglishOnly && isEnglish(locale)) continue;
|
|
36545
36972
|
const current = getIn(next, entry.pathParts);
|
|
36546
36973
|
if (typeof current === "string" && current.trim() !== "") continue;
|
|
36547
|
-
|
|
36974
|
+
const legacy = entry.legacyParts && getIn(existing, entry.legacyParts);
|
|
36975
|
+
setIn(next, entry.pathParts, typeof legacy === "string" && legacy.trim() !== "" ? legacy : entry.english);
|
|
36548
36976
|
added++;
|
|
36549
36977
|
if (entry.required) missingRequired++;
|
|
36550
36978
|
}
|
|
@@ -36552,7 +36980,6 @@ function translationSync(args) {
|
|
|
36552
36980
|
totalAdded += added;
|
|
36553
36981
|
report.push(`${locale}: +${added} key(s)${added === 0 ? " (already complete)" : ""}`);
|
|
36554
36982
|
}
|
|
36555
|
-
const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
36556
36983
|
const nonEnglish = locales.filter((l) => !isEnglish(l));
|
|
36557
36984
|
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;
|
|
36558
36985
|
const sections = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
@@ -36564,8 +36991,8 @@ function translationSync(args) {
|
|
|
36564
36991
|
}
|
|
36565
36992
|
|
|
36566
36993
|
// src/tools/seed.ts
|
|
36567
|
-
var
|
|
36568
|
-
var
|
|
36994
|
+
var fs10 = __toESM(require("fs"));
|
|
36995
|
+
var path13 = __toESM(require("path"));
|
|
36569
36996
|
var SYSTEM_USER = 0;
|
|
36570
36997
|
var seedAddSchema = {
|
|
36571
36998
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
@@ -36580,8 +37007,8 @@ var seedAddSchema = {
|
|
|
36580
37007
|
};
|
|
36581
37008
|
function seedAdd(args) {
|
|
36582
37009
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36583
|
-
const entityPath =
|
|
36584
|
-
if (!
|
|
37010
|
+
const entityPath = path13.join(paths.entitiesDir, `${args.entity}.json`);
|
|
37011
|
+
if (!fs10.existsSync(entityPath)) {
|
|
36585
37012
|
throw new Error(
|
|
36586
37013
|
`Entity '${args.entity}' not found at entities/${args.entity}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
|
|
36587
37014
|
);
|
|
@@ -36659,11 +37086,11 @@ function seedAdd(args) {
|
|
|
36659
37086
|
);
|
|
36660
37087
|
}
|
|
36661
37088
|
}
|
|
36662
|
-
const seedFiles =
|
|
37089
|
+
const seedFiles = fs10.existsSync(paths.seedDataDir) ? fs10.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
36663
37090
|
const seededPks = {};
|
|
36664
37091
|
for (const f of seedFiles) {
|
|
36665
37092
|
try {
|
|
36666
|
-
const data = JSON.parse(
|
|
37093
|
+
const data = JSON.parse(fs10.readFileSync(path13.join(paths.seedDataDir, f), "utf8"));
|
|
36667
37094
|
const ec = data?.entityCode;
|
|
36668
37095
|
if (typeof ec !== "string" || !Array.isArray(data.records)) continue;
|
|
36669
37096
|
const set2 = seededPks[ec] ?? (seededPks[ec] = /* @__PURE__ */ new Set());
|
|
@@ -36706,7 +37133,7 @@ function seedAdd(args) {
|
|
|
36706
37133
|
}
|
|
36707
37134
|
}
|
|
36708
37135
|
const fileName = args.fileName ?? `${String(order).padStart(2, "0")}-${args.entity}.json`;
|
|
36709
|
-
const seedPath =
|
|
37136
|
+
const seedPath = path13.join(paths.seedDataDir, fileName);
|
|
36710
37137
|
const payload = { entityCode: args.entity, records: args.records };
|
|
36711
37138
|
return makeResult(
|
|
36712
37139
|
`Seeded ${args.records.length} '${args.entity}' record(s) \u2192 seed-data/${fileName} (load order ${order}).`,
|
|
@@ -36720,8 +37147,8 @@ ${warnings.map((w) => ` \u2022 ${w}`).join("\n")}` : void 0
|
|
|
36720
37147
|
}
|
|
36721
37148
|
|
|
36722
37149
|
// src/tools/adds.ts
|
|
36723
|
-
var
|
|
36724
|
-
var
|
|
37150
|
+
var fs11 = __toESM(require("fs"));
|
|
37151
|
+
var path14 = __toESM(require("path"));
|
|
36725
37152
|
var reportAddSchema = {
|
|
36726
37153
|
moduleDir: external_exports.string(),
|
|
36727
37154
|
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Report code (keys ui/reports.json)."),
|
|
@@ -36908,8 +37335,8 @@ function diagramAdd(args) {
|
|
|
36908
37335
|
if (problems.length > 0) {
|
|
36909
37336
|
throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
|
|
36910
37337
|
}
|
|
36911
|
-
const file2 =
|
|
36912
|
-
const exists =
|
|
37338
|
+
const file2 = path14.join(paths.diagramsDir, `${args.code}.json`);
|
|
37339
|
+
const exists = fs11.existsSync(file2);
|
|
36913
37340
|
let diagram;
|
|
36914
37341
|
if (exists) {
|
|
36915
37342
|
const existing = readJson(file2);
|
|
@@ -36967,8 +37394,8 @@ function dependencyAdd(args) {
|
|
|
36967
37394
|
`Dependency on '${args.moduleCode}' already exists. Edit manifest.json directly to change the version.`
|
|
36968
37395
|
);
|
|
36969
37396
|
}
|
|
36970
|
-
const contractPath =
|
|
36971
|
-
if (
|
|
37397
|
+
const contractPath = path14.join(paths.root, "deps", `${args.moduleCode}.json`);
|
|
37398
|
+
if (fs11.existsSync(contractPath)) {
|
|
36972
37399
|
throw new Error(
|
|
36973
37400
|
`deps/${args.moduleCode}.json already exists but '${args.moduleCode}' is not a manifest dependency. Add the manifest entry by hand or delete the stale contract \u2014 this tool will not overwrite it.`
|
|
36974
37401
|
);
|
|
@@ -37061,8 +37488,8 @@ function roleRightSet(args) {
|
|
|
37061
37488
|
}
|
|
37062
37489
|
|
|
37063
37490
|
// src/tools/module-inspect.ts
|
|
37064
|
-
var
|
|
37065
|
-
var
|
|
37491
|
+
var fs12 = __toESM(require("fs"));
|
|
37492
|
+
var path15 = __toESM(require("path"));
|
|
37066
37493
|
var moduleInspectSchema = {
|
|
37067
37494
|
moduleDir: external_exports.string()
|
|
37068
37495
|
};
|
|
@@ -37070,7 +37497,7 @@ function moduleInspect(args) {
|
|
|
37070
37497
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
37071
37498
|
const entities = manifest.entities ?? {};
|
|
37072
37499
|
const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
|
|
37073
|
-
const abs =
|
|
37500
|
+
const abs = path15.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
37074
37501
|
const e = readJsonOrDefault(abs, {});
|
|
37075
37502
|
const fields = e.fields ?? {};
|
|
37076
37503
|
return {
|
|
@@ -37105,11 +37532,11 @@ function moduleInspect(args) {
|
|
|
37105
37532
|
const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
|
|
37106
37533
|
let d = {};
|
|
37107
37534
|
try {
|
|
37108
|
-
d = JSON.parse(
|
|
37535
|
+
d = JSON.parse(fs12.readFileSync(fp, "utf8"));
|
|
37109
37536
|
} catch {
|
|
37110
37537
|
}
|
|
37111
37538
|
return {
|
|
37112
|
-
code:
|
|
37539
|
+
code: path15.basename(fp, ".json"),
|
|
37113
37540
|
label: typeof d?.label === "string" ? d.label : void 0,
|
|
37114
37541
|
entities: Object.keys(d?.entities ?? {})
|
|
37115
37542
|
};
|
|
@@ -37166,8 +37593,8 @@ function moduleInspect(args) {
|
|
|
37166
37593
|
readJsonOrDefault(paths.printTemplates, {})
|
|
37167
37594
|
);
|
|
37168
37595
|
const domains = Object.keys(readJsonOrDefault(paths.domains, {}));
|
|
37169
|
-
const seedFiles =
|
|
37170
|
-
const translations =
|
|
37596
|
+
const seedFiles = fs12.existsSync(paths.seedDataDir) ? fs12.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
37597
|
+
const translations = fs12.existsSync(paths.translationsDir) ? fs12.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
|
|
37171
37598
|
const summary = {
|
|
37172
37599
|
module: {
|
|
37173
37600
|
code: manifest.code,
|
|
@@ -37212,8 +37639,8 @@ function computeDepth(folder, current = 1) {
|
|
|
37212
37639
|
}
|
|
37213
37640
|
|
|
37214
37641
|
// src/tools/validate/context.ts
|
|
37215
|
-
var
|
|
37216
|
-
var
|
|
37642
|
+
var fs13 = __toESM(require("fs"));
|
|
37643
|
+
var path16 = __toESM(require("path"));
|
|
37217
37644
|
var SYSTEM_ENTITY_PK = {
|
|
37218
37645
|
user: "user_id",
|
|
37219
37646
|
document: "document_id",
|
|
@@ -37239,14 +37666,14 @@ function loadContext(moduleDir) {
|
|
|
37239
37666
|
const partialColumns = /* @__PURE__ */ new Set();
|
|
37240
37667
|
for (const [name, relPath] of Object.entries(entityMap)) {
|
|
37241
37668
|
if (name.includes(".")) continue;
|
|
37242
|
-
const abs =
|
|
37243
|
-
if (!
|
|
37669
|
+
const abs = path16.join(paths.root, relPath.replace(/^\.\//, ""));
|
|
37670
|
+
if (!fs13.existsSync(abs)) {
|
|
37244
37671
|
err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
|
|
37245
37672
|
continue;
|
|
37246
37673
|
}
|
|
37247
37674
|
let e;
|
|
37248
37675
|
try {
|
|
37249
|
-
e = JSON.parse(
|
|
37676
|
+
e = JSON.parse(fs13.readFileSync(abs, "utf8"));
|
|
37250
37677
|
} catch (ex) {
|
|
37251
37678
|
err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
|
|
37252
37679
|
continue;
|
|
@@ -37579,7 +38006,7 @@ function checkMenus(ctx) {
|
|
|
37579
38006
|
}
|
|
37580
38007
|
|
|
37581
38008
|
// src/tools/validate/security.ts
|
|
37582
|
-
var
|
|
38009
|
+
var fs14 = __toESM(require("fs"));
|
|
37583
38010
|
function checkRoleRights(ctx) {
|
|
37584
38011
|
const { paths, manifest, err, deps, isKnownEntity, roles, actions, reports } = ctx;
|
|
37585
38012
|
const storedProcedures = readJsonOrDefault(paths.storedProcedures, {});
|
|
@@ -37595,7 +38022,7 @@ function checkRoleRights(ctx) {
|
|
|
37595
38022
|
const code2 = dot < 0 ? ref : ref.slice(dot + 1);
|
|
37596
38023
|
if (mod !== manifest.code) {
|
|
37597
38024
|
if (!deps.has(mod)) err(`roles \u2192 ${rcode}`, `grants on '${key}' but '${mod}' is not a declared dependency`);
|
|
37598
|
-
} else if (!
|
|
38025
|
+
} else if (!fs14.existsSync(absFile)) {
|
|
37599
38026
|
err(`roles \u2192 ${rcode}`, `grants on '${key}' but ${file2} is missing \u2014 declare the ${noun} there`);
|
|
37600
38027
|
} else if (!(code2 in own)) {
|
|
37601
38028
|
err(`roles \u2192 ${rcode}`, `grants on '${key}' but no such ${noun} exists in ${file2}`);
|
|
@@ -37629,120 +38056,6 @@ function checkSelectCoverage(ctx) {
|
|
|
37629
38056
|
}
|
|
37630
38057
|
}
|
|
37631
38058
|
|
|
37632
|
-
// src/tools/validate/translations.ts
|
|
37633
|
-
var fs14 = __toESM(require("fs"));
|
|
37634
|
-
var path16 = __toESM(require("path"));
|
|
37635
|
-
function resolveTranslationFile(translationsDir, locale) {
|
|
37636
|
-
const exact = path16.join(translationsDir, `${locale}.json`);
|
|
37637
|
-
if (fs14.existsSync(exact)) return exact;
|
|
37638
|
-
if (!fs14.existsSync(translationsDir)) return void 0;
|
|
37639
|
-
const want = `${locale}.json`.toLowerCase();
|
|
37640
|
-
for (const f of fs14.readdirSync(translationsDir)) {
|
|
37641
|
-
if (f.toLowerCase() === want) return path16.join(translationsDir, f);
|
|
37642
|
-
}
|
|
37643
|
-
return void 0;
|
|
37644
|
-
}
|
|
37645
|
-
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
37646
|
-
if (!root || typeof root !== "object") return false;
|
|
37647
|
-
const entities = root.entities;
|
|
37648
|
-
if (!entities || typeof entities !== "object") return false;
|
|
37649
|
-
const entity = entities[entityCd];
|
|
37650
|
-
if (!entity || typeof entity !== "object") return false;
|
|
37651
|
-
const constraints = entity.constraints;
|
|
37652
|
-
if (!constraints || typeof constraints !== "object") return false;
|
|
37653
|
-
const ck = constraints[constraintCd];
|
|
37654
|
-
if (!ck || typeof ck !== "object") return false;
|
|
37655
|
-
const msg = ck.message;
|
|
37656
|
-
return typeof msg === "string" && msg.trim() !== "";
|
|
37657
|
-
}
|
|
37658
|
-
function checkConstraintTranslations(ctx) {
|
|
37659
|
-
const { paths, warn, entities, supportedLocales } = ctx;
|
|
37660
|
-
if (supportedLocales.length > 0) {
|
|
37661
|
-
const declared = [];
|
|
37662
|
-
for (const [name, e] of Object.entries(entities)) {
|
|
37663
|
-
if (typeof e.extends === "string" && e.extends) continue;
|
|
37664
|
-
const constraints = e.constraints;
|
|
37665
|
-
if (!constraints || typeof constraints !== "object") continue;
|
|
37666
|
-
for (const [cname, c] of Object.entries(constraints)) {
|
|
37667
|
-
if (!c || typeof c !== "object") continue;
|
|
37668
|
-
const msg = c.message;
|
|
37669
|
-
if (typeof msg === "string" && msg.trim() !== "") {
|
|
37670
|
-
declared.push({ entity: name, constraint: cname, message: msg });
|
|
37671
|
-
}
|
|
37672
|
-
}
|
|
37673
|
-
}
|
|
37674
|
-
if (declared.length > 0) {
|
|
37675
|
-
const seen = /* @__PURE__ */ new Set();
|
|
37676
|
-
for (const raw of supportedLocales) {
|
|
37677
|
-
const locale = raw.trim();
|
|
37678
|
-
if (!locale) continue;
|
|
37679
|
-
const lc = locale.toLowerCase();
|
|
37680
|
-
if (lc === "en" || lc.startsWith("en-")) continue;
|
|
37681
|
-
if (seen.has(lc)) continue;
|
|
37682
|
-
seen.add(lc);
|
|
37683
|
-
let tx = null;
|
|
37684
|
-
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
37685
|
-
if (abs) {
|
|
37686
|
-
try {
|
|
37687
|
-
tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
|
|
37688
|
-
} catch {
|
|
37689
|
-
tx = null;
|
|
37690
|
-
}
|
|
37691
|
-
}
|
|
37692
|
-
for (const d of declared) {
|
|
37693
|
-
if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
|
|
37694
|
-
warn(
|
|
37695
|
-
`translations/${locale}.json`,
|
|
37696
|
-
`constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
|
|
37697
|
-
);
|
|
37698
|
-
}
|
|
37699
|
-
}
|
|
37700
|
-
}
|
|
37701
|
-
}
|
|
37702
|
-
}
|
|
37703
|
-
}
|
|
37704
|
-
function checkTranslationCompleteness(ctx) {
|
|
37705
|
-
const { paths, err, warn, roles, supportedLocales } = ctx;
|
|
37706
|
-
const roleCodes = Object.keys(roles);
|
|
37707
|
-
const localeFiles = fs14.existsSync(paths.translationsDir) ? fs14.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
|
|
37708
|
-
for (const raw of supportedLocales) {
|
|
37709
|
-
const locale = raw.trim();
|
|
37710
|
-
if (!locale) continue;
|
|
37711
|
-
if (!resolveTranslationFile(paths.translationsDir, locale)) {
|
|
37712
|
-
err(
|
|
37713
|
-
"translations",
|
|
37714
|
-
`manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
|
|
37715
|
-
);
|
|
37716
|
-
}
|
|
37717
|
-
}
|
|
37718
|
-
if (roleCodes.length > 0 && localeFiles.length === 0) {
|
|
37719
|
-
warn(
|
|
37720
|
-
"translations",
|
|
37721
|
-
`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.`
|
|
37722
|
-
);
|
|
37723
|
-
}
|
|
37724
|
-
for (const file2 of localeFiles) {
|
|
37725
|
-
let tx;
|
|
37726
|
-
try {
|
|
37727
|
-
tx = JSON.parse(fs14.readFileSync(path16.join(paths.translationsDir, file2), "utf8"));
|
|
37728
|
-
} catch (ex) {
|
|
37729
|
-
err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
|
|
37730
|
-
continue;
|
|
37731
|
-
}
|
|
37732
|
-
const txRoles = tx.roles ?? {};
|
|
37733
|
-
const missing = roleCodes.filter((rc) => {
|
|
37734
|
-
const entry = txRoles[rc];
|
|
37735
|
-
return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
|
|
37736
|
-
});
|
|
37737
|
-
if (missing.length > 0) {
|
|
37738
|
-
err(
|
|
37739
|
-
`translations/${file2}`,
|
|
37740
|
-
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
|
|
37741
|
-
);
|
|
37742
|
-
}
|
|
37743
|
-
}
|
|
37744
|
-
}
|
|
37745
|
-
|
|
37746
38059
|
// src/tools/validate/logic.ts
|
|
37747
38060
|
var fs15 = __toESM(require("fs"));
|
|
37748
38061
|
var path17 = __toESM(require("path"));
|
|
@@ -37836,9 +38149,10 @@ function checkTriggersJobsWebhooks(ctx) {
|
|
|
37836
38149
|
}
|
|
37837
38150
|
function checkActionDsl(ctx) {
|
|
37838
38151
|
const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
|
|
38152
|
+
const messageTranslations = readMessageTranslations(paths.translationsDir, ctx.supportedLocales);
|
|
37839
38153
|
for (const [acode, a] of Object.entries(actions)) {
|
|
37840
|
-
const
|
|
37841
|
-
|
|
38154
|
+
const declared = a?.script;
|
|
38155
|
+
const script = typeof declared === "string" && declared ? declared : acode;
|
|
37842
38156
|
const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
|
|
37843
38157
|
if (!fs15.existsSync(dslPath)) continue;
|
|
37844
38158
|
let body;
|
|
@@ -37857,7 +38171,8 @@ function checkActionDsl(ctx) {
|
|
|
37857
38171
|
executionMode: mode,
|
|
37858
38172
|
actionCode: acode,
|
|
37859
38173
|
moduleCode: manifest.code,
|
|
37860
|
-
entity: entityCode && columns ? { qualified: `${manifest.code}.${entityCode}`, columns } : void 0
|
|
38174
|
+
entity: entityCode && columns ? { qualified: `${manifest.code}.${entityCode}`, columns } : void 0,
|
|
38175
|
+
messageTranslations
|
|
37861
38176
|
})) {
|
|
37862
38177
|
issues.push({
|
|
37863
38178
|
level: issue2.level,
|
|
@@ -37877,7 +38192,7 @@ function checkFolders(ctx) {
|
|
|
37877
38192
|
for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
|
|
37878
38193
|
err(
|
|
37879
38194
|
"ui/folders.json",
|
|
37880
|
-
`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,
|
|
38195
|
+
`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, so duplicates are ambiguous.`
|
|
37881
38196
|
);
|
|
37882
38197
|
}
|
|
37883
38198
|
const folderNodes = [
|
|
@@ -38090,7 +38405,8 @@ var CHECKS = [
|
|
|
38090
38405
|
checkFolders,
|
|
38091
38406
|
checkDiagrams,
|
|
38092
38407
|
checkReports,
|
|
38093
|
-
checkTranslationCompleteness
|
|
38408
|
+
checkTranslationCompleteness,
|
|
38409
|
+
checkMessageTranslations
|
|
38094
38410
|
];
|
|
38095
38411
|
function moduleValidate(args, cli) {
|
|
38096
38412
|
const ctx = loadContext(args.moduleDir);
|
|
@@ -38463,6 +38779,11 @@ var resources = [
|
|
|
38463
38779
|
"Print templates JSON schema",
|
|
38464
38780
|
"JSON Schema for ui/print_templates.json \u2014 Liquid HTML print templates and reusable snippets, bound to entities for the print menu."
|
|
38465
38781
|
),
|
|
38782
|
+
schema(
|
|
38783
|
+
"translations",
|
|
38784
|
+
"Translations JSON schema",
|
|
38785
|
+
"JSON Schema for translations/<locale>.json \u2014 per-section labels (entities, fields, options, constraints, folders, views, menus, settings, roles, reports, actions) and the `messages` block that translates DSL info/warn/error/exit texts, keyed by the exact English text."
|
|
38786
|
+
),
|
|
38466
38787
|
schema(
|
|
38467
38788
|
"settings",
|
|
38468
38789
|
"Module settings JSON schema",
|