@datarails-ai/dr-cli 1.6.96 → 1.6.98
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 +14 -0
- package/dist/cli.js +357 -97
- package/dist/scopes-map.json +1 -1
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +9 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -230,6 +230,20 @@ under one identity and run as another.
|
|
|
230
230
|
{ "apiKey": "<key>", "server": "DEV", "email": "svc@datarails.com" }
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
The key may instead live in an environment variable, with the file carrying
|
|
234
|
+
only the variable's **name** — the shape for CI (the key stays in the runner's
|
|
235
|
+
secret env, never on disk) and for agent sandboxes (where the variable's
|
|
236
|
+
in-sandbox value is an opaque vault placeholder substituted at egress, passed
|
|
237
|
+
verbatim and never validated):
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{ "apiKeyEnvVar": "DR_API_KEY", "server": "DEV", "email": "svc@datarails.com" }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`apiKey` and `apiKeyEnvVar` are mutually exclusive — a file carrying both is
|
|
244
|
+
refused rather than guessed about. An unset variable fails the command with an
|
|
245
|
+
error naming it; the stored file is never modified by that failure.
|
|
246
|
+
|
|
233
247
|
`email` names the stored identity — what `--account` selects and what the
|
|
234
248
|
keychain entry is keyed by. It need not match the principal the key is bound to;
|
|
235
249
|
the token the exchange mints is what every request authenticates as, and
|
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.98",
|
|
12
12
|
description: "Datarails CLI",
|
|
13
13
|
type: "module",
|
|
14
14
|
engines: {
|
|
@@ -38,7 +38,7 @@ var package_default = {
|
|
|
38
38
|
postinstall: "node scripts/postinstall.mjs"
|
|
39
39
|
},
|
|
40
40
|
dependencies: {
|
|
41
|
-
"@datarails-ai/dr-appspace-cli": "0.10.
|
|
41
|
+
"@datarails-ai/dr-appspace-cli": "0.10.21",
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
43
|
"@napi-rs/keyring": "^1.3.0",
|
|
44
44
|
commander: "^12.1.0",
|
|
@@ -67,7 +67,7 @@ var package_default = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
// src/buildInfo.ts
|
|
70
|
-
var BUILD_TIME = "2026-10-
|
|
70
|
+
var BUILD_TIME = "2026-10-05T10:25:04.884Z";
|
|
71
71
|
function formatBuildTime(iso) {
|
|
72
72
|
if (iso === "") return "";
|
|
73
73
|
const d = new Date(iso);
|
|
@@ -151,14 +151,17 @@ function parseApiKeyExpiry(value) {
|
|
|
151
151
|
function hasApiKey(s) {
|
|
152
152
|
return s.apiKey !== void 0 && s.apiKey !== "";
|
|
153
153
|
}
|
|
154
|
+
function hasApiKeyEnvVar(s) {
|
|
155
|
+
return s.apiKeyEnvVar !== void 0 && s.apiKeyEnvVar !== "";
|
|
156
|
+
}
|
|
154
157
|
function isImpersonatedSession(s) {
|
|
155
158
|
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "";
|
|
156
159
|
}
|
|
157
160
|
function isCookielessSession(s) {
|
|
158
|
-
return isImpersonatedSession(s) || s.cookieless === true || hasApiKey(s);
|
|
161
|
+
return isImpersonatedSession(s) || s.cookieless === true || hasApiKey(s) || hasApiKeyEnvVar(s);
|
|
159
162
|
}
|
|
160
163
|
function usesApiKey(s) {
|
|
161
|
-
return hasApiKey(s) && !isImpersonatedSession(s);
|
|
164
|
+
return (hasApiKey(s) || hasApiKeyEnvVar(s)) && !isImpersonatedSession(s);
|
|
162
165
|
}
|
|
163
166
|
var KeychainError = class extends Error {
|
|
164
167
|
constructor(message, options) {
|
|
@@ -294,6 +297,16 @@ var credentialsFileSchema = z.object({
|
|
|
294
297
|
// A user-bound API key, exchanged for `access` on demand. A file may hold
|
|
295
298
|
// only this; `access` then appears after the first exchange.
|
|
296
299
|
apiKey: z.string().min(1).optional(),
|
|
300
|
+
// DR-52657: the NAME of an environment variable holding the API key, for
|
|
301
|
+
// provisioners that must not put the key on disk — an agent sandbox, where
|
|
302
|
+
// the variable's in-sandbox value is an opaque vault placeholder
|
|
303
|
+
// substituted at egress, or a CI runner keeping the key in its secret env.
|
|
304
|
+
// The value is resolved at exchange time and passed verbatim; dr-cli never
|
|
305
|
+
// inspects, validates, or logs it. Mutually exclusive with `apiKey` (a
|
|
306
|
+
// refinement below): a file carrying both is ambiguous about which
|
|
307
|
+
// credential authenticates it, and silently preferring either one turns a
|
|
308
|
+
// provisioning mistake into commands running as the wrong key.
|
|
309
|
+
apiKeyEnvVar: z.string().min(1).optional(),
|
|
297
310
|
// Carried verbatim, judged by `parseApiKeyExpiry` where it is read. Not
|
|
298
311
|
// validated here, and not `.catch(undefined)` either: a rotation rewrites
|
|
299
312
|
// the whole file from the parsed object, so a value this schema dropped
|
|
@@ -332,13 +345,19 @@ var credentialsFileSchema = z.object({
|
|
|
332
345
|
}).passthrough().refine((c) => c.sessionId === void 0 === (c.csrfToken === void 0), {
|
|
333
346
|
message: "sessionId and csrfToken must be provided together",
|
|
334
347
|
path: ["sessionId"]
|
|
335
|
-
}).refine((c) => c.
|
|
336
|
-
message: "
|
|
348
|
+
}).refine((c) => !(c.apiKey !== void 0 && c.apiKeyEnvVar !== void 0), {
|
|
349
|
+
message: "apiKey and apiKeyEnvVar are mutually exclusive",
|
|
350
|
+
path: ["apiKeyEnvVar"]
|
|
351
|
+
}).refine((c) => c.access !== void 0 || c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0, {
|
|
352
|
+
message: "one of access, apiKey or apiKeyEnvVar is required",
|
|
337
353
|
path: ["access"]
|
|
338
|
-
}).refine(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
}).refine(
|
|
355
|
+
(c) => c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0 || c.refresh !== void 0 || c.sessionId !== void 0,
|
|
356
|
+
{
|
|
357
|
+
message: "refresh is required unless an apiKey, apiKeyEnvVar or a session cookie is present",
|
|
358
|
+
path: ["refresh"]
|
|
359
|
+
}
|
|
360
|
+
);
|
|
342
361
|
function credentialsFilePath() {
|
|
343
362
|
const override = process.env[CREDENTIALS_FILE_ENV];
|
|
344
363
|
return override !== void 0 && override !== "" ? override : DEFAULT_CREDENTIALS_FILE;
|
|
@@ -801,7 +820,13 @@ async function exchangeApiKey(host, apiKey, options) {
|
|
|
801
820
|
if (key === "") {
|
|
802
821
|
throw new Error("api-key exchange: the stored API key is empty");
|
|
803
822
|
}
|
|
804
|
-
if (
|
|
823
|
+
if (options?.opaque === true) {
|
|
824
|
+
if (/[\r\n\0]/.test(key) || !/^[\x00-\xff]+$/.test(key)) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
"api-key exchange: the environment variable named by apiKeyEnvVar holds a value that cannot travel in an HTTP header (a line break, NUL, or a character above U+00FF)"
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
} else if (!/^[\x21-\x7e]+$/.test(key)) {
|
|
805
830
|
throw new Error(
|
|
806
831
|
"api-key exchange: dr only accepts an API key of printable ASCII with no spaces; the stored one contains whitespace, control characters, or non-ASCII characters"
|
|
807
832
|
);
|
|
@@ -965,10 +990,31 @@ function mintedByApiKey(token) {
|
|
|
965
990
|
function apiKeyFingerprint(apiKey) {
|
|
966
991
|
return createHash("sha256").update(apiKey.trim()).digest("hex").slice(0, 32);
|
|
967
992
|
}
|
|
993
|
+
function resolveApiKeyMaterial(session) {
|
|
994
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
995
|
+
return { key: session.apiKey, fromEnv: false };
|
|
996
|
+
}
|
|
997
|
+
const name = session.apiKeyEnvVar;
|
|
998
|
+
if (name === void 0 || name === "") {
|
|
999
|
+
throw new Error("api-key exchange: the session carries no API key");
|
|
1000
|
+
}
|
|
1001
|
+
const value = process.env[name];
|
|
1002
|
+
if (value === void 0 || value === "") {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
`api-key exchange: the credential file names environment variable ${name} as the API-key carrier, but it is unset or empty in this environment`
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return { key: value, fromEnv: true };
|
|
1008
|
+
}
|
|
968
1009
|
function mintedByStoredApiKey(session) {
|
|
969
1010
|
if (!mintedByApiKey(session.jwtAccess)) return false;
|
|
970
|
-
|
|
971
|
-
|
|
1011
|
+
let material;
|
|
1012
|
+
try {
|
|
1013
|
+
material = resolveApiKeyMaterial(session);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
return session.jwtApiKeyFingerprint === apiKeyFingerprint(material.key);
|
|
972
1018
|
}
|
|
973
1019
|
function apiKeyStaleBuffer(session, identity2, fallbackMs) {
|
|
974
1020
|
if (session.jwtAccess === void 0 || session.jwtAccess === "") return fallbackMs;
|
|
@@ -995,12 +1041,13 @@ function ttlOf(token) {
|
|
|
995
1041
|
function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
996
1042
|
const identity2 = `${serverKey}:${email}`;
|
|
997
1043
|
if (opts.signal?.aborted === true) return Promise.reject(opts.signal.reason);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1044
|
+
let material;
|
|
1045
|
+
try {
|
|
1046
|
+
material = resolveApiKeyMaterial(session);
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
throw new SessionExpiredError(`${identity2} (${msgOf3(err)})`);
|
|
1049
|
+
}
|
|
1050
|
+
const key = JSON.stringify([identity2, host, opts.force, apiKeyFingerprint(material.key)]);
|
|
1004
1051
|
const pending = inFlightExchanges.get(key);
|
|
1005
1052
|
if (pending !== void 0) return joinExchange(pending, opts.signal);
|
|
1006
1053
|
const controller = new AbortController();
|
|
@@ -1015,7 +1062,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1015
1062
|
for (; ; ) {
|
|
1016
1063
|
let tok;
|
|
1017
1064
|
try {
|
|
1018
|
-
tok = await exchangeApiKey(host,
|
|
1065
|
+
tok = await exchangeApiKey(host, material.key, { signal, opaque: material.fromEnv });
|
|
1019
1066
|
} catch (err) {
|
|
1020
1067
|
const cachedIsUsable = !opts.force && mintedByStoredApiKey(session) && !jwtExpiringSoon(session, 0);
|
|
1021
1068
|
if (!(err instanceof ApiKeyExchangeError)) {
|
|
@@ -1054,7 +1101,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1054
1101
|
// the token did not come from. Read back by `mintedByStoredApiKey` on
|
|
1055
1102
|
// every later command to tell this token apart from one the previous
|
|
1056
1103
|
// key minted.
|
|
1057
|
-
jwtApiKeyFingerprint: apiKeyFingerprint(
|
|
1104
|
+
jwtApiKeyFingerprint: apiKeyFingerprint(material.key),
|
|
1058
1105
|
// Dropped, not carried: the exchange mints no refresh token, and the
|
|
1059
1106
|
// refresh rung is closed to a keyed identity by contract. Clearing it
|
|
1060
1107
|
// is what lets "keyed, and still holding a refresh token" mean "these
|
|
@@ -1065,7 +1112,7 @@ function exchangeForSession(store, serverKey, email, session, host, opts) {
|
|
|
1065
1112
|
};
|
|
1066
1113
|
try {
|
|
1067
1114
|
const current = await store.getSession(serverKey, email);
|
|
1068
|
-
if (current === void 0 || current.apiKey !== session.apiKey) {
|
|
1115
|
+
if (current === void 0 || current.apiKey !== session.apiKey || current.apiKeyEnvVar !== session.apiKeyEnvVar) {
|
|
1069
1116
|
return next;
|
|
1070
1117
|
}
|
|
1071
1118
|
await store.saveSession(serverKey, next);
|
|
@@ -1383,7 +1430,8 @@ var KeyringStore = class _KeyringStore {
|
|
|
1383
1430
|
const impersonated = impersonatorEmail !== "";
|
|
1384
1431
|
const cookielessFlag = parsed2.cookieless === true;
|
|
1385
1432
|
const apiKey = await this.backend.get(SERVICE, `${sessionAccount}:apikey`);
|
|
1386
|
-
const
|
|
1433
|
+
const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" ? parsed2.apiKeyEnvVar : "";
|
|
1434
|
+
const keyPresent = apiKey !== void 0 && apiKey !== "" || apiKeyEnvVar !== "";
|
|
1387
1435
|
const cookieless = impersonated || cookielessFlag || keyPresent;
|
|
1388
1436
|
const halfCookie = sessionId === "" !== (csrfToken === "");
|
|
1389
1437
|
if (savedAtStr === "" || halfCookie || !cookieless && sessionId === "") {
|
|
@@ -1429,9 +1477,12 @@ var KeyringStore = class _KeyringStore {
|
|
|
1429
1477
|
if (jwtRefresh !== void 0 && jwtRefresh !== "") {
|
|
1430
1478
|
session.jwtRefresh = jwtRefresh;
|
|
1431
1479
|
}
|
|
1432
|
-
if (
|
|
1480
|
+
if (apiKey !== void 0 && apiKey !== "") {
|
|
1433
1481
|
session.apiKey = apiKey;
|
|
1434
1482
|
}
|
|
1483
|
+
if (apiKeyEnvVar !== "") {
|
|
1484
|
+
session.apiKeyEnvVar = apiKeyEnvVar;
|
|
1485
|
+
}
|
|
1435
1486
|
return session;
|
|
1436
1487
|
}
|
|
1437
1488
|
/**
|
|
@@ -1463,9 +1514,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
1463
1514
|
const impersonated = isImpersonatedSession(session);
|
|
1464
1515
|
if (isCookielessSession(session)) {
|
|
1465
1516
|
const hasJwt = session.jwtAccess !== void 0 && session.jwtAccess !== "";
|
|
1466
|
-
if (!hasJwt && !hasApiKey(session)) {
|
|
1517
|
+
if (!hasJwt && !hasApiKey(session) && !hasApiKeyEnvVar(session)) {
|
|
1467
1518
|
throw new Error(
|
|
1468
|
-
"saveSession: cookie-less session must have a jwtAccess token or an
|
|
1519
|
+
"saveSession: cookie-less session must have a jwtAccess token, an apiKey, or an apiKeyEnvVar"
|
|
1469
1520
|
);
|
|
1470
1521
|
}
|
|
1471
1522
|
if (session.sessionId === "" !== (session.csrfToken === "")) {
|
|
@@ -1496,6 +1547,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
1496
1547
|
if (session.jwtApiKeyFingerprint !== void 0 && session.jwtApiKeyFingerprint !== "") {
|
|
1497
1548
|
metaBlob.jwtApiKeyFingerprint = session.jwtApiKeyFingerprint;
|
|
1498
1549
|
}
|
|
1550
|
+
if (hasApiKeyEnvVar(session)) {
|
|
1551
|
+
metaBlob.apiKeyEnvVar = session.apiKeyEnvVar;
|
|
1552
|
+
}
|
|
1499
1553
|
if (impersonated) {
|
|
1500
1554
|
metaBlob.impersonatorEmail = session.impersonatorEmail;
|
|
1501
1555
|
if (session.impersonatedUserId !== void 0) {
|
|
@@ -2599,7 +2653,9 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2599
2653
|
// Named as the Session field, since the blob is what `getSession` reads.
|
|
2600
2654
|
...creds.accessApiKeyFingerprint !== void 0 && {
|
|
2601
2655
|
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2602
|
-
}
|
|
2656
|
+
},
|
|
2657
|
+
// The env var's NAME only — the resolved key never enters any store.
|
|
2658
|
+
...creds.apiKeyEnvVar !== void 0 && { apiKeyEnvVar: creds.apiKeyEnvVar }
|
|
2603
2659
|
});
|
|
2604
2660
|
}
|
|
2605
2661
|
static slotOf(creds, account) {
|
|
@@ -2627,11 +2683,16 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2627
2683
|
const csrfToken = typeof parsed2.csrfToken === "string" ? parsed2.csrfToken : "";
|
|
2628
2684
|
const apiKeyExpiresAt = typeof parsed2.apiKeyExpiresAt === "string" ? parsed2.apiKeyExpiresAt : "";
|
|
2629
2685
|
const accessApiKeyFingerprint = typeof parsed2.jwtApiKeyFingerprint === "string" ? parsed2.jwtApiKeyFingerprint : "";
|
|
2686
|
+
const apiKeyEnvVar = typeof parsed2.apiKeyEnvVar === "string" ? parsed2.apiKeyEnvVar : "";
|
|
2630
2687
|
return {
|
|
2631
2688
|
...sessionId !== "" && { sessionId },
|
|
2632
2689
|
...csrfToken !== "" && { csrfToken },
|
|
2633
2690
|
...apiKeyExpiresAt !== "" && { apiKeyExpiresAt },
|
|
2634
|
-
...accessApiKeyFingerprint !== "" && { accessApiKeyFingerprint }
|
|
2691
|
+
...accessApiKeyFingerprint !== "" && { accessApiKeyFingerprint },
|
|
2692
|
+
// Set-if-present, never deleted on a blob that lost it (the
|
|
2693
|
+
// `apiKeyExpiresAt` rule): it is the identity's only credential pointer,
|
|
2694
|
+
// and a meta rewrite must not be able to brick the file.
|
|
2695
|
+
...apiKeyEnvVar !== "" && { apiKeyEnvVar }
|
|
2635
2696
|
};
|
|
2636
2697
|
}
|
|
2637
2698
|
async get(_service, account) {
|
|
@@ -2724,6 +2785,7 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2724
2785
|
if (meta.apiKeyExpiresAt !== void 0) next.apiKeyExpiresAt = meta.apiKeyExpiresAt;
|
|
2725
2786
|
if (meta.accessApiKeyFingerprint === void 0) delete next.accessApiKeyFingerprint;
|
|
2726
2787
|
else next.accessApiKeyFingerprint = meta.accessApiKeyFingerprint;
|
|
2788
|
+
if (meta.apiKeyEnvVar !== void 0) next.apiKeyEnvVar = meta.apiKeyEnvVar;
|
|
2727
2789
|
break;
|
|
2728
2790
|
}
|
|
2729
2791
|
}
|
|
@@ -3624,6 +3686,10 @@ var PROD_GATE_TIER_B = /* @__PURE__ */ new Set([
|
|
|
3624
3686
|
// tier so a CSM or agent can run a MEC implementation's bulk assignment on
|
|
3625
3687
|
// prod (DR-54064).
|
|
3626
3688
|
"recon-member-batch|set",
|
|
3689
|
+
// clear-assignee --file: N `recon-member|clear` writes under one token, same
|
|
3690
|
+
// reasoning — each is undone by set-assignee, already-empty slots are
|
|
3691
|
+
// skipped, and the plan shows the sign-offs it resets (DR-54260).
|
|
3692
|
+
"recon-member-batch|clear",
|
|
3627
3693
|
"recon-member|clear",
|
|
3628
3694
|
"recon-member|set-due-date",
|
|
3629
3695
|
"recon-member|sign",
|
|
@@ -42254,31 +42320,51 @@ function validateRow(row, idx) {
|
|
|
42254
42320
|
|
|
42255
42321
|
// src/commands/_reconciliationAssigneeCsv.ts
|
|
42256
42322
|
import { promises as fs17 } from "fs";
|
|
42257
|
-
var
|
|
42258
|
-
var
|
|
42259
|
-
var
|
|
42323
|
+
var ASSIGNEE_REQUIRED = ["account_id", "type", "user_id"];
|
|
42324
|
+
var ASSIGNEE_OPTIONAL = ["user_name", "user_email"];
|
|
42325
|
+
var CLEAR_REQUIRED = ["account_id", "type"];
|
|
42260
42326
|
async function readAssigneeCsv(path11) {
|
|
42261
|
-
|
|
42327
|
+
return parseAssigneeCsvText(await readFileArg(path11));
|
|
42328
|
+
}
|
|
42329
|
+
async function readClearAssigneeCsv(path11) {
|
|
42330
|
+
return parseClearAssigneeCsvText(await readFileArg(path11));
|
|
42331
|
+
}
|
|
42332
|
+
async function readFileArg(path11) {
|
|
42262
42333
|
try {
|
|
42263
|
-
|
|
42334
|
+
return await fs17.readFile(path11, "utf8");
|
|
42264
42335
|
} catch (err) {
|
|
42265
42336
|
const m = err instanceof Error ? err.message : String(err);
|
|
42266
42337
|
throw new Error(`read --file: ${m}`);
|
|
42267
42338
|
}
|
|
42268
|
-
return parseAssigneeCsvText(raw);
|
|
42269
42339
|
}
|
|
42270
42340
|
function parseAssigneeCsvText(text2) {
|
|
42341
|
+
return parseSlotRows(text2, ASSIGNEE_REQUIRED, ASSIGNEE_OPTIONAL, (slot, cell) => {
|
|
42342
|
+
const rawUserId = cell("user_id");
|
|
42343
|
+
const userId = Number(rawUserId);
|
|
42344
|
+
if (!/^\d+$/.test(rawUserId) || !Number.isSafeInteger(userId) || userId <= 0) {
|
|
42345
|
+
throw new Error(
|
|
42346
|
+
`--file: line ${slot.lineNo}: invalid user_id "${rawUserId}" (expected positive integer)`
|
|
42347
|
+
);
|
|
42348
|
+
}
|
|
42349
|
+
return { ...slot, userId, userName: cell("user_name"), userEmail: cell("user_email") };
|
|
42350
|
+
});
|
|
42351
|
+
}
|
|
42352
|
+
function parseClearAssigneeCsvText(text2) {
|
|
42353
|
+
return parseSlotRows(text2, CLEAR_REQUIRED, [], (slot) => slot);
|
|
42354
|
+
}
|
|
42355
|
+
function parseSlotRows(text2, required2, optional, extra) {
|
|
42271
42356
|
const withoutBom = text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
|
|
42272
42357
|
const rows = parseCsvRows(withoutBom);
|
|
42273
42358
|
if (rows.length === 0) {
|
|
42274
42359
|
throw new Error("--file: CSV is empty");
|
|
42275
42360
|
}
|
|
42361
|
+
const known = /* @__PURE__ */ new Set([...required2, ...optional]);
|
|
42276
42362
|
const header = rows[0].map((h) => h.trim());
|
|
42277
42363
|
const col = /* @__PURE__ */ new Map();
|
|
42278
42364
|
header.forEach((h, i) => {
|
|
42279
|
-
if (!
|
|
42365
|
+
if (!known.has(h)) {
|
|
42280
42366
|
throw new Error(
|
|
42281
|
-
`--file: unknown CSV column "${h}" \u2014 expected ${[...
|
|
42367
|
+
`--file: unknown CSV column "${h}" \u2014 expected ${[...required2, ...optional].join(",")}`
|
|
42282
42368
|
);
|
|
42283
42369
|
}
|
|
42284
42370
|
if (col.has(h)) {
|
|
@@ -42286,7 +42372,7 @@ function parseAssigneeCsvText(text2) {
|
|
|
42286
42372
|
}
|
|
42287
42373
|
col.set(h, i);
|
|
42288
42374
|
});
|
|
42289
|
-
for (const c of
|
|
42375
|
+
for (const c of required2) {
|
|
42290
42376
|
if (!col.has(c)) {
|
|
42291
42377
|
throw new Error(`--file: CSV header is missing required column "${c}"`);
|
|
42292
42378
|
}
|
|
@@ -42295,30 +42381,24 @@ function parseAssigneeCsvText(text2) {
|
|
|
42295
42381
|
if (dataRows.length === 0) {
|
|
42296
42382
|
throw new Error("--file: CSV has no data rows (header only)");
|
|
42297
42383
|
}
|
|
42298
|
-
const cell = (cells, name) => {
|
|
42299
|
-
const i = col.get(name);
|
|
42300
|
-
return i === void 0 ? "" : (cells[i] ?? "").trim();
|
|
42301
|
-
};
|
|
42302
42384
|
const seenSlots = /* @__PURE__ */ new Map();
|
|
42303
42385
|
const out = [];
|
|
42304
42386
|
for (const { cells, lineNo } of dataRows) {
|
|
42305
|
-
const
|
|
42387
|
+
const cell = (name) => {
|
|
42388
|
+
const i = col.get(name);
|
|
42389
|
+
return i === void 0 ? "" : (cells[i] ?? "").trim();
|
|
42390
|
+
};
|
|
42391
|
+
const accountId = cell("account_id");
|
|
42306
42392
|
if (accountId === "") {
|
|
42307
42393
|
throw new Error(`--file: line ${lineNo}: "account_id" is required`);
|
|
42308
42394
|
}
|
|
42309
|
-
const rawType = cell(
|
|
42395
|
+
const rawType = cell("type");
|
|
42310
42396
|
if (rawType !== "Preparer" && rawType !== "Reviewer") {
|
|
42311
42397
|
throw new Error(
|
|
42312
42398
|
`--file: line ${lineNo}: invalid type "${rawType}" (expected "Preparer" or "Reviewer")`
|
|
42313
42399
|
);
|
|
42314
42400
|
}
|
|
42315
|
-
const
|
|
42316
|
-
const userId = Number(rawUserId);
|
|
42317
|
-
if (!/^\d+$/.test(rawUserId) || !Number.isSafeInteger(userId) || userId <= 0) {
|
|
42318
|
-
throw new Error(
|
|
42319
|
-
`--file: line ${lineNo}: invalid user_id "${rawUserId}" (expected positive integer)`
|
|
42320
|
-
);
|
|
42321
|
-
}
|
|
42401
|
+
const row = extra({ lineNo, accountId, type: rawType }, cell);
|
|
42322
42402
|
const slot = `${accountId}:${rawType}`;
|
|
42323
42403
|
const prior = seenSlots.get(slot);
|
|
42324
42404
|
if (prior !== void 0) {
|
|
@@ -42327,14 +42407,7 @@ function parseAssigneeCsvText(text2) {
|
|
|
42327
42407
|
);
|
|
42328
42408
|
}
|
|
42329
42409
|
seenSlots.set(slot, lineNo);
|
|
42330
|
-
out.push(
|
|
42331
|
-
lineNo,
|
|
42332
|
-
accountId,
|
|
42333
|
-
type: rawType,
|
|
42334
|
-
userId,
|
|
42335
|
-
userName: cell(cells, "user_name"),
|
|
42336
|
-
userEmail: cell(cells, "user_email")
|
|
42337
|
-
});
|
|
42410
|
+
out.push(row);
|
|
42338
42411
|
}
|
|
42339
42412
|
return out;
|
|
42340
42413
|
}
|
|
@@ -44025,52 +44098,186 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
|
|
|
44025
44098
|
account_id: id,
|
|
44026
44099
|
members: applyAssignments(byId.get(id), changesByAccount.get(id))
|
|
44027
44100
|
}));
|
|
44028
|
-
|
|
44029
|
-
|
|
44030
|
-
|
|
44031
|
-
|
|
44032
|
-
|
|
44033
|
-
|
|
44034
|
-
|
|
44035
|
-
|
|
44036
|
-
|
|
44037
|
-
|
|
44038
|
-
|
|
44039
|
-
|
|
44040
|
-
|
|
44041
|
-
|
|
44042
|
-
|
|
44043
|
-
|
|
44044
|
-
|
|
44045
|
-
|
|
44046
|
-
|
|
44047
|
-
|
|
44101
|
+
const applied = await writeMemberChunks(cfg, byId, items, chunkSize);
|
|
44102
|
+
if (cfg.asJson) {
|
|
44103
|
+
const changed = accountIds.flatMap((id) => changesByAccount.get(id).map(toJsonRow));
|
|
44104
|
+
writeJSONIndented(cfg.out, { changed, unchanged: unchanged.map(toJsonRow) });
|
|
44105
|
+
return;
|
|
44106
|
+
}
|
|
44107
|
+
cfg.out.write(
|
|
44108
|
+
`Set ${changeCount} role${plural(changeCount)} on ${applied} account${plural(applied)} (${unchanged.length} unchanged).
|
|
44109
|
+
`
|
|
44110
|
+
);
|
|
44111
|
+
}
|
|
44112
|
+
});
|
|
44113
|
+
}
|
|
44114
|
+
function clearBatchActionId(date, rows) {
|
|
44115
|
+
const canonical = rows.map((r) => [r.accountId, r.type]);
|
|
44116
|
+
return createHash8("sha256").update(JSON.stringify(["clear", date, canonical])).digest("hex").slice(0, 12);
|
|
44117
|
+
}
|
|
44118
|
+
async function runReconciliationAccountsClearAssigneeBatch(cfg) {
|
|
44119
|
+
const action = {
|
|
44120
|
+
resource: "recon-member-batch",
|
|
44121
|
+
id: clearBatchActionId(cfg.date, cfg.rows),
|
|
44122
|
+
verb: "clear"
|
|
44123
|
+
};
|
|
44124
|
+
await preflightConfirmToken(action, cfg.token);
|
|
44125
|
+
extendForBatch(cfg.extendTimeout, 1);
|
|
44126
|
+
const accounts = await getAccounts(
|
|
44127
|
+
cfg.client,
|
|
44128
|
+
{ date: cfg.date },
|
|
44129
|
+
{ signal: cfg.signal }
|
|
44130
|
+
);
|
|
44131
|
+
const byId = new Map(accounts.map((a) => [a.account_id, a]));
|
|
44132
|
+
const problems = [];
|
|
44133
|
+
for (const row of cfg.rows) {
|
|
44134
|
+
const account = byId.get(row.accountId);
|
|
44135
|
+
if (account === void 0) {
|
|
44136
|
+
problems.push(`line ${row.lineNo}: account "${row.accountId}" not found at --date`);
|
|
44137
|
+
} else if (account.members.length > 0 && !account.members.some((m) => m.type === row.type)) {
|
|
44138
|
+
problems.push(
|
|
44139
|
+
`line ${row.lineNo}: account "${row.accountId}" has a partial member set (${account.members.map((m) => m.type).join(", ")}) and is missing ${row.type}`
|
|
44140
|
+
);
|
|
44141
|
+
}
|
|
44142
|
+
}
|
|
44143
|
+
if (problems.length > 0) {
|
|
44144
|
+
const shown = problems.slice(0, 10);
|
|
44145
|
+
const more = problems.length > 10 ? [`\u2026 (+${problems.length - 10} more)`] : [];
|
|
44146
|
+
throw new Error(
|
|
44147
|
+
`--file: ${problems.length} row${plural(problems.length)} cannot be applied; nothing was written:
|
|
44148
|
+
` + [...shown, ...more].join("\n ") + `
|
|
44149
|
+
verify the date matches the accounts' reporting period (try \`dr reconciliation accounts list --date \u2026\`)`
|
|
44150
|
+
);
|
|
44151
|
+
}
|
|
44152
|
+
const clearsByAccount = /* @__PURE__ */ new Map();
|
|
44153
|
+
const unchanged = [];
|
|
44154
|
+
for (const row of cfg.rows) {
|
|
44155
|
+
const current = byId.get(row.accountId).members.find((m) => m.type === row.type);
|
|
44156
|
+
if (current === void 0 || current.user_id === 0) {
|
|
44157
|
+
unchanged.push(row);
|
|
44158
|
+
continue;
|
|
44159
|
+
}
|
|
44160
|
+
const list28 = clearsByAccount.get(row.accountId) ?? [];
|
|
44161
|
+
list28.push({ row, current });
|
|
44162
|
+
clearsByAccount.set(row.accountId, list28);
|
|
44163
|
+
}
|
|
44164
|
+
const clearCount = cfg.rows.length - unchanged.length;
|
|
44165
|
+
const n = cfg.rows.length;
|
|
44166
|
+
if (clearCount === 0) {
|
|
44167
|
+
if (cfg.asJson) {
|
|
44168
|
+
writeJSONIndented(cfg.out, {
|
|
44169
|
+
changed: [],
|
|
44170
|
+
unchanged: unchanged.map((r) => ({ account_id: r.accountId, type: r.type }))
|
|
44171
|
+
});
|
|
44172
|
+
return;
|
|
44173
|
+
}
|
|
44174
|
+
cfg.out.write(
|
|
44175
|
+
`Nothing to clear: all ${n} row${plural(n)} name a slot that is already Unassigned or does not exist yet.
|
|
44176
|
+
`
|
|
44177
|
+
);
|
|
44178
|
+
return;
|
|
44179
|
+
}
|
|
44180
|
+
const chunkSize = cfg.chunkSize ?? ASSIGNEE_BATCH_CHUNK_SIZE;
|
|
44181
|
+
const accountIds = [...clearsByAccount.keys()];
|
|
44182
|
+
const requests = Math.ceil(accountIds.length / chunkSize);
|
|
44183
|
+
await twoStepConfirm({
|
|
44184
|
+
out: cfg.out,
|
|
44185
|
+
action,
|
|
44186
|
+
token: cfg.token,
|
|
44187
|
+
server: cfg.serverFlag,
|
|
44188
|
+
confirmCmd: cfg.confirmCmd,
|
|
44189
|
+
describe: async () => {
|
|
44190
|
+
let signOffsReset = 0;
|
|
44191
|
+
const preview = [];
|
|
44192
|
+
for (const id of accountIds) {
|
|
44193
|
+
const account = byId.get(id);
|
|
44194
|
+
for (const { row, current } of clearsByAccount.get(id)) {
|
|
44195
|
+
if (current.is_sign_off) signOffsReset += 1;
|
|
44196
|
+
if (preview.length < 5) {
|
|
44197
|
+
preview.push(
|
|
44198
|
+
` ${account.account_name} (${id}) ${row.type}: ${current.name} (user ${current.user_id}) \u2192 Unassigned`
|
|
44048
44199
|
);
|
|
44049
44200
|
}
|
|
44050
44201
|
}
|
|
44051
|
-
try {
|
|
44052
|
-
await updateAccountMembers(cfg.client, chunk, { signal: cfg.signal });
|
|
44053
|
-
} catch (err) {
|
|
44054
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
44055
|
-
throw new Error(
|
|
44056
|
-
`PATCH failed after ${applied} of ${items.length} account${plural(items.length)} were written (accounts ${i + 1}\u2013${i + chunk.length} may be partially applied): ${msg}
|
|
44057
|
-
Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
|
|
44058
|
-
);
|
|
44059
|
-
}
|
|
44060
|
-
applied += chunk.length;
|
|
44061
44202
|
}
|
|
44203
|
+
const lines = [
|
|
44204
|
+
`rows: ${n}`,
|
|
44205
|
+
`to clear: ${clearCount} role${plural(clearCount)} on ${accountIds.length} account${plural(accountIds.length)} (${requests} PATCH request${plural(requests)})`,
|
|
44206
|
+
`unchanged: ${unchanged.length} (slot already Unassigned, or the account has no member slots \u2014 skipped)`,
|
|
44207
|
+
`sign-offs reset: ${signOffsReset}`,
|
|
44208
|
+
"changes:",
|
|
44209
|
+
...preview
|
|
44210
|
+
];
|
|
44211
|
+
if (clearCount > preview.length) lines.push(` \u2026 (+${clearCount - preview.length} more)`);
|
|
44212
|
+
return lines;
|
|
44213
|
+
},
|
|
44214
|
+
execute: async () => {
|
|
44215
|
+
const items = accountIds.map((id) => {
|
|
44216
|
+
const cleared = new Set(clearsByAccount.get(id).map((c) => c.row.type));
|
|
44217
|
+
return {
|
|
44218
|
+
account_id: id,
|
|
44219
|
+
members: byId.get(id).members.map(
|
|
44220
|
+
(m) => cleared.has(m.type) ? unassignedMember(m.type, m.due_date) : memberSummaryToWire(m)
|
|
44221
|
+
)
|
|
44222
|
+
};
|
|
44223
|
+
});
|
|
44224
|
+
const applied = await writeMemberChunks(cfg, byId, items, chunkSize);
|
|
44062
44225
|
if (cfg.asJson) {
|
|
44063
|
-
|
|
44064
|
-
|
|
44226
|
+
writeJSONIndented(cfg.out, {
|
|
44227
|
+
changed: accountIds.flatMap(
|
|
44228
|
+
(id) => clearsByAccount.get(id).map(({ row, current }) => ({
|
|
44229
|
+
account_id: row.accountId,
|
|
44230
|
+
type: row.type,
|
|
44231
|
+
previous_user_id: current.user_id
|
|
44232
|
+
}))
|
|
44233
|
+
),
|
|
44234
|
+
unchanged: unchanged.map((r) => ({ account_id: r.accountId, type: r.type }))
|
|
44235
|
+
});
|
|
44065
44236
|
return;
|
|
44066
44237
|
}
|
|
44067
44238
|
cfg.out.write(
|
|
44068
|
-
`
|
|
44239
|
+
`Cleared ${clearCount} role${plural(clearCount)} on ${applied} account${plural(applied)} (${unchanged.length} unchanged).
|
|
44069
44240
|
`
|
|
44070
44241
|
);
|
|
44071
44242
|
}
|
|
44072
44243
|
});
|
|
44073
44244
|
}
|
|
44245
|
+
async function writeMemberChunks(ctx, planned, items, chunkSize) {
|
|
44246
|
+
const requests = Math.ceil(items.length / chunkSize);
|
|
44247
|
+
extendForBatch(ctx.extendTimeout, requests * 2);
|
|
44248
|
+
let applied = 0;
|
|
44249
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
44250
|
+
const chunk = items.slice(i, i + chunkSize);
|
|
44251
|
+
let drift;
|
|
44252
|
+
try {
|
|
44253
|
+
drift = await chunkDrift(ctx, planned, chunk);
|
|
44254
|
+
} catch (err) {
|
|
44255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44256
|
+
throw new Error(
|
|
44257
|
+
`re-read failed after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent: ${msg}
|
|
44258
|
+
Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
|
|
44259
|
+
);
|
|
44260
|
+
}
|
|
44261
|
+
if (drift.length > 0) {
|
|
44262
|
+
throw new Error(
|
|
44263
|
+
`halted after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent:
|
|
44264
|
+
` + drift.join("\n ") + `
|
|
44265
|
+
The tenant changed since the plan. Re-run the same command without --confirm-token: rows that landed plan as unchanged. If accounts are gone, the trial balance was rebuilt and their ids changed \u2014 wait for it to settle and rebuild the file from \`dr reconciliation accounts list\`.`
|
|
44266
|
+
);
|
|
44267
|
+
}
|
|
44268
|
+
try {
|
|
44269
|
+
await updateAccountMembers(ctx.client, chunk, { signal: ctx.signal });
|
|
44270
|
+
} catch (err) {
|
|
44271
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44272
|
+
throw new Error(
|
|
44273
|
+
`PATCH failed after ${applied} of ${items.length} account${plural(items.length)} were written (accounts ${i + 1}\u2013${i + chunk.length} may be partially applied): ${msg}
|
|
44274
|
+
Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
|
|
44275
|
+
);
|
|
44276
|
+
}
|
|
44277
|
+
applied += chunk.length;
|
|
44278
|
+
}
|
|
44279
|
+
return applied;
|
|
44280
|
+
}
|
|
44074
44281
|
async function chunkDrift(cfg, planned, chunk) {
|
|
44075
44282
|
const now = await getAccounts(cfg.client, { date: cfg.date }, { signal: cfg.signal });
|
|
44076
44283
|
const nowById = new Map(now.map((a) => [a.account_id, a]));
|
|
@@ -44579,16 +44786,69 @@ function wireReconciliation(program) {
|
|
|
44579
44786
|
);
|
|
44580
44787
|
addServerFlag(setAssigneeCmd);
|
|
44581
44788
|
addAccountFlag(setAssigneeCmd);
|
|
44582
|
-
const clearAssigneeCmd = accounts.command("clear-assignee").description(
|
|
44583
|
-
|
|
44584
|
-
|
|
44789
|
+
const clearAssigneeCmd = accounts.command("clear-assignee").summary("Clear the Preparer or Reviewer slot of one account, or many with --file").description(
|
|
44790
|
+
[
|
|
44791
|
+
"Clear the Preparer or Reviewer slot (sets user_id=0 + Unassigned name/email; preserves due_date, resets sign-off)",
|
|
44792
|
+
"",
|
|
44793
|
+
"Bulk form \u2014 one plan and one token for many slots:",
|
|
44794
|
+
" dr reconciliation accounts clear-assignee --file slots.csv --date 2025-04-30",
|
|
44795
|
+
" # CSV header: account_id,type \u2014 one row per (account, role)"
|
|
44796
|
+
].join("\n")
|
|
44797
|
+
).argument("[account_uuid]", "account uuid; omit with --file").requiredOption("--date <YYYY-MM-DD>", "Reporting date").option("--type <s>", "Preparer | Reviewer (case-sensitive); required without --file").option(
|
|
44798
|
+
"--file <path>",
|
|
44799
|
+
"CSV of slots to clear (header account_id,type); one plan + one token for the whole file"
|
|
44800
|
+
).option(
|
|
44801
|
+
"--json",
|
|
44802
|
+
"Output the cleared member record(s) as JSON instead of a confirmation",
|
|
44803
|
+
false
|
|
44804
|
+
).option("--timeout <seconds>", `With --file: ${BATCH_TIMEOUT_FLAG_HELP}`).option(
|
|
44585
44805
|
"--confirm-token <token>",
|
|
44586
44806
|
"Token from a prior plan run \u2014 required to actually clear",
|
|
44587
44807
|
""
|
|
44588
44808
|
).action(
|
|
44589
44809
|
async (accountUuid, opts) => {
|
|
44590
44810
|
const date = parseDateFlag(opts.date ?? "", "--date");
|
|
44591
|
-
|
|
44811
|
+
if (opts.file !== void 0) {
|
|
44812
|
+
const conflicting = [
|
|
44813
|
+
accountUuid !== void 0 ? "<account_uuid>" : "",
|
|
44814
|
+
opts.type !== void 0 ? "--type" : ""
|
|
44815
|
+
].filter((s) => s !== "");
|
|
44816
|
+
if (conflicting.length > 0) {
|
|
44817
|
+
throw new Error(
|
|
44818
|
+
`--file cannot be combined with ${conflicting.join(", ")} (the CSV carries them per row)`
|
|
44819
|
+
);
|
|
44820
|
+
}
|
|
44821
|
+
const rows = await readClearAssigneeCsv(opts.file);
|
|
44822
|
+
await preflightConfirmToken(
|
|
44823
|
+
{ resource: "recon-member-batch", id: clearBatchActionId(date, rows), verb: "clear" },
|
|
44824
|
+
opts.confirmToken
|
|
44825
|
+
);
|
|
44826
|
+
const serverKey2 = resolveServer(opts.server);
|
|
44827
|
+
const confirmCmd2 = `dr reconciliation accounts clear-assignee --file ${JSON.stringify(opts.file)} --date ${opts.date}` + (opts.json === true ? " --json" : "") + (opts.timeout !== void 0 ? ` --timeout ${opts.timeout}` : "") + ` --server ${serverKey2}`;
|
|
44828
|
+
const setup2 = await setupCall(opts.server, opts.account, batchSetupOptions(opts.timeout));
|
|
44829
|
+
try {
|
|
44830
|
+
await runReconciliationAccountsClearAssigneeBatch({
|
|
44831
|
+
client: setup2.client,
|
|
44832
|
+
extendTimeout: setup2.extendTimeout,
|
|
44833
|
+
date,
|
|
44834
|
+
rows,
|
|
44835
|
+
token: opts.confirmToken,
|
|
44836
|
+
confirmCmd: confirmCmd2,
|
|
44837
|
+
serverFlag: serverKey2,
|
|
44838
|
+
asJson: opts.json === true,
|
|
44839
|
+
signal: setup2.signal,
|
|
44840
|
+
out: process.stdout
|
|
44841
|
+
});
|
|
44842
|
+
} finally {
|
|
44843
|
+
setup2.cleanup();
|
|
44844
|
+
}
|
|
44845
|
+
return;
|
|
44846
|
+
}
|
|
44847
|
+
if (accountUuid === void 0) {
|
|
44848
|
+
throw new Error("missing <account_uuid> (or pass --file <csv> for many slots)");
|
|
44849
|
+
}
|
|
44850
|
+
if (opts.type === void 0) throw new Error("required option '--type <s>' not specified");
|
|
44851
|
+
const type = parseMemberTypeFlag(opts.type, "--type");
|
|
44592
44852
|
await preflightConfirmToken(
|
|
44593
44853
|
{
|
|
44594
44854
|
resource: "recon-member",
|
|
@@ -44749,7 +45009,7 @@ import * as nodePath2 from "path";
|
|
|
44749
45009
|
var API_PREFIX = "/api/v1";
|
|
44750
45010
|
var OPERATOR_EMAIL_HEADER = "X-Operator-Email";
|
|
44751
45011
|
function withOperatorAttribution(client, options) {
|
|
44752
|
-
const operatorEmail = isImpersonatedSession(client.session) ? client.session.impersonatorEmail : client.session.cookieless === true && !hasApiKey(client.session) && client.session.interactiveLogin !== true ? void 0 : client.email;
|
|
45012
|
+
const operatorEmail = isImpersonatedSession(client.session) ? client.session.impersonatorEmail : client.session.cookieless === true && !hasApiKey(client.session) && !hasApiKeyEnvVar(client.session) && client.session.interactiveLogin !== true ? void 0 : client.email;
|
|
44753
45013
|
if (operatorEmail === void 0) return options ?? {};
|
|
44754
45014
|
return {
|
|
44755
45015
|
...options,
|
package/dist/scopes-map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"cliVersion": "1.6.
|
|
3
|
+
"cliVersion": "1.6.98",
|
|
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": {
|
|
@@ -45,8 +45,8 @@ Account assignment / sign-off (all take the bank-account UUID via
|
|
|
45
45
|
resets. Every account is checked before anything is written, and the
|
|
46
46
|
write goes out in PATCHes of 100 accounts. If one fails, the error says
|
|
47
47
|
how many accounts landed; re-run the same file without a token and the
|
|
48
|
-
new plan covers only the rest. Before each PATCH
|
|
49
|
-
|
|
48
|
+
new plan covers only the rest. Before each PATCH, the accounts are
|
|
49
|
+
re-read; if any account in the next chunk is gone (a
|
|
50
50
|
trial-balance rebuild re-mints account ids) or its member slots changed
|
|
51
51
|
since the plan, the run halts, says how many accounts landed, and names
|
|
52
52
|
the drifted accounts. Gone accounts mean the file must be rebuilt from
|
|
@@ -55,6 +55,13 @@ Account assignment / sign-off (all take the bank-account UUID via
|
|
|
55
55
|
- `reconciliation accounts clear-assignee <account_uuid> --date <YYYY-MM-DD>
|
|
56
56
|
--type Preparer|Reviewer` — unassigns (preserves due_date, resets sign-off).
|
|
57
57
|
Two-step confirm.
|
|
58
|
+
- `reconciliation accounts clear-assignee --file <csv> --date <YYYY-MM-DD>` —
|
|
59
|
+
the bulk form. Header `account_id,type`, one row per (account, role); the
|
|
60
|
+
user columns of a set-assignee file are rejected, so a set-assignee
|
|
61
|
+
manifest cannot be passed here by mistake. One plan and one token cover the
|
|
62
|
+
whole file. Slots already Unassigned, and accounts with no member slots,
|
|
63
|
+
are skipped. Same 100-account PATCHes, per-chunk re-check, and resume
|
|
64
|
+
behaviour as `set-assignee --file`.
|
|
58
65
|
- `reconciliation accounts set-due-date <account_uuid> --date <YYYY-MM-DD>
|
|
59
66
|
--type Preparer|Reviewer (--due-date <YYYY-MM-DD> | --epoch <n> | --clear)`.
|
|
60
67
|
Two-step confirm.
|
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.98",
|
|
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.21",
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"@napi-rs/keyring": "^1.3.0",
|
|
23
23
|
"commander": "^12.1.0",
|