@datarails-ai/dr-cli 1.6.98 → 1.6.100
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 +280 -22
- 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.100",
|
|
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:55:55.787Z";
|
|
71
73
|
function formatBuildTime(iso) {
|
|
72
74
|
if (iso === "") return "";
|
|
73
75
|
const d = new Date(iso);
|
|
@@ -5005,7 +5007,7 @@ async function runLogout(cfg) {
|
|
|
5005
5007
|
const session2 = await cfg.store.getSession(key, meta.email);
|
|
5006
5008
|
if (session2 === void 0) continue;
|
|
5007
5009
|
try {
|
|
5008
|
-
await revoker(host2, session2);
|
|
5010
|
+
writeNotes(cfg.out, await revoker(host2, session2));
|
|
5009
5011
|
} catch {
|
|
5010
5012
|
}
|
|
5011
5013
|
await cfg.store.removeSession(key, meta.email);
|
|
@@ -5038,32 +5040,95 @@ async function runLogout(cfg) {
|
|
|
5038
5040
|
}
|
|
5039
5041
|
const host = resolve(cfg.serverKey).host;
|
|
5040
5042
|
try {
|
|
5041
|
-
await revoker(host, session);
|
|
5043
|
+
writeNotes(cfg.out, await revoker(host, session));
|
|
5042
5044
|
} catch {
|
|
5043
5045
|
}
|
|
5044
5046
|
await cfg.store.removeSession(cfg.serverKey, email);
|
|
5045
5047
|
cfg.out.write(`Logged out of ${cfg.serverKey} as ${email}.
|
|
5046
5048
|
`);
|
|
5047
5049
|
}
|
|
5050
|
+
function writeNotes(out, notes) {
|
|
5051
|
+
for (const line of notes ?? []) out.write(`${line}
|
|
5052
|
+
`);
|
|
5053
|
+
}
|
|
5054
|
+
var REVOKE_TIMEOUT_MS = 5e3;
|
|
5048
5055
|
async function bestEffortLogout(host, session) {
|
|
5049
|
-
if (host === ""
|
|
5056
|
+
if (host === "") return [];
|
|
5050
5057
|
const base = host.replace(/\/+$/, "");
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5058
|
+
if (usesApiKey(session)) {
|
|
5059
|
+
return [
|
|
5060
|
+
"Note: the API key is still valid server-side until it is revoked where it was issued; only the local copy was removed."
|
|
5061
|
+
];
|
|
5062
|
+
}
|
|
5063
|
+
const refresh = session.jwtRefresh ?? "";
|
|
5064
|
+
const hasCookieSession = !isCookielessSession(session) && session.sessionId !== "" && session.csrfToken !== "";
|
|
5065
|
+
if (refresh === "" && !hasCookieSession) {
|
|
5066
|
+
return [
|
|
5067
|
+
`Warning: no refresh token is stored for this identity, so nothing was revoked on ${base}; only the local copy was removed.`
|
|
5068
|
+
];
|
|
5069
|
+
}
|
|
5070
|
+
const [refreshFailure, sessionFailure] = await Promise.all([
|
|
5071
|
+
refresh === "" ? void 0 : revokeRefreshToken(base, refresh),
|
|
5072
|
+
hasCookieSession ? endCookieSession(base, session) : void 0
|
|
5073
|
+
]);
|
|
5074
|
+
const notes = [];
|
|
5075
|
+
if (refreshFailure !== void 0) {
|
|
5076
|
+
notes.push(
|
|
5077
|
+
`Warning: could not revoke the refresh token on ${base} (${refreshFailure}); it stays valid server-side until it expires.`
|
|
5078
|
+
);
|
|
5079
|
+
}
|
|
5080
|
+
if (sessionFailure !== void 0) {
|
|
5081
|
+
notes.push(
|
|
5082
|
+
`Warning: could not end the browser session on ${base} (${sessionFailure}); it stays valid server-side until it expires.`
|
|
5083
|
+
);
|
|
5084
|
+
}
|
|
5085
|
+
return notes;
|
|
5086
|
+
}
|
|
5087
|
+
async function revokeRefreshToken(base, refresh) {
|
|
5088
|
+
return withTimeout(async (signal) => {
|
|
5089
|
+
const res = await fetch(`${base}/jwt/api/token/revoke/`, {
|
|
5090
|
+
method: "POST",
|
|
5091
|
+
headers: { "Content-Type": "application/json" },
|
|
5092
|
+
body: JSON.stringify({ refresh }),
|
|
5093
|
+
signal
|
|
5094
|
+
});
|
|
5095
|
+
await discardBody(res);
|
|
5096
|
+
return res.status === 204 ? void 0 : `HTTP ${res.status}`;
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
async function endCookieSession(base, session) {
|
|
5100
|
+
return withTimeout(async (signal) => {
|
|
5101
|
+
const res = await fetch(`${base}/api/accounts/logout/`, {
|
|
5055
5102
|
method: "POST",
|
|
5056
5103
|
headers: {
|
|
5057
5104
|
Cookie: `sessionid=${session.sessionId}; csrftoken=${session.csrfToken}`,
|
|
5058
|
-
"X-CSRFToken": session.csrfToken
|
|
5105
|
+
"X-CSRFToken": session.csrfToken,
|
|
5106
|
+
Referer: `${base}/`
|
|
5059
5107
|
},
|
|
5060
|
-
|
|
5108
|
+
redirect: "manual",
|
|
5109
|
+
signal
|
|
5061
5110
|
});
|
|
5111
|
+
await discardBody(res);
|
|
5112
|
+
return res.status >= 200 && res.status < 400 ? void 0 : `HTTP ${res.status}`;
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
async function withTimeout(run) {
|
|
5116
|
+
const controller = new AbortController();
|
|
5117
|
+
const timeout = setTimeout(() => controller.abort(), REVOKE_TIMEOUT_MS);
|
|
5118
|
+
try {
|
|
5119
|
+
return await run(controller.signal);
|
|
5062
5120
|
} catch {
|
|
5121
|
+
return controller.signal.aborted ? "timed out" : "network error";
|
|
5063
5122
|
} finally {
|
|
5064
5123
|
clearTimeout(timeout);
|
|
5065
5124
|
}
|
|
5066
5125
|
}
|
|
5126
|
+
async function discardBody(res) {
|
|
5127
|
+
try {
|
|
5128
|
+
await res.text();
|
|
5129
|
+
} catch {
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5067
5132
|
|
|
5068
5133
|
// src/commands/login.ts
|
|
5069
5134
|
import { Option } from "commander";
|
|
@@ -5170,7 +5235,7 @@ function tryParseErrorCode(text2) {
|
|
|
5170
5235
|
return typeof errVal === "string" ? errVal : "";
|
|
5171
5236
|
}
|
|
5172
5237
|
async function pollForToken(host, deviceCode, options = {}) {
|
|
5173
|
-
return poll(host, PATHS.legacy.token, deviceCode, options, (text2) => {
|
|
5238
|
+
return poll(host, PATHS.legacy.token, deviceCode, options, {}, (text2) => {
|
|
5174
5239
|
const tok = parseTokenResponse(text2);
|
|
5175
5240
|
if (!tok.sessionId || !tok.csrfToken) {
|
|
5176
5241
|
throw new Error("token response missing session_id or csrf_token");
|
|
@@ -5179,7 +5244,7 @@ async function pollForToken(host, deviceCode, options = {}) {
|
|
|
5179
5244
|
});
|
|
5180
5245
|
}
|
|
5181
5246
|
async function pollForV2Token(host, deviceCode, options = {}) {
|
|
5182
|
-
return poll(host, PATHS.v2.token, deviceCode, options, (text2) => {
|
|
5247
|
+
return poll(host, PATHS.v2.token, deviceCode, options, V2_TERMINAL_HINTS, (text2) => {
|
|
5183
5248
|
const obj = parseObject(text2);
|
|
5184
5249
|
const access = typeof obj.access === "string" ? obj.access : "";
|
|
5185
5250
|
const refresh = typeof obj.refresh === "string" ? obj.refresh : "";
|
|
@@ -5189,7 +5254,11 @@ async function pollForV2Token(host, deviceCode, options = {}) {
|
|
|
5189
5254
|
return { access, refresh };
|
|
5190
5255
|
});
|
|
5191
5256
|
}
|
|
5192
|
-
|
|
5257
|
+
var V2_TERMINAL_HINTS = {
|
|
5258
|
+
access_denied: " (the login was refused, expired, or already used: run `dr login` again)",
|
|
5259
|
+
token_issuance_failed: " (the server could not issue the token: run `dr login` again to start a new login)"
|
|
5260
|
+
};
|
|
5261
|
+
async function poll(host, path11, deviceCode, options, terminalHints, onSuccess) {
|
|
5193
5262
|
const interval = options.interval ?? 2e3;
|
|
5194
5263
|
const maxWait = options.maxWait ?? 6e5;
|
|
5195
5264
|
const now = options.now ?? Date.now;
|
|
@@ -5225,7 +5294,8 @@ async function poll(host, path11, deviceCode, options, onSuccess) {
|
|
|
5225
5294
|
continue;
|
|
5226
5295
|
}
|
|
5227
5296
|
if (errCode) {
|
|
5228
|
-
|
|
5297
|
+
const hint = Object.hasOwn(terminalHints, errCode) ? terminalHints[errCode] : "";
|
|
5298
|
+
throw new Error(`token error: ${errCode}${hint}`);
|
|
5229
5299
|
}
|
|
5230
5300
|
throw new Error(`token HTTP ${res.status}: ${text2.trim()}`);
|
|
5231
5301
|
}
|
|
@@ -25578,6 +25648,62 @@ function symbolForCurrencyCode(code) {
|
|
|
25578
25648
|
return FX_CURRENCY_SYMBOLS.get(code.trim().toUpperCase()) ?? null;
|
|
25579
25649
|
}
|
|
25580
25650
|
|
|
25651
|
+
// src/commands/_currencySymbol.ts
|
|
25652
|
+
var FIXED_SIGNS_RE = /"[$€£₪]"|[$€£₪]/g;
|
|
25653
|
+
var DEFAULT_NUMBER_FORMAT = "#,###";
|
|
25654
|
+
function stripFixedCurrencySigns(format) {
|
|
25655
|
+
return format ? format.replace(FIXED_SIGNS_RE, "") : format;
|
|
25656
|
+
}
|
|
25657
|
+
function withCurrencySign(format, sign) {
|
|
25658
|
+
const cleanSign = sign.replace(/"/g, "");
|
|
25659
|
+
const literal = `"${cleanSign}${/^[A-Za-z]+$/.test(cleanSign) ? " " : ""}"`;
|
|
25660
|
+
const base = format || DEFAULT_NUMBER_FORMAT;
|
|
25661
|
+
return base.split(";").map((section) => {
|
|
25662
|
+
if (section === "") return section;
|
|
25663
|
+
const leading = section.match(/^(\[[^\]]*\])*(?=.)/)?.[0] ?? "";
|
|
25664
|
+
const rest = section.slice(leading.length);
|
|
25665
|
+
const paren = rest.match(/^\((.*)\)$/);
|
|
25666
|
+
return paren ? `${leading}(${literal}${paren[1]})` : `${leading}${literal}${rest}`;
|
|
25667
|
+
}).join(";");
|
|
25668
|
+
}
|
|
25669
|
+
function parseWidgetOptions(raw) {
|
|
25670
|
+
if (raw === void 0 || raw === null || raw === "") return {};
|
|
25671
|
+
if (typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
25672
|
+
if (typeof raw !== "string") return null;
|
|
25673
|
+
try {
|
|
25674
|
+
const parsed2 = JSON.parse(raw);
|
|
25675
|
+
return typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2) ? parsed2 : null;
|
|
25676
|
+
} catch {
|
|
25677
|
+
return null;
|
|
25678
|
+
}
|
|
25679
|
+
}
|
|
25680
|
+
function readAutoCurrencySymbol(raw) {
|
|
25681
|
+
const opts = parseWidgetOptions(raw);
|
|
25682
|
+
if (opts === null) return null;
|
|
25683
|
+
return opts["autoCurrencySymbol"] === true;
|
|
25684
|
+
}
|
|
25685
|
+
function setAutoCurrencySymbol(raw, enabled) {
|
|
25686
|
+
const opts = parseWidgetOptions(raw);
|
|
25687
|
+
if (opts === null) {
|
|
25688
|
+
throw new Error("widget options is not a JSON object \u2014 refusing to rewrite it");
|
|
25689
|
+
}
|
|
25690
|
+
return JSON.stringify({ ...opts, autoCurrencySymbol: enabled });
|
|
25691
|
+
}
|
|
25692
|
+
function fixedSignOf(format) {
|
|
25693
|
+
if (typeof format !== "string") return null;
|
|
25694
|
+
const m = format.match(/^(?:\[[^\]]*\])*\(?"([^"]+)"/);
|
|
25695
|
+
const sign = m?.[1];
|
|
25696
|
+
return sign === void 0 ? null : sign.trim();
|
|
25697
|
+
}
|
|
25698
|
+
function describeCurrencySymbol(widget) {
|
|
25699
|
+
const auto = readAutoCurrencySymbol(widget["options"]);
|
|
25700
|
+
if (auto === true && widget["is_fx_aware"] === true) {
|
|
25701
|
+
return "Auto (follows the dashboard FX selector)";
|
|
25702
|
+
}
|
|
25703
|
+
const sign = fixedSignOf(widget["values_format"]);
|
|
25704
|
+
return sign === null ? "none" : `"${sign}" (fixed)`;
|
|
25705
|
+
}
|
|
25706
|
+
|
|
25581
25707
|
// src/commands/fxConfig.ts
|
|
25582
25708
|
function writeTable20(out, rows) {
|
|
25583
25709
|
if (rows.length === 0) return;
|
|
@@ -26571,6 +26697,11 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26571
26697
|
"--force-sum has no meaning with --off (turning conversion off never touches the aggregator)"
|
|
26572
26698
|
);
|
|
26573
26699
|
}
|
|
26700
|
+
if (cfg.disable && !cfg.autoSymbol) {
|
|
26701
|
+
throw new Error(
|
|
26702
|
+
"--no-auto-symbol has no meaning with --off (turning conversion off always clears the Auto currency symbol)"
|
|
26703
|
+
);
|
|
26704
|
+
}
|
|
26574
26705
|
let targetDashboards;
|
|
26575
26706
|
if (cfg.all) {
|
|
26576
26707
|
const dashboards = await list8(cfg.client, { signal: cfg.signal });
|
|
@@ -26619,6 +26750,7 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26619
26750
|
const v0 = vals !== null && vals.length === 1 ? vals[0] : null;
|
|
26620
26751
|
const val = v0 !== null && typeof v0 === "object" ? v0 : null;
|
|
26621
26752
|
const aggregator = val !== null ? String(val["aggregator"] ?? "").toUpperCase() : "";
|
|
26753
|
+
const autoSymbol = readAutoCurrencySymbol(raw["options"]);
|
|
26622
26754
|
if (cfg.disable) {
|
|
26623
26755
|
plan.push({
|
|
26624
26756
|
dashboardId: target.id,
|
|
@@ -26627,7 +26759,8 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26627
26759
|
widgetName,
|
|
26628
26760
|
aggregator,
|
|
26629
26761
|
raw,
|
|
26630
|
-
status: isAware ? "revert" : "already-off"
|
|
26762
|
+
status: isAware ? "revert" : "already-off",
|
|
26763
|
+
autoSymbol
|
|
26631
26764
|
});
|
|
26632
26765
|
continue;
|
|
26633
26766
|
}
|
|
@@ -26659,12 +26792,25 @@ async function runFxConvertWidgets(cfg) {
|
|
|
26659
26792
|
widgetName,
|
|
26660
26793
|
aggregator,
|
|
26661
26794
|
raw,
|
|
26662
|
-
status: status2
|
|
26795
|
+
status: status2,
|
|
26796
|
+
autoSymbol
|
|
26663
26797
|
});
|
|
26664
26798
|
}
|
|
26665
26799
|
}
|
|
26666
26800
|
const subjectOf = (p) => `widget ${p.widgetId} "${p.widgetName}" (dashboard ${p.dashboardId} "${p.dashboardName}")`;
|
|
26667
|
-
const
|
|
26801
|
+
const symbolNote = (p) => {
|
|
26802
|
+
if (p.status === "convert" || p.status === "convert-sum") {
|
|
26803
|
+
return cfg.autoSymbol ? " (+ Auto currency symbol)" : "";
|
|
26804
|
+
}
|
|
26805
|
+
if (p.status === "already" && p.autoSymbol === false) {
|
|
26806
|
+
return ` (fixed currency symbol \u2014 \`dr widgets set-currency-symbol ${p.widgetId} -d ${p.dashboardId} --auto\` to follow the FX selector)`;
|
|
26807
|
+
}
|
|
26808
|
+
if (p.status === "revert" && p.autoSymbol === true) {
|
|
26809
|
+
return " (Auto currency symbol \u2192 none)";
|
|
26810
|
+
}
|
|
26811
|
+
return "";
|
|
26812
|
+
};
|
|
26813
|
+
const describeEntry = (p) => planLine(p.status, subjectOf(p), p.aggregator) + symbolNote(p);
|
|
26668
26814
|
const targets = plan.filter((p) => isActionable(p.status, cfg.disable));
|
|
26669
26815
|
const needsRole = plan.filter((p) => p.status === "needs-fx-role").length;
|
|
26670
26816
|
const summary = () => {
|
|
@@ -26680,6 +26826,7 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26680
26826
|
writeJSONIndented(cfg.out, {
|
|
26681
26827
|
dry_run: true,
|
|
26682
26828
|
direction: cfg.disable ? "off" : "on",
|
|
26829
|
+
auto_symbol: cfg.disable ? false : cfg.autoSymbol,
|
|
26683
26830
|
scanned,
|
|
26684
26831
|
not_eligible: notEligible,
|
|
26685
26832
|
plan: plan.map((p) => ({
|
|
@@ -26688,7 +26835,8 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26688
26835
|
widget_id: p.widgetId,
|
|
26689
26836
|
name: p.widgetName,
|
|
26690
26837
|
aggregator: p.aggregator,
|
|
26691
|
-
status: p.status
|
|
26838
|
+
status: p.status,
|
|
26839
|
+
auto_currency_symbol: p.autoSymbol
|
|
26692
26840
|
})),
|
|
26693
26841
|
unreadable
|
|
26694
26842
|
});
|
|
@@ -26728,10 +26876,19 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26728
26876
|
const v0 = vals[0];
|
|
26729
26877
|
body["vals"] = [{ ...v0, aggregator: "SUM" }];
|
|
26730
26878
|
}
|
|
26879
|
+
let wantAuto = null;
|
|
26880
|
+
if (want && cfg.autoSymbol) wantAuto = true;
|
|
26881
|
+
else if (!want && p.autoSymbol === true) wantAuto = false;
|
|
26882
|
+
if (wantAuto !== null) body["options"] = setAutoCurrencySymbol(p.raw["options"], wantAuto);
|
|
26731
26883
|
const res = await update6(cfg.client, p.dashboardId, p.widgetId, body, {
|
|
26732
26884
|
signal: cfg.signal
|
|
26733
26885
|
});
|
|
26734
26886
|
assertFxAwareEcho(res["is_fx_aware"], want, "widget");
|
|
26887
|
+
if (wantAuto !== null && readAutoCurrencySymbol(res["options"]) !== wantAuto) {
|
|
26888
|
+
throw new Error(
|
|
26889
|
+
`server did not keep options.autoCurrencySymbol=${wantAuto} (the Auto currency symbol)`
|
|
26890
|
+
);
|
|
26891
|
+
}
|
|
26735
26892
|
results.push({
|
|
26736
26893
|
dashboard_id: p.dashboardId,
|
|
26737
26894
|
widget_id: p.widgetId,
|
|
@@ -26740,8 +26897,10 @@ ${needsRole} skipped for a missing fx_role \u2014 ${FX_ROLE_HINT}.` : "");
|
|
|
26740
26897
|
});
|
|
26741
26898
|
if (!cfg.asJson) {
|
|
26742
26899
|
for (const line of unreadableLines(unreadable, "dashboard")) cfg.out.write(line + "\n");
|
|
26743
|
-
cfg.out.write(
|
|
26744
|
-
|
|
26900
|
+
cfg.out.write(
|
|
26901
|
+
`\u2713 ${describeConvertStatus(p.status, subjectOf(p), p.aggregator)}${symbolNote(p)}
|
|
26902
|
+
`
|
|
26903
|
+
);
|
|
26745
26904
|
}
|
|
26746
26905
|
} catch (err) {
|
|
26747
26906
|
if (cfg.signal.aborted) throw err;
|
|
@@ -27080,11 +27239,14 @@ function registerFxConfigCommands(fx) {
|
|
|
27080
27239
|
addServerFlag(convertFunctionsCmd);
|
|
27081
27240
|
addAccountFlag(convertFunctionsCmd);
|
|
27082
27241
|
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."
|
|
27242
|
+
"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
27243
|
).option("--dashboard <id>", "Convert widgets on this dashboard only").option("--all", "Convert widgets on every dashboard", false).option(
|
|
27085
27244
|
"--force-sum",
|
|
27086
27245
|
"Also convert non-SUM widgets by switching their aggregator to SUM",
|
|
27087
27246
|
false
|
|
27247
|
+
).option(
|
|
27248
|
+
"--no-auto-symbol",
|
|
27249
|
+
"Keep each converted widget's stored number format (its fixed currency sign, if any) instead of switching it to the Auto currency symbol"
|
|
27088
27250
|
).option(
|
|
27089
27251
|
"--off",
|
|
27090
27252
|
"Reverse direction: turn currency conversion OFF on every currency-aware widget in scope",
|
|
@@ -27100,6 +27262,7 @@ function registerFxConfigCommands(fx) {
|
|
|
27100
27262
|
all: opts.all === true,
|
|
27101
27263
|
forceSum: opts.forceSum === true,
|
|
27102
27264
|
disable: opts.off === true,
|
|
27265
|
+
autoSymbol: opts.autoSymbol !== false,
|
|
27103
27266
|
confirm: opts.confirm === true,
|
|
27104
27267
|
asJson: opts.json === true,
|
|
27105
27268
|
signal: setup.signal,
|
|
@@ -41231,6 +41394,10 @@ async function runWidgetsGet(cfg) {
|
|
|
41231
41394
|
cfg.out.write(`chart_type: ${found["chart_type"] ?? ""}
|
|
41232
41395
|
`);
|
|
41233
41396
|
cfg.out.write(`template_id: ${found["template_id"] ?? "(none)"}
|
|
41397
|
+
`);
|
|
41398
|
+
cfg.out.write(`fx_aware: ${found["is_fx_aware"] === true ? "yes" : "no"}
|
|
41399
|
+
`);
|
|
41400
|
+
cfg.out.write(`fx_symbol: ${describeCurrencySymbol(found)}
|
|
41234
41401
|
`);
|
|
41235
41402
|
cfg.out.write("\n(use --json for the full widget body)\n");
|
|
41236
41403
|
}
|
|
@@ -41309,6 +41476,67 @@ async function runWidgetsUpdate(cfg) {
|
|
|
41309
41476
|
});
|
|
41310
41477
|
printWidgetMutation(cfg.out, updated, "Updated", cfg.asJson);
|
|
41311
41478
|
}
|
|
41479
|
+
async function runWidgetsSetCurrencySymbol(cfg) {
|
|
41480
|
+
if (cfg.dashboardId <= 0) throw new Error("--dashboard is required");
|
|
41481
|
+
if (cfg.id <= 0) throw new Error("widget id must be positive");
|
|
41482
|
+
const sign = cfg.mode === "sign" ? (cfg.sign ?? "").trim() : "";
|
|
41483
|
+
if (cfg.mode === "sign" && (sign === "" || sign.length > 5 || sign.includes('"'))) {
|
|
41484
|
+
throw new Error(
|
|
41485
|
+
"--sign needs a currency sign of 1\u20135 characters without quotes (e.g. $, \u20AC, \xA3, \u20AA or a code such as CHF)"
|
|
41486
|
+
);
|
|
41487
|
+
}
|
|
41488
|
+
const all = await listRaw3(cfg.client, cfg.dashboardId, { signal: cfg.signal });
|
|
41489
|
+
const found = all.find((w) => w["id"] === cfg.id);
|
|
41490
|
+
if (found === void 0) {
|
|
41491
|
+
throw new Error(`widget ${cfg.id} not found on dashboard ${cfg.dashboardId}`);
|
|
41492
|
+
}
|
|
41493
|
+
if (cfg.mode === "auto" && found["is_fx_aware"] !== true) {
|
|
41494
|
+
throw new Error(
|
|
41495
|
+
`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`
|
|
41496
|
+
);
|
|
41497
|
+
}
|
|
41498
|
+
const wantAuto = cfg.mode === "auto";
|
|
41499
|
+
const current = typeof found["values_format"] === "string" ? found["values_format"] : null;
|
|
41500
|
+
const body = {
|
|
41501
|
+
...found,
|
|
41502
|
+
options: setAutoCurrencySymbol(found["options"], wantAuto)
|
|
41503
|
+
};
|
|
41504
|
+
if (cfg.mode === "none") {
|
|
41505
|
+
body["values_format"] = stripFixedCurrencySigns(current) || null;
|
|
41506
|
+
} else if (cfg.mode === "sign") {
|
|
41507
|
+
body["values_format"] = withCurrencySign(stripFixedCurrencySigns(current), sign);
|
|
41508
|
+
}
|
|
41509
|
+
const res = await update6(cfg.client, cfg.dashboardId, cfg.id, body, {
|
|
41510
|
+
signal: cfg.signal
|
|
41511
|
+
});
|
|
41512
|
+
if (readAutoCurrencySymbol(res["options"]) !== wantAuto) {
|
|
41513
|
+
throw new Error(
|
|
41514
|
+
`server did not keep options.autoCurrencySymbol=${wantAuto} (the Auto currency symbol)`
|
|
41515
|
+
);
|
|
41516
|
+
}
|
|
41517
|
+
if (cfg.mode !== "auto" && (res["values_format"] ?? null) !== body["values_format"]) {
|
|
41518
|
+
throw new Error(
|
|
41519
|
+
`server did not keep values_format ${JSON.stringify(body["values_format"])} (came back ${JSON.stringify(res["values_format"] ?? null)})`
|
|
41520
|
+
);
|
|
41521
|
+
}
|
|
41522
|
+
if (cfg.asJson) {
|
|
41523
|
+
writeJSONIndented(cfg.out, {
|
|
41524
|
+
id: res["id"] ?? cfg.id,
|
|
41525
|
+
name: res["name"] ?? found["name"] ?? null,
|
|
41526
|
+
is_fx_aware: res["is_fx_aware"] === true,
|
|
41527
|
+
auto_currency_symbol: wantAuto,
|
|
41528
|
+
values_format: res["values_format"] ?? null,
|
|
41529
|
+
currency_symbol: describeCurrencySymbol(res)
|
|
41530
|
+
});
|
|
41531
|
+
return;
|
|
41532
|
+
}
|
|
41533
|
+
cfg.out.write(
|
|
41534
|
+
`widget ${cfg.id} "${String(found["name"] ?? "")}": currency symbol \u2192 ${describeCurrencySymbol(res)}
|
|
41535
|
+
`
|
|
41536
|
+
);
|
|
41537
|
+
cfg.out.write(`values_format: ${res["values_format"] ?? "(none)"}
|
|
41538
|
+
`);
|
|
41539
|
+
}
|
|
41312
41540
|
async function runWidgetsDelete(cfg) {
|
|
41313
41541
|
if (cfg.dashboardId <= 0) throw new Error("--dashboard is required");
|
|
41314
41542
|
if (cfg.id <= 0) throw new Error("widget id must be positive");
|
|
@@ -41660,6 +41888,36 @@ function registerWidgetsCommand(root) {
|
|
|
41660
41888
|
});
|
|
41661
41889
|
addServerFlag(updateCmd);
|
|
41662
41890
|
addAccountFlag(updateCmd);
|
|
41891
|
+
const symbolCmd = widgets.command("set-currency-symbol").description(
|
|
41892
|
+
"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."
|
|
41893
|
+
).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(
|
|
41894
|
+
async (idStr, opts) => {
|
|
41895
|
+
const id = parseWidgetIdArg(idStr);
|
|
41896
|
+
const dashboardId = parseDashboardFlag(opts.dashboard);
|
|
41897
|
+
const picked = [opts.auto === true, opts.none === true, opts.sign !== void 0].filter(
|
|
41898
|
+
Boolean
|
|
41899
|
+
).length;
|
|
41900
|
+
if (picked !== 1) throw new Error("pass exactly one of --auto, --none or --sign <S>");
|
|
41901
|
+
const mode = opts.auto === true ? "auto" : opts.none === true ? "none" : "sign";
|
|
41902
|
+
const setup = await setupCall(opts.server, opts.account);
|
|
41903
|
+
try {
|
|
41904
|
+
await runWidgetsSetCurrencySymbol({
|
|
41905
|
+
client: setup.client,
|
|
41906
|
+
dashboardId,
|
|
41907
|
+
id,
|
|
41908
|
+
mode,
|
|
41909
|
+
sign: opts.sign,
|
|
41910
|
+
asJson: opts.json === true,
|
|
41911
|
+
signal: setup.signal,
|
|
41912
|
+
out: process.stdout
|
|
41913
|
+
});
|
|
41914
|
+
} finally {
|
|
41915
|
+
setup.cleanup();
|
|
41916
|
+
}
|
|
41917
|
+
}
|
|
41918
|
+
);
|
|
41919
|
+
addServerFlag(symbolCmd);
|
|
41920
|
+
addAccountFlag(symbolCmd);
|
|
41663
41921
|
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
41922
|
const id = parseWidgetIdArg(idStr);
|
|
41665
41923
|
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.100",
|
|
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.100",
|
|
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",
|