@datarails-ai/dr-cli 1.6.98 → 1.6.99
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/README.md +54 -0
- package/dist/cli.js +199 -9
- package/dist/scopes-map.json +9 -1
- package/dist/skills/dr-cli/SKILL.md +5 -2
- package/dist/skills/dr-cli/reference/admin.md +29 -2
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +7 -0
- package/dist/skills/fx-onboard/SKILL.md +10 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -477,6 +477,60 @@ plugins tree (manifests, frontmatter, versions, the facts file, `CONTRIBUTING.md
|
|
|
477
477
|
`node --test plugins/dr-cli/hooks/tests/auto-update.test.js` runs the hook's
|
|
478
478
|
contract suite.
|
|
479
479
|
|
|
480
|
+
### The endpoint inventory
|
|
481
|
+
|
|
482
|
+
`scopes-endpoints.snapshot.json` lists, for every leaf command, the HTTP endpoints it
|
|
483
|
+
can reach — a top-level, non-empty JSON array of
|
|
484
|
+
`{command, method, url_template, service, certainty, non_integer_params?}` records.
|
|
485
|
+
It is the input of the scope contract gate in dr-datarails-server
|
|
486
|
+
(`HippiWeb/JWT_auth/scope_contract.py`). Today that gate checks that each record's URL
|
|
487
|
+
resolves to a monolith view and that the view declares its scope; holding a command's
|
|
488
|
+
endpoints against its ceiling in `scopes-map.snapshot.json` is what DR-53487 may add.
|
|
489
|
+
`pnpm gen:endpoints` also writes the same bytes to `dist/scopes-endpoints.json`.
|
|
490
|
+
|
|
491
|
+
- **Superset rule.** The list may hold an endpoint a command never actually sends, but
|
|
492
|
+
never omits one it can send: a gate built on it may over-report, never under-report.
|
|
493
|
+
- **`certainty`.** `static` was read from the call and linked to the leaf through the
|
|
494
|
+
call graph; `inferred` is a curated or fallback attribution.
|
|
495
|
+
- **`url_template`.** A path only, no host and no query; a dynamic segment is `{name}`.
|
|
496
|
+
- **`non_integer_params`.** The placeholders whose value is not a number (uuid, token,
|
|
497
|
+
slug…), so substituting a literal `1` will not resolve that route. Absent means every
|
|
498
|
+
placeholder takes an integer.
|
|
499
|
+
- **`service`.** Which service serves the path: `monolith`, `finance-os`,
|
|
500
|
+
`spend-control`, `multi-product`, `desk`, `connectors`, `onboarding-validation`.
|
|
501
|
+
A consumer checking one service filters on it.
|
|
502
|
+
- **`command: "*"`.** A call any authenticated command may trigger (JWT mint/refresh,
|
|
503
|
+
API-key and login-as re-exchange).
|
|
504
|
+
- **Leaves with no record** are explained in `scripts/scopes-map.sources.json` under
|
|
505
|
+
`endpoints`: `noHttpCommands` (no Datarails call) and `externalCommands` (the App Space
|
|
506
|
+
leaves mounted from `@datarails-ai/dr-appspace-cli`, which owns its own contract).
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
pnpm gen:endpoints # rewrite the snapshot (and dist/scopes-endpoints.json)
|
|
510
|
+
pnpm check:endpoints # what CI runs: exit 1 on drift
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The generator is static analysis plus the curated tables in `scopes-map.sources.json`;
|
|
514
|
+
its header (`scripts/gen-endpoints.mts`) documents each step and every fatal case.
|
|
515
|
+
`scripts/gen-endpoints.spotcheck.test.mts` runs a few leaves under msw and checks the
|
|
516
|
+
requests they send are in the snapshot — a sample, not the mechanism. Adding a command
|
|
517
|
+
or an api call changes the snapshot: run `pnpm gen:endpoints` and commit the diff.
|
|
518
|
+
|
|
519
|
+
**Transferring it to dr-datarails-server.** The snapshot is copied by hand, with the
|
|
520
|
+
dr-cli version it was taken at (the `version` in `package.json` at that commit), assuming
|
|
521
|
+
both repos are checked out side by side:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
cp dr-cli/scopes-endpoints.snapshot.json dr-datarails-server/HippiWeb/JWT_auth/tests/fixtures/dr_cli_endpoints.json
|
|
525
|
+
node -p "require('./dr-cli/package.json').version" > dr-datarails-server/HippiWeb/JWT_auth/tests/fixtures/dr_cli_endpoints.version
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
A missing fixture makes the server test skip with a reason; a broken one is a hard
|
|
529
|
+
failure. Lifting the skip is DR-53487. Two caveats for that work: the server's
|
|
530
|
+
`contract_violations` currently resolves every record against the monolith URLconf and
|
|
531
|
+
does not filter by `service`, so it must filter to `service == "monolith"`, and it must
|
|
532
|
+
decide how to treat `command: "*"` records and `non_integer_params`.
|
|
533
|
+
|
|
480
534
|
### Adding a gated (destructive/sensitive) command
|
|
481
535
|
|
|
482
536
|
Sensitive writes route through `twoStepConfirm` (`src/commands/_confirm.ts`),
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@datarails-ai/dr-cli",
|
|
11
|
-
version: "1.6.
|
|
11
|
+
version: "1.6.99",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -29,6 +29,8 @@ var package_default = {
|
|
|
29
29
|
build: "tsup",
|
|
30
30
|
"gen:scopes": "tsx scripts/gen-scopes-map.mts",
|
|
31
31
|
"check:scopes": "tsx scripts/gen-scopes-map.mts --check",
|
|
32
|
+
"gen:endpoints": "tsx scripts/gen-endpoints.mts",
|
|
33
|
+
"check:endpoints": "tsx scripts/gen-endpoints.mts --check",
|
|
32
34
|
typecheck: "tsc -p tsconfig.build.json --noEmit",
|
|
33
35
|
test: "vitest run",
|
|
34
36
|
"test:watch": "vitest",
|
|
@@ -67,7 +69,7 @@ var package_default = {
|
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
// src/buildInfo.ts
|
|
70
|
-
var BUILD_TIME = "2026-10-
|
|
72
|
+
var BUILD_TIME = "2026-10-05T12:03:05.855Z";
|
|
71
73
|
function formatBuildTime(iso) {
|
|
72
74
|
if (iso === "") return "";
|
|
73
75
|
const d = new Date(iso);
|
|
@@ -25578,6 +25580,62 @@ function symbolForCurrencyCode(code) {
|
|
|
25578
25580
|
return FX_CURRENCY_SYMBOLS.get(code.trim().toUpperCase()) ?? null;
|
|
25579
25581
|
}
|
|
25580
25582
|
|
|
25583
|
+
// src/commands/_currencySymbol.ts
|
|
25584
|
+
var FIXED_SIGNS_RE = /"[$€£₪]"|[$€£₪]/g;
|
|
25585
|
+
var DEFAULT_NUMBER_FORMAT = "#,###";
|
|
25586
|
+
function stripFixedCurrencySigns(format) {
|
|
25587
|
+
return format ? format.replace(FIXED_SIGNS_RE, "") : format;
|
|
25588
|
+
}
|
|
25589
|
+
function withCurrencySign(format, sign) {
|
|
25590
|
+
const cleanSign = sign.replace(/"/g, "");
|
|
25591
|
+
const literal = `"${cleanSign}${/^[A-Za-z]+$/.test(cleanSign) ? " " : ""}"`;
|
|
25592
|
+
const base = format || DEFAULT_NUMBER_FORMAT;
|
|
25593
|
+
return base.split(";").map((section) => {
|
|
25594
|
+
if (section === "") return section;
|
|
25595
|
+
const leading = section.match(/^(\[[^\]]*\])*(?=.)/)?.[0] ?? "";
|
|
25596
|
+
const rest = section.slice(leading.length);
|
|
25597
|
+
const paren = rest.match(/^\((.*)\)$/);
|
|
25598
|
+
return paren ? `${leading}(${literal}${paren[1]})` : `${leading}${literal}${rest}`;
|
|
25599
|
+
}).join(";");
|
|
25600
|
+
}
|
|
25601
|
+
function parseWidgetOptions(raw) {
|
|
25602
|
+
if (raw === void 0 || raw === null || raw === "") return {};
|
|
25603
|
+
if (typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
25604
|
+
if (typeof raw !== "string") return null;
|
|
25605
|
+
try {
|
|
25606
|
+
const parsed2 = JSON.parse(raw);
|
|
25607
|
+
return typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2) ? parsed2 : null;
|
|
25608
|
+
} catch {
|
|
25609
|
+
return null;
|
|
25610
|
+
}
|
|
25611
|
+
}
|
|
25612
|
+
function readAutoCurrencySymbol(raw) {
|
|
25613
|
+
const opts = parseWidgetOptions(raw);
|
|
25614
|
+
if (opts === null) return null;
|
|
25615
|
+
return opts["autoCurrencySymbol"] === true;
|
|
25616
|
+
}
|
|
25617
|
+
function setAutoCurrencySymbol(raw, enabled) {
|
|
25618
|
+
const opts = parseWidgetOptions(raw);
|
|
25619
|
+
if (opts === null) {
|
|
25620
|
+
throw new Error("widget options is not a JSON object \u2014 refusing to rewrite it");
|
|
25621
|
+
}
|
|
25622
|
+
return JSON.stringify({ ...opts, autoCurrencySymbol: enabled });
|
|
25623
|
+
}
|
|
25624
|
+
function fixedSignOf(format) {
|
|
25625
|
+
if (typeof format !== "string") return null;
|
|
25626
|
+
const m = format.match(/^(?:\[[^\]]*\])*\(?"([^"]+)"/);
|
|
25627
|
+
const sign = m?.[1];
|
|
25628
|
+
return sign === void 0 ? null : sign.trim();
|
|
25629
|
+
}
|
|
25630
|
+
function describeCurrencySymbol(widget) {
|
|
25631
|
+
const auto = readAutoCurrencySymbol(widget["options"]);
|
|
25632
|
+
if (auto === true && widget["is_fx_aware"] === true) {
|
|
25633
|
+
return "Auto (follows the dashboard FX selector)";
|
|
25634
|
+
}
|
|
25635
|
+
const sign = fixedSignOf(widget["values_format"]);
|
|
25636
|
+
return sign === null ? "none" : `"${sign}" (fixed)`;
|
|
25637
|
+
}
|
|
25638
|
+
|
|
25581
25639
|
// src/commands/fxConfig.ts
|
|
25582
25640
|
function writeTable20(out, rows) {
|
|
25583
25641
|
if (rows.length === 0) return;
|
|
@@ -26571,6 +26629,11 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26571
26629
|
"--force-sum has no meaning with --off (turning conversion off never touches the aggregator)"
|
|
26572
26630
|
);
|
|
26573
26631
|
}
|
|
26632
|
+
if (cfg.disable && !cfg.autoSymbol) {
|
|
26633
|
+
throw new Error(
|
|
26634
|
+
"--no-auto-symbol has no meaning with --off (turning conversion off always clears the Auto currency symbol)"
|
|
26635
|
+
);
|
|
26636
|
+
}
|
|
26574
26637
|
let targetDashboards;
|
|
26575
26638
|
if (cfg.all) {
|
|
26576
26639
|
const dashboards = await list8(cfg.client, { signal: cfg.signal });
|
|
@@ -26619,6 +26682,7 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26619
26682
|
const v0 = vals !== null && vals.length === 1 ? vals[0] : null;
|
|
26620
26683
|
const val = v0 !== null && typeof v0 === "object" ? v0 : null;
|
|
26621
26684
|
const aggregator = val !== null ? String(val["aggregator"] ?? "").toUpperCase() : "";
|
|
26685
|
+
const autoSymbol = readAutoCurrencySymbol(raw["options"]);
|
|
26622
26686
|
if (cfg.disable) {
|
|
26623
26687
|
plan.push({
|
|
26624
26688
|
dashboardId: target.id,
|
|
@@ -26627,7 +26691,8 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26627
26691
|
widgetName,
|
|
26628
26692
|
aggregator,
|
|
26629
26693
|
raw,
|
|
26630
|
-
status: isAware ? "revert" : "already-off"
|
|
26694
|
+
status: isAware ? "revert" : "already-off",
|
|
26695
|
+
autoSymbol
|
|
26631
26696
|
});
|
|
26632
26697
|
continue;
|
|
26633
26698
|
}
|
|
@@ -26659,12 +26724,25 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26659
26724
|
widgetName,
|
|
26660
26725
|
aggregator,
|
|
26661
26726
|
raw,
|
|
26662
|
-
status: status2
|
|
26727
|
+
status: status2,
|
|
26728
|
+
autoSymbol
|
|
26663
26729
|
});
|
|
26664
26730
|
}
|
|
26665
26731
|
}
|
|
26666
26732
|
const subjectOf = (p) => `widget ${p.widgetId} "${p.widgetName}" (dashboard ${p.dashboardId} "${p.dashboardName}")`;
|
|
26667
|
-
const
|
|
26733
|
+
const symbolNote = (p) => {
|
|
26734
|
+
if (p.status === "convert" || p.status === "convert-sum") {
|
|
26735
|
+
return cfg.autoSymbol ? " (+ Auto currency symbol)" : "";
|
|
26736
|
+
}
|
|
26737
|
+
if (p.status === "already" && p.autoSymbol === false) {
|
|
26738
|
+
return ` (fixed currency symbol \u2014 \`dr widgets set-currency-symbol ${p.widgetId} -d ${p.dashboardId} --auto\` to follow the FX selector)`;
|
|
26739
|
+
}
|
|
26740
|
+
if (p.status === "revert" && p.autoSymbol === true) {
|
|
26741
|
+
return " (Auto currency symbol \u2192 none)";
|
|
26742
|
+
}
|
|
26743
|
+
return "";
|
|
26744
|
+
};
|
|
26745
|
+
const describeEntry = (p) => planLine(p.status, subjectOf(p), p.aggregator) + symbolNote(p);
|
|
26668
26746
|
const targets = plan.filter((p) => isActionable(p.status, cfg.disable));
|
|
26669
26747
|
const needsRole = plan.filter((p) => p.status === "needs-fx-role").length;
|
|
26670
26748
|
const summary = () => {
|
|
@@ -26680,6 +26758,7 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26680
26758
|
writeJSONIndented(cfg.out, {
|
|
26681
26759
|
dry_run: true,
|
|
26682
26760
|
direction: cfg.disable ? "off" : "on",
|
|
26761
|
+
auto_symbol: cfg.disable ? false : cfg.autoSymbol,
|
|
26683
26762
|
scanned,
|
|
26684
26763
|
not_eligible: notEligible,
|
|
26685
26764
|
plan: plan.map((p) => ({
|
|
@@ -26688,7 +26767,8 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26688
26767
|
widget_id: p.widgetId,
|
|
26689
26768
|
name: p.widgetName,
|
|
26690
26769
|
aggregator: p.aggregator,
|
|
26691
|
-
status: p.status
|
|
26770
|
+
status: p.status,
|
|
26771
|
+
auto_currency_symbol: p.autoSymbol
|
|
26692
26772
|
})),
|
|
26693
26773
|
unreadable
|
|
26694
26774
|
});
|
|
@@ -26728,10 +26808,19 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26728
26808
|
const v0 = vals[0];
|
|
26729
26809
|
body["vals"] = [{ ...v0, aggregator: "SUM" }];
|
|
26730
26810
|
}
|
|
26811
|
+
let wantAuto = null;
|
|
26812
|
+
if (want && cfg.autoSymbol) wantAuto = true;
|
|
26813
|
+
else if (!want && p.autoSymbol === true) wantAuto = false;
|
|
26814
|
+
if (wantAuto !== null) body["options"] = setAutoCurrencySymbol(p.raw["options"], wantAuto);
|
|
26731
26815
|
const res = await update6(cfg.client, p.dashboardId, p.widgetId, body, {
|
|
26732
26816
|
signal: cfg.signal
|
|
26733
26817
|
});
|
|
26734
26818
|
assertFxAwareEcho(res["is_fx_aware"], want, "widget");
|
|
26819
|
+
if (wantAuto !== null && readAutoCurrencySymbol(res["options"]) !== wantAuto) {
|
|
26820
|
+
throw new Error(
|
|
26821
|
+
`server did not keep options.autoCurrencySymbol=${wantAuto} (the Auto currency symbol)`
|
|
26822
|
+
);
|
|
26823
|
+
}
|
|
26735
26824
|
results.push({
|
|
26736
26825
|
dashboard_id: p.dashboardId,
|
|
26737
26826
|
widget_id: p.widgetId,
|
|
@@ -26740,8 +26829,10 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26740
26829
|
});
|
|
26741
26830
|
if (!cfg.asJson) {
|
|
26742
26831
|
for (const line of unreadableLines(unreadable, "dashboard")) cfg.out.write(line + "\n");
|
|
26743
|
-
cfg.out.write(
|
|
26744
|
-
|
|
26832
|
+
cfg.out.write(
|
|
26833
|
+
`\u2713 ${describeConvertStatus(p.status, subjectOf(p), p.aggregator)}${symbolNote(p)}
|
|
26834
|
+
`
|
|
26835
|
+
);
|
|
26745
26836
|
}
|
|
26746
26837
|
} catch (err) {
|
|
26747
26838
|
if (cfg.signal.aborted) throw err;
|
|
@@ -27080,11 +27171,14 @@ function registerFxConfigCommands(fx) {
|
|
|
27080
27171
|
addServerFlag(convertFunctionsCmd);
|
|
27081
27172
|
addAccountFlag(convertFunctionsCmd);
|
|
27082
27173
|
const convertWidgetsCmd = fx.command("convert-widgets").description(
|
|
27083
|
-
"Enable currency conversion (is_fx_aware=true) on every eligible dashboard widget (PUT /api/dashboards/<d>/widgets/<w>, round-tripping the widget body verbatim). Eligible = the value section holds exactly one field, that field is Monetary, and it carries an fx_role. Widgets whose aggregator is not SUM are skipped unless --force-sum switches them to SUM. The server has the final say on the flag \u2014 a widget it deems ineligible is reported as failed. Use --off to sweep the other way and turn conversion off \u2014 a rollback, which ignores eligibility and the aggregator and acts on the stored flag alone. Dry-run by default \u2014 prints the plan; re-run with --confirm to apply. Scope with --dashboard <id> or sweep --all."
|
|
27174
|
+
"Enable currency conversion (is_fx_aware=true) on every eligible dashboard widget (PUT /api/dashboards/<d>/widgets/<w>, round-tripping the widget body verbatim). Eligible = the value section holds exactly one field, that field is Monetary, and it carries an fx_role. Widgets whose aggregator is not SUM are skipped unless --force-sum switches them to SUM. The server has the final say on the flag \u2014 a widget it deems ineligible is reported as failed. Each converted widget also switches to the Auto currency symbol (options.autoCurrencySymbol=true \u2014 the sign of the currency selected on the dashboard), as the widget editor does; --no-auto-symbol keeps the stored number format instead. Use --off to sweep the other way and turn conversion off \u2014 a rollback, which ignores eligibility and the aggregator and acts on the stored flag alone (and clears Auto, which only renders on a currency-aware widget). Dry-run by default \u2014 prints the plan; re-run with --confirm to apply. Scope with --dashboard <id> or sweep --all."
|
|
27084
27175
|
).option("--dashboard <id>", "Convert widgets on this dashboard only").option("--all", "Convert widgets on every dashboard", false).option(
|
|
27085
27176
|
"--force-sum",
|
|
27086
27177
|
"Also convert non-SUM widgets by switching their aggregator to SUM",
|
|
27087
27178
|
false
|
|
27179
|
+
).option(
|
|
27180
|
+
"--no-auto-symbol",
|
|
27181
|
+
"Keep each converted widget's stored number format (its fixed currency sign, if any) instead of switching it to the Auto currency symbol"
|
|
27088
27182
|
).option(
|
|
27089
27183
|
"--off",
|
|
27090
27184
|
"Reverse direction: turn currency conversion OFF on every currency-aware widget in scope",
|
|
@@ -27100,6 +27194,7 @@ function registerFxConfigCommands(fx) {
|
|
|
27100
27194
|
all: opts.all === true,
|
|
27101
27195
|
forceSum: opts.forceSum === true,
|
|
27102
27196
|
disable: opts.off === true,
|
|
27197
|
+
autoSymbol: opts.autoSymbol !== false,
|
|
27103
27198
|
confirm: opts.confirm === true,
|
|
27104
27199
|
asJson: opts.json === true,
|
|
27105
27200
|
signal: setup.signal,
|
|
@@ -41231,6 +41326,10 @@ async function runWidgetsGet(cfg) {
|
|
|
41231
41326
|
cfg.out.write(`chart_type: ${found["chart_type"] ?? ""}
|
|
41232
41327
|
`);
|
|
41233
41328
|
cfg.out.write(`template_id: ${found["template_id"] ?? "(none)"}
|
|
41329
|
+
`);
|
|
41330
|
+
cfg.out.write(`fx_aware: ${found["is_fx_aware"] === true ? "yes" : "no"}
|
|
41331
|
+
`);
|
|
41332
|
+
cfg.out.write(`fx_symbol: ${describeCurrencySymbol(found)}
|
|
41234
41333
|
`);
|
|
41235
41334
|
cfg.out.write("\n(use --json for the full widget body)\n");
|
|
41236
41335
|
}
|
|
@@ -41309,6 +41408,67 @@ async function runWidgetsUpdate(cfg) {
|
|
|
41309
41408
|
});
|
|
41310
41409
|
printWidgetMutation(cfg.out, updated, "Updated", cfg.asJson);
|
|
41311
41410
|
}
|
|
41411
|
+
async function runWidgetsSetCurrencySymbol(cfg) {
|
|
41412
|
+
if (cfg.dashboardId <= 0) throw new Error("--dashboard is required");
|
|
41413
|
+
if (cfg.id <= 0) throw new Error("widget id must be positive");
|
|
41414
|
+
const sign = cfg.mode === "sign" ? (cfg.sign ?? "").trim() : "";
|
|
41415
|
+
if (cfg.mode === "sign" && (sign === "" || sign.length > 5 || sign.includes('"'))) {
|
|
41416
|
+
throw new Error(
|
|
41417
|
+
"--sign needs a currency sign of 1\u20135 characters without quotes (e.g. $, \u20AC, \xA3, \u20AA or a code such as CHF)"
|
|
41418
|
+
);
|
|
41419
|
+
}
|
|
41420
|
+
const all = await listRaw3(cfg.client, cfg.dashboardId, { signal: cfg.signal });
|
|
41421
|
+
const found = all.find((w) => w["id"] === cfg.id);
|
|
41422
|
+
if (found === void 0) {
|
|
41423
|
+
throw new Error(`widget ${cfg.id} not found on dashboard ${cfg.dashboardId}`);
|
|
41424
|
+
}
|
|
41425
|
+
if (cfg.mode === "auto" && found["is_fx_aware"] !== true) {
|
|
41426
|
+
throw new Error(
|
|
41427
|
+
`widget ${cfg.id} is not currency-aware \u2014 the Auto symbol only renders on a widget with currency conversion on; run dr fx convert-widgets --dashboard ${cfg.dashboardId} first`
|
|
41428
|
+
);
|
|
41429
|
+
}
|
|
41430
|
+
const wantAuto = cfg.mode === "auto";
|
|
41431
|
+
const current = typeof found["values_format"] === "string" ? found["values_format"] : null;
|
|
41432
|
+
const body = {
|
|
41433
|
+
...found,
|
|
41434
|
+
options: setAutoCurrencySymbol(found["options"], wantAuto)
|
|
41435
|
+
};
|
|
41436
|
+
if (cfg.mode === "none") {
|
|
41437
|
+
body["values_format"] = stripFixedCurrencySigns(current) || null;
|
|
41438
|
+
} else if (cfg.mode === "sign") {
|
|
41439
|
+
body["values_format"] = withCurrencySign(stripFixedCurrencySigns(current), sign);
|
|
41440
|
+
}
|
|
41441
|
+
const res = await update6(cfg.client, cfg.dashboardId, cfg.id, body, {
|
|
41442
|
+
signal: cfg.signal
|
|
41443
|
+
});
|
|
41444
|
+
if (readAutoCurrencySymbol(res["options"]) !== wantAuto) {
|
|
41445
|
+
throw new Error(
|
|
41446
|
+
`server did not keep options.autoCurrencySymbol=${wantAuto} (the Auto currency symbol)`
|
|
41447
|
+
);
|
|
41448
|
+
}
|
|
41449
|
+
if (cfg.mode !== "auto" && (res["values_format"] ?? null) !== body["values_format"]) {
|
|
41450
|
+
throw new Error(
|
|
41451
|
+
`server did not keep values_format ${JSON.stringify(body["values_format"])} (came back ${JSON.stringify(res["values_format"] ?? null)})`
|
|
41452
|
+
);
|
|
41453
|
+
}
|
|
41454
|
+
if (cfg.asJson) {
|
|
41455
|
+
writeJSONIndented(cfg.out, {
|
|
41456
|
+
id: res["id"] ?? cfg.id,
|
|
41457
|
+
name: res["name"] ?? found["name"] ?? null,
|
|
41458
|
+
is_fx_aware: res["is_fx_aware"] === true,
|
|
41459
|
+
auto_currency_symbol: wantAuto,
|
|
41460
|
+
values_format: res["values_format"] ?? null,
|
|
41461
|
+
currency_symbol: describeCurrencySymbol(res)
|
|
41462
|
+
});
|
|
41463
|
+
return;
|
|
41464
|
+
}
|
|
41465
|
+
cfg.out.write(
|
|
41466
|
+
`widget ${cfg.id} "${String(found["name"] ?? "")}": currency symbol \u2192 ${describeCurrencySymbol(res)}
|
|
41467
|
+
`
|
|
41468
|
+
);
|
|
41469
|
+
cfg.out.write(`values_format: ${res["values_format"] ?? "(none)"}
|
|
41470
|
+
`);
|
|
41471
|
+
}
|
|
41312
41472
|
async function runWidgetsDelete(cfg) {
|
|
41313
41473
|
if (cfg.dashboardId <= 0) throw new Error("--dashboard is required");
|
|
41314
41474
|
if (cfg.id <= 0) throw new Error("widget id must be positive");
|
|
@@ -41660,6 +41820,36 @@ function registerWidgetsCommand(root) {
|
|
|
41660
41820
|
});
|
|
41661
41821
|
addServerFlag(updateCmd);
|
|
41662
41822
|
addAccountFlag(updateCmd);
|
|
41823
|
+
const symbolCmd = widgets.command("set-currency-symbol").description(
|
|
41824
|
+
"Set a widget's currency symbol the way the widget editor's symbol dropdown does. --auto follows the dashboard FX selector (options.autoCurrencySymbol=true; offered on currency-aware widgets only \u2014 see fx convert-widgets); --none shows no sign (clears Auto and strips the fixed $ \u20AC \xA3 \u20AA from the number format); --sign <S> pins one fixed sign as the format's leading literal (clears Auto). Read-modify-write of the widget body (PUT /api/dashboards/<d>/widgets/<w>); the echo is verified."
|
|
41825
|
+
).argument("<id>", "widget id (positive integer)").requiredOption("-d, --dashboard <id>", "dashboard id (positive integer)").option("--auto", "Follow the dashboard FX selector (currency-aware widgets only)", false).option("--none", "No currency sign", false).option("--sign <S>", "One fixed sign: $ \u20AC \xA3 \u20AA, or a code such as CHF").option("--json", "Output a structured JSON result instead of a summary", false).action(
|
|
41826
|
+
async (idStr, opts) => {
|
|
41827
|
+
const id = parseWidgetIdArg(idStr);
|
|
41828
|
+
const dashboardId = parseDashboardFlag(opts.dashboard);
|
|
41829
|
+
const picked = [opts.auto === true, opts.none === true, opts.sign !== void 0].filter(
|
|
41830
|
+
Boolean
|
|
41831
|
+
).length;
|
|
41832
|
+
if (picked !== 1) throw new Error("pass exactly one of --auto, --none or --sign <S>");
|
|
41833
|
+
const mode = opts.auto === true ? "auto" : opts.none === true ? "none" : "sign";
|
|
41834
|
+
const setup = await setupCall(opts.server, opts.account);
|
|
41835
|
+
try {
|
|
41836
|
+
await runWidgetsSetCurrencySymbol({
|
|
41837
|
+
client: setup.client,
|
|
41838
|
+
dashboardId,
|
|
41839
|
+
id,
|
|
41840
|
+
mode,
|
|
41841
|
+
sign: opts.sign,
|
|
41842
|
+
asJson: opts.json === true,
|
|
41843
|
+
signal: setup.signal,
|
|
41844
|
+
out: process.stdout
|
|
41845
|
+
});
|
|
41846
|
+
} finally {
|
|
41847
|
+
setup.cleanup();
|
|
41848
|
+
}
|
|
41849
|
+
}
|
|
41850
|
+
);
|
|
41851
|
+
addServerFlag(symbolCmd);
|
|
41852
|
+
addAccountFlag(symbolCmd);
|
|
41663
41853
|
const deleteCmd = widgets.command("delete").description("Delete a widget (two-step confirm)").argument("<id>", "widget id (positive integer)").requiredOption("-d, --dashboard <id>", "dashboard id (positive integer)").option("--confirm-token <tok>", "Token from a prior plan run \u2014 required to actually delete").action(async (idStr, opts) => {
|
|
41664
41854
|
const id = parseWidgetIdArg(idStr);
|
|
41665
41855
|
const dashboardId = parseDashboardFlag(opts.dashboard);
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.99",
|
|
4
4
|
"_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
|
|
5
5
|
"commands": {
|
|
6
6
|
"login": {
|
|
@@ -2251,6 +2251,14 @@
|
|
|
2251
2251
|
}
|
|
2252
2252
|
]
|
|
2253
2253
|
},
|
|
2254
|
+
"widgets set-currency-symbol": {
|
|
2255
|
+
"requires": [
|
|
2256
|
+
{
|
|
2257
|
+
"resource": "dashboards",
|
|
2258
|
+
"action": "write"
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
2261
|
+
},
|
|
2254
2262
|
"widgets delete": {
|
|
2255
2263
|
"requires": [
|
|
2256
2264
|
{
|
|
@@ -248,7 +248,10 @@ area's gotchas.
|
|
|
248
248
|
- `dashboards push <id> --to <orgs> [--override]` — copy a dashboard into other
|
|
249
249
|
organizations (two-step confirm). Destinations must come from
|
|
250
250
|
`organizations push-targets`.
|
|
251
|
-
- `widgets list|get|data|create|update|delete|chart|kpi|pie|table|gauge|waterfall|published-item|text` (alias `widget`)
|
|
251
|
+
- `widgets list|get|data|create|update|set-currency-symbol|delete|chart|kpi|pie|table|gauge|waterfall|published-item|text` (alias `widget`)
|
|
252
|
+
— `widgets set-currency-symbol <id> -d <dash> --auto|--none|--sign <S>` is the
|
|
253
|
+
widget editor's currency-symbol dropdown: **Auto** (the sign of the currency
|
|
254
|
+
selected on the dashboard; currency-aware widgets only), none, or one fixed sign.
|
|
252
255
|
— `widgets data` applies the dashboard's saved filter bar + date picker by
|
|
253
256
|
default so results match the product UI; `--no-dashboard-context` computes
|
|
254
257
|
from the widget's own settings only.
|
|
@@ -439,7 +442,7 @@ area's gotchas.
|
|
|
439
442
|
- `devices list|get|logs|request-logs|received-logs|download-logs|commands|command-delete`
|
|
440
443
|
- `features list`, `features groups list|get`, `features orgs list|add|remove`
|
|
441
444
|
- `semantictables list|get|data|distinct` (alias `semtables`), `fx rates`
|
|
442
|
-
- `fx set-posting|set-reporting` (Monetary field FX roles; `--all-tables` sweeps the org), `fx default-currency` (org-wide default, Super Admin), `fx add-currency` (org custom currency, e.g. BUSD), `fx convert-functions|convert-widgets` (bulk currency-aware sweeps, `--off` to roll back)
|
|
445
|
+
- `fx set-posting|set-reporting` (Monetary field FX roles; `--all-tables` sweeps the org), `fx default-currency` (org-wide default, Super Admin), `fx add-currency` (org custom currency, e.g. BUSD), `fx convert-functions|convert-widgets` (bulk currency-aware sweeps, `--off` to roll back; `convert-widgets` also switches each converted widget to the Auto currency symbol unless `--no-auto-symbol`)
|
|
443
446
|
- `kb search|get|categories|sections|articles-in-section|recent|attachments|article-images`
|
|
444
447
|
- `mcp register|unregister|list|serve`, `skill list|print|install|init-project`
|
|
445
448
|
|
|
@@ -755,11 +755,38 @@ name.
|
|
|
755
755
|
**and carries an fx_role**. Non-SUM functions are skipped unless `--force-sum`
|
|
756
756
|
switches them. **Dry-run by default** — always run once without `--confirm` and
|
|
757
757
|
read the plan.
|
|
758
|
-
- `fx convert-widgets (--dashboard <id> | --all) [--force-sum] [--off] [--confirm]` —
|
|
758
|
+
- `fx convert-widgets (--dashboard <id> | --all) [--force-sum] [--no-auto-symbol] [--off] [--confirm]` —
|
|
759
759
|
same sweep for dashboard widgets (eligible = exactly one value field, Monetary,
|
|
760
760
|
with an fx_role). The server has the final say per widget; an echo that doesn't
|
|
761
761
|
confirm the flag is reported as a failure, not silently accepted. Dry-run by
|
|
762
|
-
default.
|
|
762
|
+
default. Each converted widget is also switched to the **Auto currency symbol**
|
|
763
|
+
(see below) — the plan line reads `enable currency conversion (+ Auto currency
|
|
764
|
+
symbol)`; `--no-auto-symbol` keeps the widget's stored number format instead.
|
|
765
|
+
|
|
766
|
+
**The currency symbol travels with the flag (DR-53702).** The widget editor's number
|
|
767
|
+
format offers three symbol states: a *fixed* sign (`"$"#,###` — the sign is the
|
|
768
|
+
format's leading literal), *none*, and — on currency-aware widgets only — **Auto**,
|
|
769
|
+
which renders the sign of the currency selected in the dashboard's FX selector (its
|
|
770
|
+
ISO code when a custom currency has no symbol). Auto is not in the format string; it is
|
|
771
|
+
`autoCurrencySymbol: true` inside the widget's `options` JSON blob, and the renderer
|
|
772
|
+
applies it only while `is_fx_aware` is true, stripping any fixed `$ € £ ₪` the format
|
|
773
|
+
still carries. The editor switches a widget to Auto when it becomes currency-aware and
|
|
774
|
+
back to none when it stops being so, and the CLI mirrors that:
|
|
775
|
+
|
|
776
|
+
- `fx convert-widgets` sets Auto on every widget it converts (the format itself is left
|
|
777
|
+
alone — the renderer ignores its fixed sign while Auto applies). `--off` clears Auto
|
|
778
|
+
on widgets that had it. An already-aware widget that still shows a fixed sign is
|
|
779
|
+
**not** changed by the sweep; the plan names it and points at the per-widget command.
|
|
780
|
+
- `widgets set-currency-symbol <id> -d <dashboard> (--auto | --none | --sign <S>)` is
|
|
781
|
+
the dropdown for one widget: `--auto` (refused on a widget that is not
|
|
782
|
+
currency-aware — convert it first), `--none` (clears Auto and strips the fixed
|
|
783
|
+
`$ € £ ₪` from every section of the format), `--sign <S>` (clears Auto and writes `S`
|
|
784
|
+
as the leading literal of every section, with the renderer's placement rules — after
|
|
785
|
+
`[Red]`-style tokens, inside parentheses, a trailing space after a letter code such as
|
|
786
|
+
`CHF`). Read-modify-write of the server's body with the echo verified; `--json` for
|
|
787
|
+
the resulting state.
|
|
788
|
+
- `widgets get <id> -d <dashboard>` prints `fx_aware:` and `fx_symbol:` (`Auto …`,
|
|
789
|
+
`none`, or `"$" (fixed)`) so the state is visible without reading the body.
|
|
763
790
|
|
|
764
791
|
**Eligibility is stricter server-side than in the web app.** `FxEligibilityService`
|
|
765
792
|
requires the value field to be Monetary AND to have an `fx_role`; the serializer
|
|
@@ -51,6 +51,13 @@ notifications. Confirm with `dashboards list --account <destination>`.
|
|
|
51
51
|
same as scheduled report emails.
|
|
52
52
|
- `widgets create --file` (power mode), `widgets update <id> --file`,
|
|
53
53
|
`widgets delete <id>` (two-step confirm).
|
|
54
|
+
- `widgets set-currency-symbol <id> -d <dashboard> --auto|--none|--sign <S>` — the
|
|
55
|
+
widget editor's currency-symbol dropdown from the CLI. `--auto` follows the
|
|
56
|
+
dashboard FX selector (currency-aware widgets only — `fx convert-widgets` first);
|
|
57
|
+
`--none` clears Auto and strips the fixed `$ € £ ₪` from the number format;
|
|
58
|
+
`--sign <S>` pins one fixed sign. `widgets get` shows the current state as
|
|
59
|
+
`fx_symbol:`. Full semantics under **FX configuration** in
|
|
60
|
+
[admin.md](admin.md).
|
|
54
61
|
- Flag-driven builders:
|
|
55
62
|
- `widgets chart --type column|bar|line|area` + modifiers `--stacked`
|
|
56
63
|
`--percent` `--smooth`.
|
|
@@ -246,6 +246,13 @@ Rules while executing:
|
|
|
246
246
|
|
|
247
247
|
- **Never pass `--force-sum` without saying so in the plan.** It changes a widget's
|
|
248
248
|
aggregator to SUM, which changes what the customer sees.
|
|
249
|
+
- **The widget sweep also switches each converted widget to the Auto currency
|
|
250
|
+
symbol** (the sign of the currency selected on the dashboard), exactly as the widget
|
|
251
|
+
editor does — the plan line says `(+ Auto currency symbol)`. Say so in the plan. If
|
|
252
|
+
the customer wants converted widgets to keep their fixed `$`, pass
|
|
253
|
+
`--no-auto-symbol`. Widgets that were already currency-aware keep their fixed sign;
|
|
254
|
+
the plan lists them with the `dr widgets set-currency-symbol <id> -d <dash> --auto`
|
|
255
|
+
command to run per widget if the customer wants them to follow the selector too.
|
|
249
256
|
- **Never use `--allow-custom` or `--allow-unlisted` to get past a validation error.**
|
|
250
257
|
They exist for a code you know is coming, not to silence a refusal.
|
|
251
258
|
- **Re-runs are safe.** Every command is idempotent — an already-configured field
|
|
@@ -307,7 +314,9 @@ These stay manual, and omitting them from the report leaves the job half-done:
|
|
|
307
314
|
|
|
308
315
|
- **Widgets / functions:** `dr fx convert-widgets --all --off --confirm` and
|
|
309
316
|
`dr fx convert-functions --off --confirm`. Acts on the stored flag alone, so it also
|
|
310
|
-
clears flags left on entities that no longer qualify.
|
|
317
|
+
clears flags left on entities that no longer qualify. `--off` also clears the Auto
|
|
318
|
+
currency symbol on widgets that had it; a widget's fixed sign, if it had one before
|
|
319
|
+
conversion, is still in its number format and shows again.
|
|
311
320
|
- **A widget body:** `dr widgets update <id> -d <dashboard> --file before-widget-<id>.json`.
|
|
312
321
|
- **Org default:** `dr fx default-currency --none --confirm` (back to posting currency)
|
|
313
322
|
or set the previous code.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datarails-ai/dr-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.99",
|
|
4
4
|
"description": "Datarails CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"build": "tsup",
|
|
49
49
|
"gen:scopes": "tsx scripts/gen-scopes-map.mts",
|
|
50
50
|
"check:scopes": "tsx scripts/gen-scopes-map.mts --check",
|
|
51
|
+
"gen:endpoints": "tsx scripts/gen-endpoints.mts",
|
|
52
|
+
"check:endpoints": "tsx scripts/gen-endpoints.mts --check",
|
|
51
53
|
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
52
54
|
"test": "vitest run",
|
|
53
55
|
"test:watch": "vitest",
|