@datarails-ai/dr-cli 1.6.101 → 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 CHANGED
@@ -397,6 +397,7 @@ dr skill print <name> # print an embedded skill's SKILL.md
397
397
  | Data models | `data-models list/get` |
398
398
  | Datamappers | `datamappers list/get/create/update/delete` |
399
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`) |
400
401
  | Functions | `functions list/get/create/delete` |
401
402
  | Dynamic ranges | `dynamic-ranges list/get/data/create/delete` |
402
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.101",
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.21",
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-05T14:18:41.284Z";
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);
@@ -574,11 +574,18 @@ var UnknownAccountError = class extends Error {
574
574
  var SCOPE_DENIED_CODE = 40301;
575
575
  var SCOPE_UNDECLARED_CODE = 40302;
576
576
  var ScopeDeniedError = class extends Error {
577
- constructor(method, path11, code, required2) {
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) {
578
584
  super("");
579
585
  this.method = method;
580
586
  this.path = path11;
581
587
  this.code = code;
588
+ this.identity = identity2;
582
589
  this.name = "ScopeDeniedError";
583
590
  this.required = required2;
584
591
  this.message = this.render();
@@ -586,6 +593,7 @@ var ScopeDeniedError = class extends Error {
586
593
  method;
587
594
  path;
588
595
  code;
596
+ identity;
589
597
  /** `resource:action` the command needs, joined when it needs more than one. Filled in by
590
598
  * `cli.ts` after construction (see the class docstring); `undefined` until then, and for
591
599
  * a command the local map says nothing about. */
@@ -604,6 +612,10 @@ var ScopeDeniedError = class extends Error {
604
612
  return `${this.method} ${this.path}: this endpoint is not yet scope-declared on the server; report to #dr-cli`;
605
613
  }
606
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
+ }
607
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`;
608
620
  }
609
621
  };
@@ -3670,6 +3682,11 @@ var PROD_GATE_TIER_B = /* @__PURE__ */ new Set([
3670
3682
  // cash (dr cash …). rules delete / delete-all / hidden-rules delete → Tier A.
3671
3683
  "cash-rule|update",
3672
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",
3673
3690
  "cash-hidden-rules|upsert",
3674
3691
  "cash-ai-rules|trigger",
3675
3692
  // metrics (dr metrics …). delete / dimension|category delete → A.
@@ -6079,7 +6096,17 @@ async function resolveTargetUserId(client, targetEmail, signal) {
6079
6096
  `no user you can log in as matches "${targetEmail}".${near} Use --user-id <n> to impersonate by id.`
6080
6097
  );
6081
6098
  }
6082
- function friendlyLoginAsError(err, targetEmail) {
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
+ }
6083
6110
  switch (err.code) {
6084
6111
  case "not_permitted":
6085
6112
  return new Error(`You are not permitted to impersonate ${targetEmail}.`);
@@ -6139,7 +6166,10 @@ async function runLoginAs(cfg) {
6139
6166
  signal: cfg.signal
6140
6167
  });
6141
6168
  } catch (err) {
6142
- if (err instanceof LoginAsError) throw friendlyLoginAsError(err, cfg.targetEmail);
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
+ }
6143
6173
  throw err;
6144
6174
  }
6145
6175
  let storageEmail = cfg.targetEmail;
@@ -14366,8 +14396,15 @@ function registerDashboardsCommand(root) {
14366
14396
  addAccountFlag(setFolderCmd);
14367
14397
  }
14368
14398
 
14399
+ // src/commands/folders.ts
14400
+ import { createHash as createHash3 } from "crypto";
14401
+
14369
14402
  // src/api/folders.ts
14370
14403
  var FOLDER_ENTITY_TYPES = ["dashboard", "story"];
14404
+ var FOLDER_FLAGS = {
14405
+ dashboard: "dashboards__folders",
14406
+ story: "storytelling__folders"
14407
+ };
14371
14408
  async function list9(client, entityType, options) {
14372
14409
  const out = await client.getJSON(
14373
14410
  `/api/folders/?entity_type=${entityType}`,
@@ -14384,6 +14421,39 @@ async function create4(client, entityType, name, parentId, options) {
14384
14421
  }
14385
14422
  return out;
14386
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
+ }
14387
14457
 
14388
14458
  // src/commands/folders.ts
14389
14459
  async function runFoldersList(cfg) {
@@ -14393,6 +14463,235 @@ async function runFoldersList(cfg) {
14393
14463
  return;
14394
14464
  }
14395
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
+ });
14396
14695
  }
14397
14696
  async function runFoldersCreate(cfg) {
14398
14697
  if (cfg.name === "") {
@@ -14525,6 +14824,70 @@ function registerFoldersCommand(root) {
14525
14824
  );
14526
14825
  addServerFlag(createCmd);
14527
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);
14528
14891
  }
14529
14892
 
14530
14893
  // src/api/dynamicRanges.ts
@@ -17040,7 +17403,7 @@ async function update3(client, id, body, options) {
17040
17403
  async function deleteById6(client, id, options) {
17041
17404
  await client.deleteJSON(`/api/templates/lutdocuments/${id}`, options);
17042
17405
  }
17043
- async function reorder(client, body, options) {
17406
+ async function reorder2(client, body, options) {
17044
17407
  await client.postJSON("/api/templates/lutdocuments/update_orders", body, options);
17045
17408
  }
17046
17409
  var LUT_SAVE_POLL_INTERVAL_MS = 2e3;
@@ -18170,7 +18533,7 @@ async function runLutReorder(cfg) {
18170
18533
  body = await readJsonPayload(payloadFile);
18171
18534
  count = Array.isArray(body["objects"]) ? body["objects"].length : 0;
18172
18535
  }
18173
- await reorder(cfg.client, body, { signal: cfg.signal });
18536
+ await reorder2(cfg.client, body, { signal: cfg.signal });
18174
18537
  cfg.out.write(`Reordered ${count} lookup${plural(count)}.
18175
18538
  `);
18176
18539
  }
@@ -20136,7 +20499,7 @@ function parseDimensionFilters(raw) {
20136
20499
  }
20137
20500
 
20138
20501
  // src/commands/metricsCreate.ts
20139
- import { createHash as createHash3 } from "crypto";
20502
+ import { createHash as createHash4 } from "crypto";
20140
20503
 
20141
20504
  // src/api/metricsMonolith.ts
20142
20505
  async function create9(client, body, options) {
@@ -23588,7 +23951,7 @@ async function runMetricsDelete(cfg) {
23588
23951
  });
23589
23952
  }
23590
23953
  function shortHash(input) {
23591
- return createHash3("sha256").update(input).digest("hex").slice(0, 12);
23954
+ return createHash4("sha256").update(input).digest("hex").slice(0, 12);
23592
23955
  }
23593
23956
  function buildMetricsBatchDeleteAction(ids) {
23594
23957
  const sorted = [...new Set(ids)].sort((a, b) => a - b);
@@ -25401,7 +25764,7 @@ async function getOrgReportingCurrencies(client, organizationId, options) {
25401
25764
  }
25402
25765
 
25403
25766
  // src/commands/fxConfig.ts
25404
- import { createHash as createHash4 } from "crypto";
25767
+ import { createHash as createHash5 } from "crypto";
25405
25768
 
25406
25769
  // src/api/widgets.ts
25407
25770
  async function list18(client, dashboardId, options) {
@@ -25964,7 +26327,7 @@ async function runFxSetPosting(cfg) {
25964
26327
  verb: "set-fx"
25965
26328
  } : {
25966
26329
  resource: "field-batch",
25967
- id: createHash4("sha256").update(
26330
+ id: createHash5("sha256").update(
25968
26331
  changes.map((c) => `${c.field.id}:posting`).slice().sort().join("|")
25969
26332
  ).digest("hex").slice(0, 12),
25970
26333
  verb: "set-fx"
@@ -26418,7 +26781,7 @@ function buildSetReportingAction(changes) {
26418
26781
  };
26419
26782
  }
26420
26783
  const canonical = changes.map((c) => `${c.field.id}:reporting:${c.code}:${c.symbol ?? ""}`).slice().sort().join("|");
26421
- const sha = createHash4("sha256").update(canonical).digest("hex").slice(0, 12);
26784
+ const sha = createHash5("sha256").update(canonical).digest("hex").slice(0, 12);
26422
26785
  return { resource: "field-batch", id: sha, verb: "set-fx" };
26423
26786
  }
26424
26787
  function renderReportingOutcome(out, results, skipped, notFxTables, asJson) {
@@ -27398,7 +27761,7 @@ function registerFxCommand(root) {
27398
27761
  }
27399
27762
 
27400
27763
  // src/commands/errorEnrichments.ts
27401
- import { createHash as createHash5 } from "crypto";
27764
+ import { createHash as createHash6 } from "crypto";
27402
27765
  import { promises as fs13 } from "fs";
27403
27766
  import * as path4 from "path";
27404
27767
 
@@ -27938,7 +28301,7 @@ async function runEnrichmentsExport(cfg) {
27938
28301
  requireSweepSuccess(results);
27939
28302
  }
27940
28303
  function shortHash2(input) {
27941
- return createHash5("sha256").update(input).digest("hex").slice(0, 12);
28304
+ return createHash6("sha256").update(input).digest("hex").slice(0, 12);
27942
28305
  }
27943
28306
  function canonicalBatch(parts) {
27944
28307
  return stableJson(parts);
@@ -28733,7 +29096,7 @@ function registerErrorEnrichmentsCommand(root) {
28733
29096
  }
28734
29097
 
28735
29098
  // src/commands/skill.ts
28736
- import { createHash as createHash6 } from "crypto";
29099
+ import { createHash as createHash7 } from "crypto";
28737
29100
  import { readFile as readFile3, mkdir as mkdir2, readdir as readdir2, rm as rm2, rmdir, stat, writeFile as writeFile3 } from "fs/promises";
28738
29101
  import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
28739
29102
 
@@ -28895,7 +29258,7 @@ async function runSkillPrint(cfg) {
28895
29258
  throw new Error(`skill "${skill.name}" has no file "${relPath}" \u2014 available: ${available}`);
28896
29259
  }
28897
29260
  if (cfg.hash) {
28898
- const hex = createHash6("sha256").update(hit.bytes).digest("hex");
29261
+ const hex = createHash7("sha256").update(hit.bytes).digest("hex");
28899
29262
  cfg.out.write(hex + "\n");
28900
29263
  return;
28901
29264
  }
@@ -32760,7 +33123,7 @@ async function itResync(client, id, reqOptions) {
32760
33123
  // src/commands/filebox.ts
32761
33124
  import { promises as fsPromises, existsSync as existsSync2 } from "fs";
32762
33125
  import * as nodePath from "path";
32763
- import { createHash as createHash7 } from "crypto";
33126
+ import { createHash as createHash8 } from "crypto";
32764
33127
 
32765
33128
  // src/commands/filebox_admin.ts
32766
33129
  import { promises as fs15 } from "fs";
@@ -35389,7 +35752,7 @@ function buildIncludeBatchAction(docId, include, targets) {
35389
35752
  const canonical = `${docId}|${include ? "on" : "off"}|${targets.map((v) => v.id).slice().sort((a, b) => a - b).join(",")}`;
35390
35753
  return {
35391
35754
  resource: "filebox-version-batch",
35392
- id: createHash7("sha256").update(canonical).digest("hex").slice(0, 12),
35755
+ id: createHash8("sha256").update(canonical).digest("hex").slice(0, 12),
35393
35756
  verb: includeStateWords(include).verb
35394
35757
  };
35395
35758
  }
@@ -42232,7 +42595,7 @@ function registerWidgetsCommand(root) {
42232
42595
  }
42233
42596
 
42234
42597
  // src/commands/reconciliation.ts
42235
- import { createHash as createHash8 } from "crypto";
42598
+ import { createHash as createHash9 } from "crypto";
42236
42599
  import { promises as fs18 } from "fs";
42237
42600
 
42238
42601
  // src/api/reconciliation.ts
@@ -43859,7 +44222,7 @@ function chunkByJsonBytes(rows, maxBytes) {
43859
44222
  return chunks;
43860
44223
  }
43861
44224
  function settingsActionId(settings) {
43862
- return createHash8("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 12);
44225
+ return createHash9("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 12);
43863
44226
  }
43864
44227
  async function runReconciliationAccountsUpdateSettings(cfg) {
43865
44228
  const action = {
@@ -44237,7 +44600,7 @@ async function runReconciliationAccountsSetAssignee(cfg) {
44237
44600
  var ASSIGNEE_BATCH_CHUNK_SIZE = 100;
44238
44601
  function assigneeBatchActionId(date, rows) {
44239
44602
  const canonical = rows.map((r) => [r.accountId, r.type, r.userId, r.userName, r.userEmail]);
44240
- return createHash8("sha256").update(JSON.stringify([date, canonical])).digest("hex").slice(0, 12);
44603
+ return createHash9("sha256").update(JSON.stringify([date, canonical])).digest("hex").slice(0, 12);
44241
44604
  }
44242
44605
  async function runReconciliationAccountsSetAssigneeBatch(cfg) {
44243
44606
  const action = {
@@ -44391,7 +44754,7 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
44391
44754
  }
44392
44755
  function clearBatchActionId(date, rows) {
44393
44756
  const canonical = rows.map((r) => [r.accountId, r.type]);
44394
- return createHash8("sha256").update(JSON.stringify(["clear", date, canonical])).digest("hex").slice(0, 12);
44757
+ return createHash9("sha256").update(JSON.stringify(["clear", date, canonical])).digest("hex").slice(0, 12);
44395
44758
  }
44396
44759
  async function runReconciliationAccountsClearAssigneeBatch(cfg) {
44397
44760
  const action = {
@@ -46281,7 +46644,7 @@ function renderForError(v) {
46281
46644
  }
46282
46645
 
46283
46646
  // src/commands/mecWorkflow.ts
46284
- import { createHash as createHash9 } from "crypto";
46647
+ import { createHash as createHash10 } from "crypto";
46285
46648
 
46286
46649
  // src/api/workflow.ts
46287
46650
  function buildQueryString(params) {
@@ -48332,7 +48695,7 @@ async function runMecAccountsTaskStatuses(cfg) {
48332
48695
  renderMecAccountsTasksOrStatuses(cfg.out, resp, "statuses");
48333
48696
  }
48334
48697
  function shortHash3(input) {
48335
- return createHash9("sha256").update(input).digest("hex").slice(0, 12);
48698
+ return createHash10("sha256").update(input).digest("hex").slice(0, 12);
48336
48699
  }
48337
48700
  async function runMecWorkflowCreate(cfg) {
48338
48701
  const action = {
@@ -50716,7 +51079,7 @@ function wireMec(program) {
50716
51079
  }
50717
51080
 
50718
51081
  // src/commands/workflow.ts
50719
- import { createHash as createHash10 } from "crypto";
51082
+ import { createHash as createHash11 } from "crypto";
50720
51083
  function dash6(s) {
50721
51084
  if (s === null || s === void 0) return "-";
50722
51085
  if (typeof s === "number") return String(s);
@@ -51185,7 +51548,7 @@ function renderGroupList(out, rows) {
51185
51548
  `);
51186
51549
  }
51187
51550
  function shortHash4(input) {
51188
- return createHash10("sha256").update(input).digest("hex").slice(0, 12);
51551
+ return createHash11("sha256").update(input).digest("hex").slice(0, 12);
51189
51552
  }
51190
51553
  async function runWorkflowCreate(cfg) {
51191
51554
  const action = {
@@ -53344,7 +53707,7 @@ function wireWorkflow(root) {
53344
53707
  }
53345
53708
 
53346
53709
  // src/commands/cash.ts
53347
- import { createHash as createHash11 } from "crypto";
53710
+ import { createHash as createHash12 } from "crypto";
53348
53711
  import { promises as fs25 } from "fs";
53349
53712
 
53350
53713
  // src/api/cash.ts
@@ -54435,7 +54798,7 @@ async function runCashRulesDeleteAll(cfg) {
54435
54798
  return;
54436
54799
  }
54437
54800
  const ids = targets.map((r) => r.id).sort((a, b) => a - b);
54438
- const sha = createHash11("sha256").update(ids.join(",")).digest("hex").slice(0, 12);
54801
+ const sha = createHash12("sha256").update(ids.join(",")).digest("hex").slice(0, 12);
54439
54802
  const action = {
54440
54803
  resource: "cash-rules-batch",
54441
54804
  id: `${templateId}:${sha}`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.101",
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.101",
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.21",
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",