@datarails-ai/dr-cli 1.6.100 → 1.6.102
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 +16 -11
- package/dist/cli.js +429 -46
- package/dist/scopes-map.json +17 -1
- package/dist/skills/dr-cli/SKILL.md +4 -1
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +23 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -162,19 +162,23 @@ JWT refresh is automatic. Re-run `login` only when the refresh token itself
|
|
|
162
162
|
dies (days of idle, password change, admin revoke). Never paste the
|
|
163
163
|
session/JWT — treat them like a password.
|
|
164
164
|
|
|
165
|
-
**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
**Device flow and credential scope.** `dr login` uses the monolith's
|
|
166
|
+
`/oauth/v2/` device flow. The consent page lets you pick the credential's scope
|
|
167
|
+
(below) before "Allow", and the login stores a cookie-less JWT pair, with no
|
|
168
|
+
session, refreshed at the external refresh endpoint. The page offers the choice
|
|
169
|
+
only where the org enforces it; elsewhere the token is unrestricted. This works
|
|
170
|
+
with a credential file too (`DR_CREDENTIALS_FILE`), which records the login as
|
|
171
|
+
`"interactiveLogin": true` so that `dr login` and `dr logout` still accept it.
|
|
172
|
+
|
|
173
|
+
`DR_OAUTH_FLOW=legacy` runs the old `/oauth/device/code/` flow instead, for a
|
|
174
|
+
server that does not serve `/oauth/v2/` yet (a 404 from the v2 code endpoint
|
|
175
|
+
says so). A legacy token carries **no** credential scope, whatever the person
|
|
176
|
+
would have picked, and the login prints a line saying so. `v2` or unset selects
|
|
177
|
+
the default; any other value is refused. Log in again to switch an identity
|
|
178
|
+
between flows.
|
|
175
179
|
|
|
176
180
|
```
|
|
177
|
-
DR_OAUTH_FLOW=
|
|
181
|
+
DR_OAUTH_FLOW=legacy dr login --server US # PowerShell: $env:DR_OAUTH_FLOW="legacy"; dr login --server US
|
|
178
182
|
```
|
|
179
183
|
|
|
180
184
|
### Credential scope
|
|
@@ -393,6 +397,7 @@ dr skill print <name> # print an embedded skill's SKILL.md
|
|
|
393
397
|
| Data models | `data-models list/get` |
|
|
394
398
|
| Datamappers | `datamappers list/get/create/update/delete` |
|
|
395
399
|
| Dashboards | `dashboards list/get/create/delete/prepare`, `dashboards set-date-picker <id> --mode <preset> \| --year YYYY \| --quarter YYYY-Qn \| --month YYYY-MM \| --day YYYY-MM-DD` |
|
|
400
|
+
| Folders | `folders list/create`, `folders reorder --container <root\|id> (--items folder:12,entity:87 \| --move <kind:id> --to-index <n>)`, `folders move <id> --parent <root\|id>` (dashboard / story picker tree, flag-gated; reorder and move are two-step confirm; a dashboard moves with `dashboards set-folder`) |
|
|
396
401
|
| Functions | `functions list/get/create/delete` |
|
|
397
402
|
| Dynamic ranges | `dynamic-ranges list/get/data/create/delete` |
|
|
398
403
|
| Lookups (LUTs) | `lut list/create/update/delete/reorder/unmapped` (alias `lookups`) |
|
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.102",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -40,7 +40,7 @@ var package_default = {
|
|
|
40
40
|
postinstall: "node scripts/postinstall.mjs"
|
|
41
41
|
},
|
|
42
42
|
dependencies: {
|
|
43
|
-
"@datarails-ai/dr-appspace-cli": "0.10.
|
|
43
|
+
"@datarails-ai/dr-appspace-cli": "0.10.22",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
45
45
|
"@napi-rs/keyring": "^1.3.0",
|
|
46
46
|
commander: "^12.1.0",
|
|
@@ -69,7 +69,7 @@ var package_default = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
// src/buildInfo.ts
|
|
72
|
-
var BUILD_TIME = "2026-10-
|
|
72
|
+
var BUILD_TIME = "2026-10-07T11:02:43.513Z";
|
|
73
73
|
function formatBuildTime(iso) {
|
|
74
74
|
if (iso === "") return "";
|
|
75
75
|
const d = new Date(iso);
|
|
@@ -336,6 +336,13 @@ var credentialsFileSchema = z.object({
|
|
|
336
336
|
// would rewrite live token material after every successful command.
|
|
337
337
|
sessionId: z.string().min(1).optional(),
|
|
338
338
|
csrfToken: z.string().min(1).optional(),
|
|
339
|
+
// DR-53844: set by a v2 `dr login`, whose pair comes with no cookie, so the
|
|
340
|
+
// file still reads as a person's login — `dr login` may replace it and
|
|
341
|
+
// `dr logout` may remove it — rather than as one provisioned externally.
|
|
342
|
+
// Written by the CLI only. `z.boolean()`, not `z.literal(true)`: a
|
|
343
|
+
// top-level rejection fails the whole file (see `apiKeyExpiresAt`), and a
|
|
344
|
+
// hand-edited `false` must read as absent, not lock the operator out.
|
|
345
|
+
interactiveLogin: z.boolean().optional(),
|
|
339
346
|
// Validated, not just typed: `KeyringStore` turns this straight into a Date
|
|
340
347
|
// and later calls `toISOString()` on it, which throws on an Invalid Date.
|
|
341
348
|
// `datetime()` rejects a bad calendar date (2026-02-30, 2026-13-40) on its
|
|
@@ -567,11 +574,18 @@ var UnknownAccountError = class extends Error {
|
|
|
567
574
|
var SCOPE_DENIED_CODE = 40301;
|
|
568
575
|
var SCOPE_UNDECLARED_CODE = 40302;
|
|
569
576
|
var ScopeDeniedError = class extends Error {
|
|
570
|
-
|
|
577
|
+
/**
|
|
578
|
+
* @param identity The identity whose ceiling refused, when it is NOT necessarily the one
|
|
579
|
+
* the command line selected — `dr login-as --as <operator>` mints with the operator's
|
|
580
|
+
* credential, not the `--server`/`--account` one. Named, the message points `dr whoami`
|
|
581
|
+
* at it; omitted, "this identity" is the one the reader just ran the command as.
|
|
582
|
+
*/
|
|
583
|
+
constructor(method, path11, code, required2, identity2) {
|
|
571
584
|
super("");
|
|
572
585
|
this.method = method;
|
|
573
586
|
this.path = path11;
|
|
574
587
|
this.code = code;
|
|
588
|
+
this.identity = identity2;
|
|
575
589
|
this.name = "ScopeDeniedError";
|
|
576
590
|
this.required = required2;
|
|
577
591
|
this.message = this.render();
|
|
@@ -579,6 +593,7 @@ var ScopeDeniedError = class extends Error {
|
|
|
579
593
|
method;
|
|
580
594
|
path;
|
|
581
595
|
code;
|
|
596
|
+
identity;
|
|
582
597
|
/** `resource:action` the command needs, joined when it needs more than one. Filled in by
|
|
583
598
|
* `cli.ts` after construction (see the class docstring); `undefined` until then, and for
|
|
584
599
|
* a command the local map says nothing about. */
|
|
@@ -597,6 +612,10 @@ var ScopeDeniedError = class extends Error {
|
|
|
597
612
|
return `${this.method} ${this.path}: this endpoint is not yet scope-declared on the server; report to #dr-cli`;
|
|
598
613
|
}
|
|
599
614
|
const needs = this.required === void 0 || this.required === "" ? "" : ` (needs ${this.required})`;
|
|
615
|
+
if (this.identity !== void 0) {
|
|
616
|
+
const { server, account } = this.identity;
|
|
617
|
+
return `${this.method} ${this.path}: the credential scope of ${account} on ${server} does not allow it${needs}; run 'dr whoami --server ${server} --account ${account}' to see the scopes it carries`;
|
|
618
|
+
}
|
|
600
619
|
return `${this.method} ${this.path}: this identity's credential scope does not allow it${needs}; run 'dr whoami' to see the scopes this identity carries`;
|
|
601
620
|
}
|
|
602
621
|
};
|
|
@@ -2657,7 +2676,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2657
2676
|
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2658
2677
|
},
|
|
2659
2678
|
// The env var's NAME only — the resolved key never enters any store.
|
|
2660
|
-
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar }
|
|
2679
|
+
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar },
|
|
2680
|
+
...creds.interactiveLogin === true && { interactiveLogin: true }
|
|
2661
2681
|
});
|
|
2662
2682
|
}
|
|
2663
2683
|
static slotOf(creds, account) {
|
|
@@ -2694,7 +2714,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2694
2714
|
// Set-if-present, never deleted on a blob that lost it (the
|
|
2695
2715
|
// `apiKeyExpiresAt` rule): it is the identity's only credential pointer,
|
|
2696
2716
|
// and a meta rewrite must not be able to brick the file.
|
|
2697
|
-
...apiKeyEnvVar !== "" && { apiKeyEnvVar }
|
|
2717
|
+
...apiKeyEnvVar !== "" && { apiKeyEnvVar },
|
|
2718
|
+
...parsed2.interactiveLogin === true && { interactiveLogin: true }
|
|
2698
2719
|
};
|
|
2699
2720
|
}
|
|
2700
2721
|
async get(_service, account) {
|
|
@@ -2788,6 +2809,8 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2788
2809
|
if (meta.accessApiKeyFingerprint === void 0) delete next.accessApiKeyFingerprint;
|
|
2789
2810
|
else next.accessApiKeyFingerprint = meta.accessApiKeyFingerprint;
|
|
2790
2811
|
if (meta.apiKeyEnvVar !== void 0) next.apiKeyEnvVar = meta.apiKeyEnvVar;
|
|
2812
|
+
if (meta.interactiveLogin === true) next.interactiveLogin = true;
|
|
2813
|
+
else delete next.interactiveLogin;
|
|
2791
2814
|
break;
|
|
2792
2815
|
}
|
|
2793
2816
|
}
|
|
@@ -3659,6 +3682,11 @@ var PROD_GATE_TIER_B = /* @__PURE__ */ new Set([
|
|
|
3659
3682
|
// cash (dr cash …). rules delete / delete-all / hidden-rules delete → Tier A.
|
|
3660
3683
|
"cash-rule|update",
|
|
3661
3684
|
"cash-rules|reorder",
|
|
3685
|
+
// dashboard / story folders (dr folders reorder, dr folders move). Reorder is
|
|
3686
|
+
// per-user (only the caller's own picker order changes) and a move is undone by
|
|
3687
|
+
// another move.
|
|
3688
|
+
"folder-container|reorder",
|
|
3689
|
+
"folder|move",
|
|
3662
3690
|
"cash-hidden-rules|upsert",
|
|
3663
3691
|
"cash-ai-rules|trigger",
|
|
3664
3692
|
// metrics (dr metrics …). delete / dimension|category delete → A.
|
|
@@ -4290,9 +4318,17 @@ async function refuseIfProvisionedExternally(action, opts) {
|
|
|
4290
4318
|
`cannot ${action}: the credentials in ${path11} are an API key. Run \`dr login --api-key\` to replace the key (and update its \`apiKeyExpiresAt\` in that file, if set \u2014 nothing derives that date, and a stale one warns "expired" on every command), or \`dr logout\` to remove it and log in with a browser instead \u2014 it cannot be read back afterwards, so keep a copy if it is still needed.`
|
|
4291
4319
|
);
|
|
4292
4320
|
}
|
|
4293
|
-
if (creds.
|
|
4321
|
+
if (creds.apiKeyEnvVar !== void 0 && opts?.allowApiKey !== true) {
|
|
4322
|
+
throw new Error(
|
|
4323
|
+
`cannot ${action}: the credentials in ${path11} name the environment variable ${creds.apiKeyEnvVar} as their API key, which is what later commands would authenticate as. Remove that file (\`dr logout\`) to log in with a browser instead.`
|
|
4324
|
+
);
|
|
4325
|
+
}
|
|
4326
|
+
if (isInteractiveFileLogin(creds)) return;
|
|
4294
4327
|
throw provisionedExternallyError(path11, action);
|
|
4295
4328
|
}
|
|
4329
|
+
function isInteractiveFileLogin(creds) {
|
|
4330
|
+
return creds.sessionId !== void 0 && creds.sessionId !== "" || creds.interactiveLogin === true;
|
|
4331
|
+
}
|
|
4296
4332
|
async function refuseLoginOverStoredApiKey(store, serverKey, email) {
|
|
4297
4333
|
let existing;
|
|
4298
4334
|
try {
|
|
@@ -4323,7 +4359,7 @@ async function refuseImpersonationWithCredentialFile() {
|
|
|
4323
4359
|
`cannot ${action}: the credentials in ${path11} are an API key, and an API-key token cannot mint further credentials. Impersonating needs a browser login, so remove that file (\`dr logout\`) and run \`dr login --server <key>\` \u2014 the key cannot be read back afterwards, so keep a copy if it is still needed.`
|
|
4324
4360
|
);
|
|
4325
4361
|
}
|
|
4326
|
-
if (creds
|
|
4362
|
+
if (!isInteractiveFileLogin(creds)) {
|
|
4327
4363
|
throw provisionedExternallyError(path11, action);
|
|
4328
4364
|
}
|
|
4329
4365
|
throw new Error(
|
|
@@ -5141,8 +5177,8 @@ var OAUTH_FLOW_ENV = "DR_OAUTH_FLOW";
|
|
|
5141
5177
|
function oauthFlowFromEnv(env = process.env) {
|
|
5142
5178
|
const raw = (env[OAUTH_FLOW_ENV] ?? "").trim();
|
|
5143
5179
|
const value = raw.toLowerCase();
|
|
5144
|
-
if (value === "" || value === "
|
|
5145
|
-
if (value === "
|
|
5180
|
+
if (value === "" || value === "v2") return "v2";
|
|
5181
|
+
if (value === "legacy") return "legacy";
|
|
5146
5182
|
throw new Error(`${OAUTH_FLOW_ENV} must be "legacy" or "v2", got "${raw}"`);
|
|
5147
5183
|
}
|
|
5148
5184
|
var PATHS = {
|
|
@@ -5152,7 +5188,7 @@ var PATHS = {
|
|
|
5152
5188
|
var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:";
|
|
5153
5189
|
async function requestDeviceCode(host, options) {
|
|
5154
5190
|
const base = host.replace(/\/+$/, "");
|
|
5155
|
-
const flow = options?.flow ?? "
|
|
5191
|
+
const flow = options?.flow ?? "v2";
|
|
5156
5192
|
const paths = PATHS[flow];
|
|
5157
5193
|
const body = new URLSearchParams({ client_id: CLIENT_ID, scope: paths.scope });
|
|
5158
5194
|
const res = await fetch(`${base}${paths.code}`, {
|
|
@@ -5163,7 +5199,12 @@ async function requestDeviceCode(host, options) {
|
|
|
5163
5199
|
});
|
|
5164
5200
|
const text2 = await res.text();
|
|
5165
5201
|
if (!res.ok) {
|
|
5166
|
-
|
|
5202
|
+
let hint = "";
|
|
5203
|
+
if (flow === "v2" && res.status === 429) {
|
|
5204
|
+
hint = " (too many logins from this address: wait a minute and run `dr login` again)";
|
|
5205
|
+
} else if (flow === "v2" && res.status === 404) {
|
|
5206
|
+
hint = ` (this server does not serve the v2 device flow yet: set ${OAUTH_FLOW_ENV}=legacy to log in through the old one, whose token carries no credential scope)`;
|
|
5207
|
+
}
|
|
5167
5208
|
throw new Error(`device/code failed: ${res.status} ${text2.trim()}${hint}`);
|
|
5168
5209
|
}
|
|
5169
5210
|
let raw;
|
|
@@ -5530,11 +5571,6 @@ function registerLoginCommand(root) {
|
|
|
5530
5571
|
await refuseIfProvisionedExternally("log in");
|
|
5531
5572
|
const flow = oauthFlowFromEnv();
|
|
5532
5573
|
const fileStore = await resolveFileStore(opts.server);
|
|
5533
|
-
if (flow === "v2" && fileStore !== void 0) {
|
|
5534
|
-
throw new Error(
|
|
5535
|
-
`DR_OAUTH_FLOW=v2 cannot log into the credential file ${fileStore.path} yet; unset DR_CREDENTIALS_FILE to log in through the keychain, or unset DR_OAUTH_FLOW.`
|
|
5536
|
-
);
|
|
5537
|
-
}
|
|
5538
5574
|
let serverKey = fileStore?.key ?? (opts.server ?? "").trim();
|
|
5539
5575
|
if (fileStore === void 0) {
|
|
5540
5576
|
if (serverKey === "") {
|
|
@@ -5643,13 +5679,14 @@ async function runLogin(cfg) {
|
|
|
5643
5679
|
await refuseIfProvisionedExternally("log in");
|
|
5644
5680
|
const host = cfg.host ?? resolve(cfg.serverKey).host;
|
|
5645
5681
|
const timeoutMs = cfg.timeoutMs !== void 0 && cfg.timeoutMs > 0 ? cfg.timeoutMs : DEFAULT_LOGIN_TIMEOUT_MS;
|
|
5646
|
-
const flow = cfg.flow
|
|
5682
|
+
const flow = cfg.flow;
|
|
5647
5683
|
const controller = new AbortController();
|
|
5648
5684
|
const overallTimer = setTimeout(() => controller.abort(), timeoutMs + ABORT_SLACK_MS);
|
|
5649
5685
|
try {
|
|
5650
|
-
if (flow === "
|
|
5651
|
-
cfg.out.write(
|
|
5652
|
-
|
|
5686
|
+
if (flow === "legacy") {
|
|
5687
|
+
cfg.out.write(
|
|
5688
|
+
"Using the legacy device flow (DR_OAUTH_FLOW=legacy): its token carries no credential scope.\n"
|
|
5689
|
+
);
|
|
5653
5690
|
}
|
|
5654
5691
|
const dc = await requestDeviceCode(host, { signal: controller.signal, flow });
|
|
5655
5692
|
cfg.out.write(`Opening browser to authenticate with Datarails (${cfg.serverKey})...
|
|
@@ -6059,7 +6096,17 @@ async function resolveTargetUserId(client, targetEmail, signal) {
|
|
|
6059
6096
|
`no user you can log in as matches "${targetEmail}".${near} Use --user-id <n> to impersonate by id.`
|
|
6060
6097
|
);
|
|
6061
6098
|
}
|
|
6062
|
-
|
|
6099
|
+
var LOGINAS_REQUIRED_SCOPE = "support-manager:write";
|
|
6100
|
+
function friendlyLoginAsError(err, targetEmail, explicitOperator) {
|
|
6101
|
+
if (err.code === "insufficient_scope" && err.httpStatus === 403) {
|
|
6102
|
+
return new ScopeDeniedError(
|
|
6103
|
+
"POST",
|
|
6104
|
+
"/jwt/api/loginas/",
|
|
6105
|
+
SCOPE_DENIED_CODE,
|
|
6106
|
+
LOGINAS_REQUIRED_SCOPE,
|
|
6107
|
+
explicitOperator
|
|
6108
|
+
);
|
|
6109
|
+
}
|
|
6063
6110
|
switch (err.code) {
|
|
6064
6111
|
case "not_permitted":
|
|
6065
6112
|
return new Error(`You are not permitted to impersonate ${targetEmail}.`);
|
|
@@ -6119,7 +6166,10 @@ async function runLoginAs(cfg) {
|
|
|
6119
6166
|
signal: cfg.signal
|
|
6120
6167
|
});
|
|
6121
6168
|
} catch (err) {
|
|
6122
|
-
if (err instanceof LoginAsError)
|
|
6169
|
+
if (err instanceof LoginAsError) {
|
|
6170
|
+
const explicitOperator = cfg.operatorFlag !== void 0 && cfg.operatorFlag !== "" ? { server: cfg.serverKey, account: operatorEmail } : void 0;
|
|
6171
|
+
throw friendlyLoginAsError(err, cfg.targetEmail, explicitOperator);
|
|
6172
|
+
}
|
|
6123
6173
|
throw err;
|
|
6124
6174
|
}
|
|
6125
6175
|
let storageEmail = cfg.targetEmail;
|
|
@@ -14346,8 +14396,15 @@ function registerDashboardsCommand(root) {
|
|
|
14346
14396
|
addAccountFlag(setFolderCmd);
|
|
14347
14397
|
}
|
|
14348
14398
|
|
|
14399
|
+
// src/commands/folders.ts
|
|
14400
|
+
import { createHash as createHash3 } from "crypto";
|
|
14401
|
+
|
|
14349
14402
|
// src/api/folders.ts
|
|
14350
14403
|
var FOLDER_ENTITY_TYPES = ["dashboard", "story"];
|
|
14404
|
+
var FOLDER_FLAGS = {
|
|
14405
|
+
dashboard: "dashboards__folders",
|
|
14406
|
+
story: "storytelling__folders"
|
|
14407
|
+
};
|
|
14351
14408
|
async function list9(client, entityType, options) {
|
|
14352
14409
|
const out = await client.getJSON(
|
|
14353
14410
|
`/api/folders/?entity_type=${entityType}`,
|
|
@@ -14364,6 +14421,39 @@ async function create4(client, entityType, name, parentId, options) {
|
|
|
14364
14421
|
}
|
|
14365
14422
|
return out;
|
|
14366
14423
|
}
|
|
14424
|
+
async function reorder(client, payload, options) {
|
|
14425
|
+
const path11 = "/api/folders/reorder";
|
|
14426
|
+
const res = await client.do("POST", path11, JSON.stringify(payload), options);
|
|
14427
|
+
if (!res.ok) {
|
|
14428
|
+
throw new Error(`POST ${path11}: HTTP ${res.status}: ${await res.text()}`);
|
|
14429
|
+
}
|
|
14430
|
+
}
|
|
14431
|
+
async function move(client, folderId, parentId, options) {
|
|
14432
|
+
const path11 = `/api/folders/${folderId}`;
|
|
14433
|
+
const out = await client.patchJSON(path11, { parent_id: parentId }, options);
|
|
14434
|
+
if (out === null) {
|
|
14435
|
+
throw new Error(`unexpected empty response from PATCH ${path11}`);
|
|
14436
|
+
}
|
|
14437
|
+
return out;
|
|
14438
|
+
}
|
|
14439
|
+
var STORY_PAGE_SIZE = 100;
|
|
14440
|
+
async function listEntities(client, entityType, options) {
|
|
14441
|
+
if (entityType === "dashboard") {
|
|
14442
|
+
return await client.getJSON("/api/dashboards/", options) ?? [];
|
|
14443
|
+
}
|
|
14444
|
+
const stories = [];
|
|
14445
|
+
for (let page = 1; ; page++) {
|
|
14446
|
+
const env = await client.getJSON(
|
|
14447
|
+
`/api/storytelling/?page_size=${STORY_PAGE_SIZE}&page=${page}`,
|
|
14448
|
+
options
|
|
14449
|
+
);
|
|
14450
|
+
if (env === null || env.results.length === 0) break;
|
|
14451
|
+
stories.push(...env.results);
|
|
14452
|
+
if (env.next === null) break;
|
|
14453
|
+
}
|
|
14454
|
+
const listed = new Set(stories.map((s) => s.id));
|
|
14455
|
+
return stories.filter((s) => s.parent_story === null || s.is_roll_forwarded || !listed.has(s.parent_story)).map(({ id, name, folder_id, position }) => ({ id, name, folder_id, position }));
|
|
14456
|
+
}
|
|
14367
14457
|
|
|
14368
14458
|
// src/commands/folders.ts
|
|
14369
14459
|
async function runFoldersList(cfg) {
|
|
@@ -14373,6 +14463,235 @@ async function runFoldersList(cfg) {
|
|
|
14373
14463
|
return;
|
|
14374
14464
|
}
|
|
14375
14465
|
renderFolderTree(cfg.out, tree);
|
|
14466
|
+
const entities = await listEntities(cfg.client, cfg.entityType, {
|
|
14467
|
+
signal: cfg.signal
|
|
14468
|
+
});
|
|
14469
|
+
if (tree.length === 0 && entities.length > 0 && entities.every((e) => e.position === null)) {
|
|
14470
|
+
cfg.out.write(
|
|
14471
|
+
`
|
|
14472
|
+
folders are disabled for ${cfg.entityType} on this server (Unleash flag ${FOLDER_FLAGS[cfg.entityType]} is off) \u2014 no member order to show.
|
|
14473
|
+
`
|
|
14474
|
+
);
|
|
14475
|
+
return;
|
|
14476
|
+
}
|
|
14477
|
+
const names = /* @__PURE__ */ new Map();
|
|
14478
|
+
const walk = (n) => {
|
|
14479
|
+
names.set(n.id, n.name);
|
|
14480
|
+
for (const c of n.children ?? []) walk(c);
|
|
14481
|
+
};
|
|
14482
|
+
for (const root of tree) walk(root);
|
|
14483
|
+
for (const [containerId, members] of buildContainerMembers(tree, entities)) {
|
|
14484
|
+
cfg.out.write(`
|
|
14485
|
+
${containerLabel(containerId, names)}:
|
|
14486
|
+
`);
|
|
14487
|
+
if (members.length === 0) {
|
|
14488
|
+
cfg.out.write(" (empty)\n");
|
|
14489
|
+
continue;
|
|
14490
|
+
}
|
|
14491
|
+
const rows = members.map((m) => ({
|
|
14492
|
+
cells: [` ${m.kind}:${m.id}`, m.name, String(m.position ?? "-")]
|
|
14493
|
+
}));
|
|
14494
|
+
writeTable9(cfg.out, rows);
|
|
14495
|
+
}
|
|
14496
|
+
}
|
|
14497
|
+
function buildContainerMembers(tree, entities) {
|
|
14498
|
+
const out = /* @__PURE__ */ new Map();
|
|
14499
|
+
const add3 = (containerId, children2) => {
|
|
14500
|
+
out.set(
|
|
14501
|
+
containerId,
|
|
14502
|
+
children2.map((f) => ({
|
|
14503
|
+
kind: "folder",
|
|
14504
|
+
id: f.id,
|
|
14505
|
+
name: f.name,
|
|
14506
|
+
position: f.position ?? null
|
|
14507
|
+
}))
|
|
14508
|
+
);
|
|
14509
|
+
for (const f of children2) add3(f.id, f.children ?? []);
|
|
14510
|
+
};
|
|
14511
|
+
add3(null, tree);
|
|
14512
|
+
for (const e of entities) {
|
|
14513
|
+
const bucket = out.get(e.folder_id);
|
|
14514
|
+
if (bucket === void 0) continue;
|
|
14515
|
+
bucket.push({ kind: "entity", id: e.id, name: e.name, position: e.position });
|
|
14516
|
+
}
|
|
14517
|
+
const rank = (m) => m.position ?? Number.MAX_SAFE_INTEGER;
|
|
14518
|
+
for (const members of out.values()) {
|
|
14519
|
+
members.sort(
|
|
14520
|
+
(a, b) => rank(a) - rank(b) || (a.kind === b.kind ? 0 : a.kind === "folder" ? -1 : 1) || a.id - b.id
|
|
14521
|
+
);
|
|
14522
|
+
}
|
|
14523
|
+
return out;
|
|
14524
|
+
}
|
|
14525
|
+
var ITEM_RE = /^(folder|entity):([1-9][0-9]*)$/;
|
|
14526
|
+
function parseReorderItems(raw) {
|
|
14527
|
+
const tokens = raw.split(",").map((t) => t.trim());
|
|
14528
|
+
return tokens.map((t) => {
|
|
14529
|
+
const m = ITEM_RE.exec(t);
|
|
14530
|
+
if (m === null) {
|
|
14531
|
+
throw new Error(`invalid --items entry "${t}" (expected: folder:<id> | entity:<id>)`);
|
|
14532
|
+
}
|
|
14533
|
+
return { kind: m[1], id: Number(m[2]) };
|
|
14534
|
+
});
|
|
14535
|
+
}
|
|
14536
|
+
function parseContainerOpt(raw, flag) {
|
|
14537
|
+
if (raw === "root") return null;
|
|
14538
|
+
if (!/^[1-9][0-9]*$/.test(raw)) {
|
|
14539
|
+
throw new Error(`invalid ${flag} "${raw}" (expected: root | positive folder id)`);
|
|
14540
|
+
}
|
|
14541
|
+
return Number(raw);
|
|
14542
|
+
}
|
|
14543
|
+
function moveItem(items, item, toIndex) {
|
|
14544
|
+
const from = items.findIndex((i) => i.kind === item.kind && i.id === item.id);
|
|
14545
|
+
if (from === -1) {
|
|
14546
|
+
throw new Error(`${item.kind}:${item.id} is not in this container`);
|
|
14547
|
+
}
|
|
14548
|
+
const rest = items.filter((_, i) => i !== from).map((i) => ({ kind: i.kind, id: i.id }));
|
|
14549
|
+
rest.splice(Math.min(toIndex, rest.length), 0, { kind: item.kind, id: item.id });
|
|
14550
|
+
return rest;
|
|
14551
|
+
}
|
|
14552
|
+
function containerLabel(containerId, names) {
|
|
14553
|
+
if (containerId === null) return "root";
|
|
14554
|
+
const name = names.get(containerId);
|
|
14555
|
+
return name === void 0 ? `folder ${containerId}` : `folder ${containerId} (${name})`;
|
|
14556
|
+
}
|
|
14557
|
+
var itemKey = (i) => `${i.kind}:${i.id}`;
|
|
14558
|
+
function reorderConfirmId(entityType, containerArg, request) {
|
|
14559
|
+
const sha = createHash3("sha256").update(request).digest("hex").slice(0, 12);
|
|
14560
|
+
return `${entityType}:${containerArg}:${sha}`;
|
|
14561
|
+
}
|
|
14562
|
+
function flagOffError(entityType, err) {
|
|
14563
|
+
return new Error(
|
|
14564
|
+
`folders are not enabled for ${entityType} on this server (Unleash flag ${FOLDER_FLAGS[entityType]} is off?): ${err.message}`
|
|
14565
|
+
);
|
|
14566
|
+
}
|
|
14567
|
+
async function runFoldersReorder(cfg) {
|
|
14568
|
+
const hasItems = cfg.items !== void 0 && cfg.items.trim() !== "";
|
|
14569
|
+
const hasMove = cfg.move !== void 0 && cfg.move.trim() !== "";
|
|
14570
|
+
if (hasItems && hasMove) throw new Error("--items and --move are mutually exclusive");
|
|
14571
|
+
if (!hasItems && !hasMove) throw new Error("one of --items or --move is required");
|
|
14572
|
+
let toIndex = 0;
|
|
14573
|
+
if (hasMove) {
|
|
14574
|
+
if (cfg.toIndex === void 0 || !/^[0-9]+$/.test(cfg.toIndex)) {
|
|
14575
|
+
throw new Error("--move needs --to-index <n> (non-negative integer)");
|
|
14576
|
+
}
|
|
14577
|
+
toIndex = Number(cfg.toIndex);
|
|
14578
|
+
}
|
|
14579
|
+
const given = parseReorderItems(hasItems ? cfg.items ?? "" : cfg.move ?? "");
|
|
14580
|
+
if (hasMove && given.length !== 1) throw new Error("--move takes a single kind:id");
|
|
14581
|
+
const containerArg = cfg.container === null ? "root" : String(cfg.container);
|
|
14582
|
+
const firstGiven = given[0];
|
|
14583
|
+
const request = hasMove && firstGiven !== void 0 ? `move:${itemKey(firstGiven)}@${toIndex}` : given.map(itemKey).join(",");
|
|
14584
|
+
const action = {
|
|
14585
|
+
resource: "folder-container",
|
|
14586
|
+
id: reorderConfirmId(cfg.entityType, containerArg, request),
|
|
14587
|
+
verb: "reorder"
|
|
14588
|
+
};
|
|
14589
|
+
await preflightConfirmToken(action, cfg.token);
|
|
14590
|
+
const opts = { signal: cfg.signal };
|
|
14591
|
+
const [tree, entities] = await Promise.all([
|
|
14592
|
+
list9(cfg.client, cfg.entityType, opts),
|
|
14593
|
+
listEntities(cfg.client, cfg.entityType, opts)
|
|
14594
|
+
]);
|
|
14595
|
+
const current = buildContainerMembers(tree, entities).get(cfg.container) ?? [];
|
|
14596
|
+
const items = hasMove && firstGiven !== void 0 ? moveItem(current, firstGiven, toIndex) : given;
|
|
14597
|
+
const payload = {
|
|
14598
|
+
entity_type: cfg.entityType,
|
|
14599
|
+
container_id: cfg.container,
|
|
14600
|
+
items
|
|
14601
|
+
};
|
|
14602
|
+
const flags = hasItems ? `--items ${cfg.items?.replace(/\s+/g, "") ?? ""}` : `--move ${cfg.move?.trim() ?? ""} --to-index ${toIndex}`;
|
|
14603
|
+
await twoStepConfirm({
|
|
14604
|
+
out: cfg.out,
|
|
14605
|
+
action,
|
|
14606
|
+
token: cfg.token,
|
|
14607
|
+
server: cfg.serverFlag,
|
|
14608
|
+
confirmCmd: `dr folders reorder --entity-type ${cfg.entityType} --container ${containerArg} ${flags} --server ${cfg.serverFlag}`,
|
|
14609
|
+
describe: async () => {
|
|
14610
|
+
const byKey2 = new Map(current.map((m, i) => [itemKey(m), { m, i }]));
|
|
14611
|
+
const lines = [`container: ${containerArg} (${cfg.entityType})`, "current order:"];
|
|
14612
|
+
if (current.length === 0) lines.push(" (empty)");
|
|
14613
|
+
current.forEach((m, i) => lines.push(` ${i} ${itemKey(m)} ${m.name}`));
|
|
14614
|
+
lines.push("new order:");
|
|
14615
|
+
items.forEach((it, i) => {
|
|
14616
|
+
const was = byKey2.get(itemKey(it));
|
|
14617
|
+
const note = was === void 0 ? "not in container" : was.i === i ? "" : `was ${was.i}`;
|
|
14618
|
+
const name = was?.m.name ?? "?";
|
|
14619
|
+
lines.push(` ${i} ${itemKey(it)} ${name}${note === "" ? "" : ` (${note})`}`);
|
|
14620
|
+
});
|
|
14621
|
+
const sent = new Set(items.map(itemKey));
|
|
14622
|
+
const missing = current.filter((m) => !sent.has(itemKey(m))).map(itemKey);
|
|
14623
|
+
const foreign = items.filter((it) => !byKey2.has(itemKey(it))).map(itemKey);
|
|
14624
|
+
if (missing.length > 0) lines.push(`missing from payload: ${missing.join(", ")}`);
|
|
14625
|
+
if (foreign.length > 0) lines.push(`not in container: ${foreign.join(", ")}`);
|
|
14626
|
+
lines.push(`payload: ${JSON.stringify(payload)}`);
|
|
14627
|
+
return lines;
|
|
14628
|
+
},
|
|
14629
|
+
execute: async () => {
|
|
14630
|
+
try {
|
|
14631
|
+
await reorder(cfg.client, payload, opts);
|
|
14632
|
+
} catch (err) {
|
|
14633
|
+
if (isNotFoundError(err) && err instanceof Error) throw flagOffError(cfg.entityType, err);
|
|
14634
|
+
throw err;
|
|
14635
|
+
}
|
|
14636
|
+
const where = cfg.container === null ? "root" : `folder ${cfg.container}`;
|
|
14637
|
+
cfg.out.write(`Reordered ${items.length} items in ${where}.
|
|
14638
|
+
`);
|
|
14639
|
+
}
|
|
14640
|
+
});
|
|
14641
|
+
}
|
|
14642
|
+
async function runFoldersMove(cfg) {
|
|
14643
|
+
const parentArg = cfg.parentId === null ? "root" : String(cfg.parentId);
|
|
14644
|
+
const action = {
|
|
14645
|
+
resource: "folder",
|
|
14646
|
+
id: `${cfg.entityType}:${cfg.folderId}->${parentArg}`,
|
|
14647
|
+
verb: "move"
|
|
14648
|
+
};
|
|
14649
|
+
await preflightConfirmToken(action, cfg.token);
|
|
14650
|
+
const opts = { signal: cfg.signal };
|
|
14651
|
+
await twoStepConfirm({
|
|
14652
|
+
out: cfg.out,
|
|
14653
|
+
action,
|
|
14654
|
+
token: cfg.token,
|
|
14655
|
+
server: cfg.serverFlag,
|
|
14656
|
+
confirmCmd: `dr folders move ${cfg.folderId} --parent ${parentArg} --entity-type ${cfg.entityType} --server ${cfg.serverFlag}`,
|
|
14657
|
+
describe: async () => {
|
|
14658
|
+
const tree = await list9(cfg.client, cfg.entityType, opts);
|
|
14659
|
+
const byId = /* @__PURE__ */ new Map();
|
|
14660
|
+
const walk = (n) => {
|
|
14661
|
+
byId.set(n.id, n);
|
|
14662
|
+
for (const c of n.children ?? []) walk(c);
|
|
14663
|
+
};
|
|
14664
|
+
for (const root of tree) walk(root);
|
|
14665
|
+
const names = new Map([...byId].map(([id, n]) => [id, n.name]));
|
|
14666
|
+
const folder = byId.get(cfg.folderId);
|
|
14667
|
+
const to = containerLabel(cfg.parentId, names);
|
|
14668
|
+
if (folder === void 0) {
|
|
14669
|
+
return [
|
|
14670
|
+
`folder ${cfg.folderId}: not in the ${cfg.entityType} tree (wrong --entity-type, flag off, or no access) -> ${to}`
|
|
14671
|
+
];
|
|
14672
|
+
}
|
|
14673
|
+
return [
|
|
14674
|
+
`folder ${folder.id} (${folder.name}): ${containerLabel(folder.parent_id, names)} -> ${to}`,
|
|
14675
|
+
"the folder is appended at the end of the destination (Admin / Data Manager only)"
|
|
14676
|
+
];
|
|
14677
|
+
},
|
|
14678
|
+
execute: async () => {
|
|
14679
|
+
try {
|
|
14680
|
+
await move(cfg.client, cfg.folderId, cfg.parentId, opts);
|
|
14681
|
+
} catch (err) {
|
|
14682
|
+
if (isNotFoundError(err) && err instanceof Error) {
|
|
14683
|
+
throw new Error(
|
|
14684
|
+
`folder ${cfg.folderId} not found, or folders are disabled (${FOLDER_FLAGS[cfg.entityType]}): ${err.message}`
|
|
14685
|
+
);
|
|
14686
|
+
}
|
|
14687
|
+
throw err;
|
|
14688
|
+
}
|
|
14689
|
+
cfg.out.write(
|
|
14690
|
+
`Moved folder ${cfg.folderId} to ${parentArg === "root" ? "root" : `folder ${parentArg}`}.
|
|
14691
|
+
`
|
|
14692
|
+
);
|
|
14693
|
+
}
|
|
14694
|
+
});
|
|
14376
14695
|
}
|
|
14377
14696
|
async function runFoldersCreate(cfg) {
|
|
14378
14697
|
if (cfg.name === "") {
|
|
@@ -14505,6 +14824,70 @@ function registerFoldersCommand(root) {
|
|
|
14505
14824
|
);
|
|
14506
14825
|
addServerFlag(createCmd);
|
|
14507
14826
|
addAccountFlag(createCmd);
|
|
14827
|
+
const reorderCmd = cmd.command("reorder").description(
|
|
14828
|
+
"Set the interleaved order of one container (two-step confirm). The order is per-user. --items sends exactly what is given; --move splices the current members and sends them all."
|
|
14829
|
+
).option(
|
|
14830
|
+
"--entity-type <t>",
|
|
14831
|
+
`folder entity type: ${FOLDER_ENTITY_TYPES.join(" | ")}`,
|
|
14832
|
+
"dashboard"
|
|
14833
|
+
).requiredOption("--container <c>", "root | folder id").option("--items <list>", "full ordered list, e.g. folder:12,entity:87 (sent verbatim)").option("--move <kind:id>", "item to move, e.g. entity:87").option("--to-index <n>", "0-based target index for --move (past the end = last)").option("--confirm-token <token>", "Token from a prior plan run \u2014 required to actually reorder").action(
|
|
14834
|
+
async (opts) => {
|
|
14835
|
+
const entityType = parseEntityTypeOpt(opts.entityType ?? "dashboard");
|
|
14836
|
+
const container = parseContainerOpt(opts.container, "--container");
|
|
14837
|
+
const serverKey = resolveServer(opts.server);
|
|
14838
|
+
const setup = await setupCall(opts.server, opts.account);
|
|
14839
|
+
try {
|
|
14840
|
+
await runFoldersReorder({
|
|
14841
|
+
client: setup.client,
|
|
14842
|
+
entityType,
|
|
14843
|
+
container,
|
|
14844
|
+
items: opts.items,
|
|
14845
|
+
move: opts.move,
|
|
14846
|
+
toIndex: opts.toIndex,
|
|
14847
|
+
token: opts.confirmToken,
|
|
14848
|
+
serverFlag: serverKey,
|
|
14849
|
+
signal: setup.signal,
|
|
14850
|
+
out: process.stdout
|
|
14851
|
+
});
|
|
14852
|
+
} finally {
|
|
14853
|
+
setup.cleanup();
|
|
14854
|
+
}
|
|
14855
|
+
}
|
|
14856
|
+
);
|
|
14857
|
+
addServerFlag(reorderCmd);
|
|
14858
|
+
addAccountFlag(reorderCmd);
|
|
14859
|
+
const moveCmd = cmd.command("move").description(
|
|
14860
|
+
"Move a folder under another folder or to root (two-step confirm). Appends at the end of the destination; Admin / Data Manager only."
|
|
14861
|
+
).argument("<folderId>", "folder id (positive integer)").requiredOption("--parent <p>", "root | destination folder id").option(
|
|
14862
|
+
"--entity-type <t>",
|
|
14863
|
+
`tree to describe the move from: ${FOLDER_ENTITY_TYPES.join(" | ")}`,
|
|
14864
|
+
"dashboard"
|
|
14865
|
+
).option("--confirm-token <token>", "Token from a prior plan run \u2014 required to actually move").action(
|
|
14866
|
+
async (idStr, opts) => {
|
|
14867
|
+
const folderId = parseContainerOpt(idStr, "<folderId>");
|
|
14868
|
+
if (folderId === null) throw new Error('invalid <folderId> "root"');
|
|
14869
|
+
const parentId = parseContainerOpt(opts.parent, "--parent");
|
|
14870
|
+
const entityType = parseEntityTypeOpt(opts.entityType ?? "dashboard");
|
|
14871
|
+
const serverKey = resolveServer(opts.server);
|
|
14872
|
+
const setup = await setupCall(opts.server, opts.account);
|
|
14873
|
+
try {
|
|
14874
|
+
await runFoldersMove({
|
|
14875
|
+
client: setup.client,
|
|
14876
|
+
entityType,
|
|
14877
|
+
folderId,
|
|
14878
|
+
parentId,
|
|
14879
|
+
token: opts.confirmToken,
|
|
14880
|
+
serverFlag: serverKey,
|
|
14881
|
+
signal: setup.signal,
|
|
14882
|
+
out: process.stdout
|
|
14883
|
+
});
|
|
14884
|
+
} finally {
|
|
14885
|
+
setup.cleanup();
|
|
14886
|
+
}
|
|
14887
|
+
}
|
|
14888
|
+
);
|
|
14889
|
+
addServerFlag(moveCmd);
|
|
14890
|
+
addAccountFlag(moveCmd);
|
|
14508
14891
|
}
|
|
14509
14892
|
|
|
14510
14893
|
// src/api/dynamicRanges.ts
|
|
@@ -17020,7 +17403,7 @@ async function update3(client, id, body, options) {
|
|
|
17020
17403
|
async function deleteById6(client, id, options) {
|
|
17021
17404
|
await client.deleteJSON(`/api/templates/lutdocuments/${id}`, options);
|
|
17022
17405
|
}
|
|
17023
|
-
async function
|
|
17406
|
+
async function reorder2(client, body, options) {
|
|
17024
17407
|
await client.postJSON("/api/templates/lutdocuments/update_orders", body, options);
|
|
17025
17408
|
}
|
|
17026
17409
|
var LUT_SAVE_POLL_INTERVAL_MS = 2e3;
|
|
@@ -18150,7 +18533,7 @@ async function runLutReorder(cfg) {
|
|
|
18150
18533
|
body = await readJsonPayload(payloadFile);
|
|
18151
18534
|
count = Array.isArray(body["objects"]) ? body["objects"].length : 0;
|
|
18152
18535
|
}
|
|
18153
|
-
await
|
|
18536
|
+
await reorder2(cfg.client, body, { signal: cfg.signal });
|
|
18154
18537
|
cfg.out.write(`Reordered ${count} lookup${plural(count)}.
|
|
18155
18538
|
`);
|
|
18156
18539
|
}
|
|
@@ -20116,7 +20499,7 @@ function parseDimensionFilters(raw) {
|
|
|
20116
20499
|
}
|
|
20117
20500
|
|
|
20118
20501
|
// src/commands/metricsCreate.ts
|
|
20119
|
-
import { createHash as
|
|
20502
|
+
import { createHash as createHash4 } from "crypto";
|
|
20120
20503
|
|
|
20121
20504
|
// src/api/metricsMonolith.ts
|
|
20122
20505
|
async function create9(client, body, options) {
|
|
@@ -23568,7 +23951,7 @@ async function runMetricsDelete(cfg) {
|
|
|
23568
23951
|
});
|
|
23569
23952
|
}
|
|
23570
23953
|
function shortHash(input) {
|
|
23571
|
-
return
|
|
23954
|
+
return createHash4("sha256").update(input).digest("hex").slice(0, 12);
|
|
23572
23955
|
}
|
|
23573
23956
|
function buildMetricsBatchDeleteAction(ids) {
|
|
23574
23957
|
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
@@ -25381,7 +25764,7 @@ async function getOrgReportingCurrencies(client, organizationId, options) {
|
|
|
25381
25764
|
}
|
|
25382
25765
|
|
|
25383
25766
|
// src/commands/fxConfig.ts
|
|
25384
|
-
import { createHash as
|
|
25767
|
+
import { createHash as createHash5 } from "crypto";
|
|
25385
25768
|
|
|
25386
25769
|
// src/api/widgets.ts
|
|
25387
25770
|
async function list18(client, dashboardId, options) {
|
|
@@ -25944,7 +26327,7 @@ async function runFxSetPosting(cfg) {
|
|
|
25944
26327
|
verb: "set-fx"
|
|
25945
26328
|
} : {
|
|
25946
26329
|
resource: "field-batch",
|
|
25947
|
-
id:
|
|
26330
|
+
id: createHash5("sha256").update(
|
|
25948
26331
|
changes.map((c) => `${c.field.id}:posting`).slice().sort().join("|")
|
|
25949
26332
|
).digest("hex").slice(0, 12),
|
|
25950
26333
|
verb: "set-fx"
|
|
@@ -26398,7 +26781,7 @@ function buildSetReportingAction(changes) {
|
|
|
26398
26781
|
};
|
|
26399
26782
|
}
|
|
26400
26783
|
const canonical = changes.map((c) => `${c.field.id}:reporting:${c.code}:${c.symbol ?? ""}`).slice().sort().join("|");
|
|
26401
|
-
const sha =
|
|
26784
|
+
const sha = createHash5("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
26402
26785
|
return { resource: "field-batch", id: sha, verb: "set-fx" };
|
|
26403
26786
|
}
|
|
26404
26787
|
function renderReportingOutcome(out, results, skipped, notFxTables, asJson) {
|
|
@@ -27378,7 +27761,7 @@ function registerFxCommand(root) {
|
|
|
27378
27761
|
}
|
|
27379
27762
|
|
|
27380
27763
|
// src/commands/errorEnrichments.ts
|
|
27381
|
-
import { createHash as
|
|
27764
|
+
import { createHash as createHash6 } from "crypto";
|
|
27382
27765
|
import { promises as fs13 } from "fs";
|
|
27383
27766
|
import * as path4 from "path";
|
|
27384
27767
|
|
|
@@ -27918,7 +28301,7 @@ async function runEnrichmentsExport(cfg) {
|
|
|
27918
28301
|
requireSweepSuccess(results);
|
|
27919
28302
|
}
|
|
27920
28303
|
function shortHash2(input) {
|
|
27921
|
-
return
|
|
28304
|
+
return createHash6("sha256").update(input).digest("hex").slice(0, 12);
|
|
27922
28305
|
}
|
|
27923
28306
|
function canonicalBatch(parts) {
|
|
27924
28307
|
return stableJson(parts);
|
|
@@ -28713,7 +29096,7 @@ function registerErrorEnrichmentsCommand(root) {
|
|
|
28713
29096
|
}
|
|
28714
29097
|
|
|
28715
29098
|
// src/commands/skill.ts
|
|
28716
|
-
import { createHash as
|
|
29099
|
+
import { createHash as createHash7 } from "crypto";
|
|
28717
29100
|
import { readFile as readFile3, mkdir as mkdir2, readdir as readdir2, rm as rm2, rmdir, stat, writeFile as writeFile3 } from "fs/promises";
|
|
28718
29101
|
import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
|
|
28719
29102
|
|
|
@@ -28875,7 +29258,7 @@ async function runSkillPrint(cfg) {
|
|
|
28875
29258
|
throw new Error(`skill "${skill.name}" has no file "${relPath}" \u2014 available: ${available}`);
|
|
28876
29259
|
}
|
|
28877
29260
|
if (cfg.hash) {
|
|
28878
|
-
const hex =
|
|
29261
|
+
const hex = createHash7("sha256").update(hit.bytes).digest("hex");
|
|
28879
29262
|
cfg.out.write(hex + "\n");
|
|
28880
29263
|
return;
|
|
28881
29264
|
}
|
|
@@ -32740,7 +33123,7 @@ async function itResync(client, id, reqOptions) {
|
|
|
32740
33123
|
// src/commands/filebox.ts
|
|
32741
33124
|
import { promises as fsPromises, existsSync as existsSync2 } from "fs";
|
|
32742
33125
|
import * as nodePath from "path";
|
|
32743
|
-
import { createHash as
|
|
33126
|
+
import { createHash as createHash8 } from "crypto";
|
|
32744
33127
|
|
|
32745
33128
|
// src/commands/filebox_admin.ts
|
|
32746
33129
|
import { promises as fs15 } from "fs";
|
|
@@ -35369,7 +35752,7 @@ function buildIncludeBatchAction(docId, include, targets) {
|
|
|
35369
35752
|
const canonical = `${docId}|${include ? "on" : "off"}|${targets.map((v) => v.id).slice().sort((a, b) => a - b).join(",")}`;
|
|
35370
35753
|
return {
|
|
35371
35754
|
resource: "filebox-version-batch",
|
|
35372
|
-
id:
|
|
35755
|
+
id: createHash8("sha256").update(canonical).digest("hex").slice(0, 12),
|
|
35373
35756
|
verb: includeStateWords(include).verb
|
|
35374
35757
|
};
|
|
35375
35758
|
}
|
|
@@ -42212,7 +42595,7 @@ function registerWidgetsCommand(root) {
|
|
|
42212
42595
|
}
|
|
42213
42596
|
|
|
42214
42597
|
// src/commands/reconciliation.ts
|
|
42215
|
-
import { createHash as
|
|
42598
|
+
import { createHash as createHash9 } from "crypto";
|
|
42216
42599
|
import { promises as fs18 } from "fs";
|
|
42217
42600
|
|
|
42218
42601
|
// src/api/reconciliation.ts
|
|
@@ -43839,7 +44222,7 @@ function chunkByJsonBytes(rows, maxBytes) {
|
|
|
43839
44222
|
return chunks;
|
|
43840
44223
|
}
|
|
43841
44224
|
function settingsActionId(settings) {
|
|
43842
|
-
return
|
|
44225
|
+
return createHash9("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 12);
|
|
43843
44226
|
}
|
|
43844
44227
|
async function runReconciliationAccountsUpdateSettings(cfg) {
|
|
43845
44228
|
const action = {
|
|
@@ -44217,7 +44600,7 @@ async function runReconciliationAccountsSetAssignee(cfg) {
|
|
|
44217
44600
|
var ASSIGNEE_BATCH_CHUNK_SIZE = 100;
|
|
44218
44601
|
function assigneeBatchActionId(date, rows) {
|
|
44219
44602
|
const canonical = rows.map((r) => [r.accountId, r.type, r.userId, r.userName, r.userEmail]);
|
|
44220
|
-
return
|
|
44603
|
+
return createHash9("sha256").update(JSON.stringify([date, canonical])).digest("hex").slice(0, 12);
|
|
44221
44604
|
}
|
|
44222
44605
|
async function runReconciliationAccountsSetAssigneeBatch(cfg) {
|
|
44223
44606
|
const action = {
|
|
@@ -44371,7 +44754,7 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44371
44754
|
}
|
|
44372
44755
|
function clearBatchActionId(date, rows) {
|
|
44373
44756
|
const canonical = rows.map((r) => [r.accountId, r.type]);
|
|
44374
|
-
return
|
|
44757
|
+
return createHash9("sha256").update(JSON.stringify(["clear", date, canonical])).digest("hex").slice(0, 12);
|
|
44375
44758
|
}
|
|
44376
44759
|
async function runReconciliationAccountsClearAssigneeBatch(cfg) {
|
|
44377
44760
|
const action = {
|
|
@@ -46261,7 +46644,7 @@ function renderForError(v) {
|
|
|
46261
46644
|
}
|
|
46262
46645
|
|
|
46263
46646
|
// src/commands/mecWorkflow.ts
|
|
46264
|
-
import { createHash as
|
|
46647
|
+
import { createHash as createHash10 } from "crypto";
|
|
46265
46648
|
|
|
46266
46649
|
// src/api/workflow.ts
|
|
46267
46650
|
function buildQueryString(params) {
|
|
@@ -48312,7 +48695,7 @@ async function runMecAccountsTaskStatuses(cfg) {
|
|
|
48312
48695
|
renderMecAccountsTasksOrStatuses(cfg.out, resp, "statuses");
|
|
48313
48696
|
}
|
|
48314
48697
|
function shortHash3(input) {
|
|
48315
|
-
return
|
|
48698
|
+
return createHash10("sha256").update(input).digest("hex").slice(0, 12);
|
|
48316
48699
|
}
|
|
48317
48700
|
async function runMecWorkflowCreate(cfg) {
|
|
48318
48701
|
const action = {
|
|
@@ -50696,7 +51079,7 @@ function wireMec(program) {
|
|
|
50696
51079
|
}
|
|
50697
51080
|
|
|
50698
51081
|
// src/commands/workflow.ts
|
|
50699
|
-
import { createHash as
|
|
51082
|
+
import { createHash as createHash11 } from "crypto";
|
|
50700
51083
|
function dash6(s) {
|
|
50701
51084
|
if (s === null || s === void 0) return "-";
|
|
50702
51085
|
if (typeof s === "number") return String(s);
|
|
@@ -51165,7 +51548,7 @@ function renderGroupList(out, rows) {
|
|
|
51165
51548
|
`);
|
|
51166
51549
|
}
|
|
51167
51550
|
function shortHash4(input) {
|
|
51168
|
-
return
|
|
51551
|
+
return createHash11("sha256").update(input).digest("hex").slice(0, 12);
|
|
51169
51552
|
}
|
|
51170
51553
|
async function runWorkflowCreate(cfg) {
|
|
51171
51554
|
const action = {
|
|
@@ -53324,7 +53707,7 @@ function wireWorkflow(root) {
|
|
|
53324
53707
|
}
|
|
53325
53708
|
|
|
53326
53709
|
// src/commands/cash.ts
|
|
53327
|
-
import { createHash as
|
|
53710
|
+
import { createHash as createHash12 } from "crypto";
|
|
53328
53711
|
import { promises as fs25 } from "fs";
|
|
53329
53712
|
|
|
53330
53713
|
// src/api/cash.ts
|
|
@@ -54415,7 +54798,7 @@ async function runCashRulesDeleteAll(cfg) {
|
|
|
54415
54798
|
return;
|
|
54416
54799
|
}
|
|
54417
54800
|
const ids = targets.map((r) => r.id).sort((a, b) => a - b);
|
|
54418
|
-
const sha =
|
|
54801
|
+
const sha = createHash12("sha256").update(ids.join(",")).digest("hex").slice(0, 12);
|
|
54419
54802
|
const action = {
|
|
54420
54803
|
resource: "cash-rules-batch",
|
|
54421
54804
|
id: `${templateId}:${sha}`,
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.102",
|
|
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": {
|
|
@@ -635,6 +635,22 @@
|
|
|
635
635
|
}
|
|
636
636
|
]
|
|
637
637
|
},
|
|
638
|
+
"folders reorder": {
|
|
639
|
+
"requires": [
|
|
640
|
+
{
|
|
641
|
+
"resource": "dashboards",
|
|
642
|
+
"action": "write"
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
},
|
|
646
|
+
"folders move": {
|
|
647
|
+
"requires": [
|
|
648
|
+
{
|
|
649
|
+
"resource": "dashboards",
|
|
650
|
+
"action": "write"
|
|
651
|
+
}
|
|
652
|
+
]
|
|
653
|
+
},
|
|
638
654
|
"dynamic-ranges list": {
|
|
639
655
|
"requires": [
|
|
640
656
|
{
|
|
@@ -244,7 +244,10 @@ area's gotchas.
|
|
|
244
244
|
subcommand.
|
|
245
245
|
|
|
246
246
|
**Dashboards, widgets, storyboards, reports, extables** → [reference/dashboards-widgets.md](reference/dashboards-widgets.md)
|
|
247
|
-
- `dashboards list|get|create|delete|prepare|set-date-picker`
|
|
247
|
+
- `dashboards list|get|create|delete|prepare|set-date-picker|set-folder`
|
|
248
|
+
- `folders list|create|reorder|move` — dashboard / story picker folders;
|
|
249
|
+
`reorder` and `move` are two-step confirm. Reorder is per-user and takes the
|
|
250
|
+
container's full member set (`folders list` shows it).
|
|
248
251
|
- `dashboards push <id> --to <orgs> [--override]` — copy a dashboard into other
|
|
249
252
|
organizations (two-step confirm). Destinations must come from
|
|
250
253
|
`organizations push-targets`.
|
|
@@ -34,6 +34,29 @@ dashboard is added, with `' Copy'` appended if the name is taken.
|
|
|
34
34
|
The push is queued and returns no job id — results arrive as in-app
|
|
35
35
|
notifications. Confirm with `dashboards list --account <destination>`.
|
|
36
36
|
|
|
37
|
+
### Folders (picker tree)
|
|
38
|
+
|
|
39
|
+
Gated per entity type by Unleash `dashboards__folders` / `storytelling__folders`.
|
|
40
|
+
Flag off: `folders list` shows no tree and says the flag is off; every write 404s.
|
|
41
|
+
|
|
42
|
+
- `folders list [--entity-type dashboard|story]` — the tree, then each
|
|
43
|
+
container's members in picker order as `kind:id name position` (`root` first).
|
|
44
|
+
This is the input for a reorder. `--json` is the raw tree only.
|
|
45
|
+
- `folders create --name <n> [--parent <id>]`.
|
|
46
|
+
- `folders reorder --container <root|folderId> (--items folder:12,entity:87,… | --move <kind:id> --to-index <n>)`
|
|
47
|
+
— two-step confirm. The order is **per user**, and the server takes only the
|
|
48
|
+
FULL set of the container's members visible to the caller, so anything partial,
|
|
49
|
+
foreign or duplicated is a 400. `--move` fetches the current members, splices
|
|
50
|
+
the item in (an index past the end lands it last) and sends them all.
|
|
51
|
+
`--items` is sent exactly as given, which is how you reproduce a 400; the plan
|
|
52
|
+
lists what is missing from the payload and what is not in the container. The
|
|
53
|
+
400 body is printed verbatim.
|
|
54
|
+
- `folders move <folderId> --parent <root|folderId>` — two-step confirm.
|
|
55
|
+
Re-parents the folder and appends it at the destination's end. Admin / Data
|
|
56
|
+
Manager only.
|
|
57
|
+
- `dashboards set-folder <id> (--folder <fid> | --root)` moves a dashboard
|
|
58
|
+
(`PUT /api/dashboards/<id>` `{folder_id}`). It is appended at the end.
|
|
59
|
+
|
|
37
60
|
## Widgets
|
|
38
61
|
|
|
39
62
|
- `widgets list`, `widgets get <id>`, `widgets data <id>` (alias `widget`).
|
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.102",
|
|
4
4
|
"description": "Datarails CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@datarails-ai/dr-appspace-cli": "0.10.
|
|
20
|
+
"@datarails-ai/dr-appspace-cli": "0.10.22",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|