@gravity-rail/cli 0.53.1 → 0.55.1
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 +10 -0
- package/dist/{account-verify-phone-ABHHHGCC.js → account-verify-phone-OXHKM36Z.js} +3 -3
- package/dist/{chunk-FSY2BM4J.js → chunk-73INPBWK.js} +211 -33
- package/dist/chunk-73INPBWK.js.map +1 -0
- package/dist/{chunk-FQMZ2EBF.js → chunk-RAFKHKG2.js} +2 -2
- package/dist/cli-utils.d.ts +23 -4
- package/dist/cli-utils.js +1 -1
- package/dist/gr.js +2994 -94
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +4 -1
- package/dist/lib.js +819 -35
- package/dist/lib.js.map +1 -1
- package/package.json +6 -4
- package/dist/chunk-FSY2BM4J.js.map +0 -1
- /package/dist/{account-verify-phone-ABHHHGCC.js.map → account-verify-phone-OXHKM36Z.js.map} +0 -0
- /package/dist/{chunk-FQMZ2EBF.js.map → chunk-RAFKHKG2.js.map} +0 -0
package/README.md
CHANGED
|
@@ -40,6 +40,15 @@ own confidential client. Both kinds of profile refresh normally; nothing needs t
|
|
|
40
40
|
be re-authorized when a server gains the pinned client, though the next
|
|
41
41
|
`gr login` on that profile will switch to it.
|
|
42
42
|
|
|
43
|
+
Access tokens refresh automatically. When several `gr` commands on one machine
|
|
44
|
+
need a refresh at the same time, one of them exchanges the refresh token while
|
|
45
|
+
the others wait briefly and reuse the result, so parallel runs no longer sign
|
|
46
|
+
each other out. A refresh that fails for a temporary reason (network error,
|
|
47
|
+
timeout, or server error) keeps your stored credentials and exits with an
|
|
48
|
+
error; retry the command. Only when the server rejects the stored refresh token
|
|
49
|
+
is that one profile cleared, and `gr login` (or `gr auth login --name <profile>`)
|
|
50
|
+
signs it back in.
|
|
51
|
+
|
|
43
52
|
### Multi-account profiles
|
|
44
53
|
|
|
45
54
|
Like `aws --profile` / `gh auth switch`, the public CLI supports multiple named accounts:
|
|
@@ -308,6 +317,7 @@ not forwarded, as it may contain sensitive data. Use `resume.chatUuid` /
|
|
|
308
317
|
| `calendars` | Scheduling — calendars, events, event types, availability, Google sync |
|
|
309
318
|
| `qualifications` | Skills evaluation — requirements, submissions, scoring |
|
|
310
319
|
| `queries` | Unified Query Engine — fields, validate, preview, run, syntax reference |
|
|
320
|
+
| `data-reports` | Form Reports — preview a report as the Agent would run it |
|
|
311
321
|
|
|
312
322
|
### Global (no workspace)
|
|
313
323
|
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
confirmAccountPhoneVerify,
|
|
3
3
|
requireAuth,
|
|
4
4
|
startAccountPhoneVerify
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-RAFKHKG2.js";
|
|
6
|
+
import "./chunk-73INPBWK.js";
|
|
7
7
|
import "./chunk-3T7A5PSF.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/account-verify-phone.ts
|
|
@@ -38,4 +38,4 @@ async function promptLine(question) {
|
|
|
38
38
|
export {
|
|
39
39
|
accountVerifyPhoneCommand
|
|
40
40
|
};
|
|
41
|
-
//# sourceMappingURL=account-verify-phone-
|
|
41
|
+
//# sourceMappingURL=account-verify-phone-OXHKM36Z.js.map
|
|
@@ -230,6 +230,76 @@ function getEnvironmentConfig(env, options = {}) {
|
|
|
230
230
|
import { createServer } from "http";
|
|
231
231
|
import { createHash, randomBytes } from "crypto";
|
|
232
232
|
import { exec } from "child_process";
|
|
233
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
234
|
+
import { dirname as dirname2 } from "path";
|
|
235
|
+
import lockfile from "proper-lockfile";
|
|
236
|
+
|
|
237
|
+
// package.json
|
|
238
|
+
var package_default = {
|
|
239
|
+
name: "@gravity-rail/cli",
|
|
240
|
+
version: "0.55.1",
|
|
241
|
+
description: "Gravity Rail CLI \u2014 manage workspaces from the command line",
|
|
242
|
+
type: "module",
|
|
243
|
+
bin: {
|
|
244
|
+
gr: "./dist/gr.js"
|
|
245
|
+
},
|
|
246
|
+
exports: {
|
|
247
|
+
".": {
|
|
248
|
+
types: "./dist/lib.d.ts",
|
|
249
|
+
import: "./dist/lib.js"
|
|
250
|
+
},
|
|
251
|
+
"./lib": {
|
|
252
|
+
types: "./dist/lib.d.ts",
|
|
253
|
+
import: "./dist/lib.js"
|
|
254
|
+
},
|
|
255
|
+
"./cli-utils": {
|
|
256
|
+
types: "./dist/cli-utils.d.ts",
|
|
257
|
+
import: "./dist/cli-utils.js"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
files: [
|
|
261
|
+
"dist"
|
|
262
|
+
],
|
|
263
|
+
license: "MIT",
|
|
264
|
+
repository: {
|
|
265
|
+
type: "git",
|
|
266
|
+
url: "https://developer.gravityrail.com"
|
|
267
|
+
},
|
|
268
|
+
homepage: "https://developer.gravityrail.com",
|
|
269
|
+
keywords: [
|
|
270
|
+
"gravity-rail",
|
|
271
|
+
"cli",
|
|
272
|
+
"healthcare",
|
|
273
|
+
"workspace"
|
|
274
|
+
],
|
|
275
|
+
engines: {
|
|
276
|
+
node: ">=18"
|
|
277
|
+
},
|
|
278
|
+
scripts: {
|
|
279
|
+
build: "tsup",
|
|
280
|
+
"generate:skill-docs": "tsx scripts/generate-skill-docs.ts",
|
|
281
|
+
clean: "rm -rf dist",
|
|
282
|
+
typecheck: "tsc --noEmit",
|
|
283
|
+
test: "jest --forceExit",
|
|
284
|
+
"test:ci": "jest --forceExit --passWithNoTests --coverage --coverageReporters=json-summary --coverageReporters=text-summary"
|
|
285
|
+
},
|
|
286
|
+
dependencies: {
|
|
287
|
+
"@gravity-rail/sdk": "workspace:^",
|
|
288
|
+
"js-yaml": "^5.4.3",
|
|
289
|
+
"proper-lockfile": "^4.1.2"
|
|
290
|
+
},
|
|
291
|
+
devDependencies: {
|
|
292
|
+
"@gravity-rail/sandbox": "workspace:*",
|
|
293
|
+
"@types/jest": "^30.0.0",
|
|
294
|
+
"@types/node": "^26.6.5",
|
|
295
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
296
|
+
jest: "^30.5.2",
|
|
297
|
+
"ts-jest": "29.4.9",
|
|
298
|
+
tsup: "^8.5.1",
|
|
299
|
+
typescript: "^6.0.3",
|
|
300
|
+
undici: "6.28.1"
|
|
301
|
+
}
|
|
302
|
+
};
|
|
233
303
|
|
|
234
304
|
// src/cli-utils/credentials-manager.ts
|
|
235
305
|
import {
|
|
@@ -582,6 +652,15 @@ var CredentialsManager = class {
|
|
|
582
652
|
|
|
583
653
|
// src/cli-utils/auth.ts
|
|
584
654
|
var ACCESS_TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
655
|
+
var TOKEN_REQUEST_USER_AGENT = `gravityrail-cli/${package_default.version}`;
|
|
656
|
+
var REFRESH_REQUEST_TIMEOUT_MS = 15e3;
|
|
657
|
+
var REFRESH_LOCK_RETRIES = { retries: 23, factor: 2, minTimeout: 100, maxTimeout: 1e3 };
|
|
658
|
+
var REFRESH_LOCK_FAILED = "Could not lock the credential store to refresh the access token (another gr process may still be refreshing). Stored credentials were kept; retry the command.";
|
|
659
|
+
var REFRESH_REQUEST_FAILED = "Token refresh did not complete (network error or timeout). Stored credentials were kept; retry the command.";
|
|
660
|
+
var REFRESH_FAILED = "Token refresh failed. Stored credentials were kept; retry the command, and log in again if it keeps failing.";
|
|
661
|
+
var REFRESH_SUPERSEDED = "Token refresh was rejected, but the stored credentials changed meanwhile and were kept. Retry the command.";
|
|
662
|
+
var REFRESH_BAD_RESPONSE = "Token refresh returned an unusable response. Stored credentials were kept; retry the command.";
|
|
663
|
+
var REFRESH_SAVE_FAILED = "Token refresh succeeded, but the new credentials could not be saved. Retry the command, and log in again if it keeps failing.";
|
|
585
664
|
function stderr(msg) {
|
|
586
665
|
process.stderr.write(msg + "\n");
|
|
587
666
|
}
|
|
@@ -604,7 +683,7 @@ function validateTokenResponse(tokenData) {
|
|
|
604
683
|
if (tokenData.expires_in != null) {
|
|
605
684
|
const raw = Number(tokenData.expires_in);
|
|
606
685
|
if (!Number.isFinite(raw) || raw < 60 || raw > 31536e3) {
|
|
607
|
-
stderr(
|
|
686
|
+
stderr(" Warning: unusual expires_in value in the token response, using default");
|
|
608
687
|
} else {
|
|
609
688
|
expiresIn = raw;
|
|
610
689
|
}
|
|
@@ -825,7 +904,8 @@ async function performOAuthFlow(options) {
|
|
|
825
904
|
method: "POST",
|
|
826
905
|
headers: {
|
|
827
906
|
...auth.headers,
|
|
828
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
907
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
908
|
+
"User-Agent": TOKEN_REQUEST_USER_AGENT
|
|
829
909
|
},
|
|
830
910
|
body: new URLSearchParams({
|
|
831
911
|
grant_type: "authorization_code",
|
|
@@ -862,53 +942,150 @@ async function performOAuthFlow(options) {
|
|
|
862
942
|
return { accessToken, credentials: creds };
|
|
863
943
|
}
|
|
864
944
|
}
|
|
865
|
-
|
|
866
|
-
|
|
945
|
+
function parseOAuthErrorCode(body) {
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(body);
|
|
948
|
+
if (parsed && typeof parsed === "object") {
|
|
949
|
+
const code = parsed.error;
|
|
950
|
+
if (typeof code === "string") return code;
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
function isFreshEnough(creds) {
|
|
957
|
+
return !creds.expiresAt || new Date(creds.expiresAt).getTime() > Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
|
|
958
|
+
}
|
|
959
|
+
async function acquireRefreshLock(credentialsPath) {
|
|
960
|
+
try {
|
|
961
|
+
const cacheDir = dirname2(credentialsPath);
|
|
962
|
+
if (!existsSync3(cacheDir)) {
|
|
963
|
+
mkdirSync2(cacheDir, { recursive: true, mode: 448 });
|
|
964
|
+
}
|
|
965
|
+
return await lockfile.lock(credentialsPath, {
|
|
966
|
+
realpath: false,
|
|
967
|
+
retries: REFRESH_LOCK_RETRIES,
|
|
968
|
+
// The library default throws from a timer, which would crash the
|
|
969
|
+
// command. Losing the lock mid-refresh is worth a warning, not that.
|
|
970
|
+
// The library has already dropped its in-process record of the lock
|
|
971
|
+
// by the time this runs, so the `release()` in `refreshAccessToken`'s
|
|
972
|
+
// `finally` is refused (ENOTACQUIRED) and can never remove a lock
|
|
973
|
+
// another process has since taken.
|
|
974
|
+
onCompromised: () => {
|
|
975
|
+
stderr(
|
|
976
|
+
" Warning: lost the credential refresh lock; another gr process may refresh at the same time"
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
} catch {
|
|
981
|
+
throw new Error(REFRESH_LOCK_FAILED);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function refreshAccessToken(envConfig, _creds, credentialsManager, env, profile) {
|
|
985
|
+
const profileName = credentialsManager.resolveProfileName(profile, env);
|
|
986
|
+
const release = await acquireRefreshLock(credentialsManager.getCredentialsFilePath(env));
|
|
987
|
+
try {
|
|
988
|
+
return await refreshUnderLock(envConfig, credentialsManager, env, profileName);
|
|
989
|
+
} finally {
|
|
990
|
+
await release().catch(() => void 0);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function refreshUnderLock(envConfig, credentialsManager, env, profileName) {
|
|
994
|
+
const stored = credentialsManager.load(env, profileName);
|
|
995
|
+
if (stored?.accessToken && isFreshEnough(stored)) {
|
|
996
|
+
return stored;
|
|
997
|
+
}
|
|
998
|
+
if (!stored?.refreshToken || !stored.clientId) {
|
|
867
999
|
stderr(" Cannot refresh: missing credentials (re-login required)");
|
|
868
1000
|
return null;
|
|
869
1001
|
}
|
|
870
|
-
const
|
|
871
|
-
const
|
|
872
|
-
|
|
1002
|
+
const presentedRefreshToken = stored.refreshToken;
|
|
1003
|
+
const auth = tokenRequestAuth(stored.clientId, stored.clientSecret);
|
|
1004
|
+
let response;
|
|
873
1005
|
try {
|
|
874
|
-
|
|
1006
|
+
response = await fetch(`${envConfig.apiUrl}/api/v2/oauth/token`, {
|
|
875
1007
|
method: "POST",
|
|
876
1008
|
headers: {
|
|
877
1009
|
...auth.headers,
|
|
878
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1010
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1011
|
+
"User-Agent": TOKEN_REQUEST_USER_AGENT
|
|
879
1012
|
},
|
|
880
1013
|
body: new URLSearchParams({
|
|
881
1014
|
grant_type: "refresh_token",
|
|
882
|
-
refresh_token:
|
|
1015
|
+
refresh_token: presentedRefreshToken,
|
|
883
1016
|
...auth.params
|
|
884
|
-
})
|
|
1017
|
+
}),
|
|
1018
|
+
// Bounds the request and the body reads below. Never retried: a
|
|
1019
|
+
// second POST of the same token is itself a reuse.
|
|
1020
|
+
//
|
|
1021
|
+
// Known residual: an abort only stops *this* side. If the server
|
|
1022
|
+
// commits the rotation after the deadline, the response carrying the
|
|
1023
|
+
// successor is lost and the stored token is spent; the next refresh
|
|
1024
|
+
// of it is a genuine invalid_grant and clears the profile. The client
|
|
1025
|
+
// cannot recover an unseen successor; the deadline is long enough
|
|
1026
|
+
// that this needs a token endpoint stalled for 15 s, not a slow one.
|
|
1027
|
+
signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS)
|
|
885
1028
|
});
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1029
|
+
} catch {
|
|
1030
|
+
throw new Error(REFRESH_REQUEST_FAILED);
|
|
1031
|
+
}
|
|
1032
|
+
if (!response.ok) {
|
|
1033
|
+
let body;
|
|
1034
|
+
try {
|
|
1035
|
+
body = await response.text();
|
|
1036
|
+
} catch {
|
|
1037
|
+
throw new Error(REFRESH_REQUEST_FAILED);
|
|
1038
|
+
}
|
|
1039
|
+
if (response.status === 400 && parseOAuthErrorCode(body) === "invalid_grant") {
|
|
1040
|
+
const current = credentialsManager.load(env, profileName);
|
|
1041
|
+
if (!current) {
|
|
1042
|
+
stderr(" The server rejected the stored refresh token; re-login required");
|
|
1043
|
+
return null;
|
|
893
1044
|
}
|
|
894
|
-
|
|
1045
|
+
if (current.refreshToken === presentedRefreshToken) {
|
|
1046
|
+
credentialsManager.clear(env, profileName);
|
|
1047
|
+
stderr(" The server rejected the stored refresh token; re-login required");
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
throw new Error(REFRESH_SUPERSEDED);
|
|
1051
|
+
}
|
|
1052
|
+
if (parseOAuthErrorCode(body) === "invalid_client") {
|
|
1053
|
+
stderr(" The server no longer recognises this profile's OAuth client; re-login required");
|
|
895
1054
|
return null;
|
|
896
1055
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
stderr(` Token refresh error: ${err instanceof Error ? err.message : "unknown"}`);
|
|
909
|
-
credentialsManager.clear(env, profileName);
|
|
1056
|
+
throw new Error(REFRESH_FAILED);
|
|
1057
|
+
}
|
|
1058
|
+
let tokens;
|
|
1059
|
+
try {
|
|
1060
|
+
tokens = validateTokenResponse(await response.json());
|
|
1061
|
+
} catch {
|
|
1062
|
+
throw new Error(REFRESH_BAD_RESPONSE);
|
|
1063
|
+
}
|
|
1064
|
+
const latest = credentialsManager.load(env, profileName);
|
|
1065
|
+
if (!latest) {
|
|
1066
|
+
stderr(" Cannot refresh: missing credentials (re-login required)");
|
|
910
1067
|
return null;
|
|
911
1068
|
}
|
|
1069
|
+
if (latest.clientId !== stored.clientId || latest.refreshToken !== presentedRefreshToken) {
|
|
1070
|
+
if (latest.accessToken && isFreshEnough(latest)) {
|
|
1071
|
+
return latest;
|
|
1072
|
+
}
|
|
1073
|
+
throw new Error(REFRESH_SUPERSEDED);
|
|
1074
|
+
}
|
|
1075
|
+
const updated = {
|
|
1076
|
+
...latest,
|
|
1077
|
+
accessToken: tokens.accessToken,
|
|
1078
|
+
// A server may omit `refresh_token` when it does not rotate; the
|
|
1079
|
+
// presented token then stays valid.
|
|
1080
|
+
refreshToken: tokens.refreshToken ?? presentedRefreshToken,
|
|
1081
|
+
expiresAt: new Date(Date.now() + tokens.expiresIn * 1e3).toISOString()
|
|
1082
|
+
};
|
|
1083
|
+
try {
|
|
1084
|
+
credentialsManager.save(env, updated, profileName);
|
|
1085
|
+
} catch {
|
|
1086
|
+
throw new Error(REFRESH_SAVE_FAILED);
|
|
1087
|
+
}
|
|
1088
|
+
return updated;
|
|
912
1089
|
}
|
|
913
1090
|
async function resolveNonInteractiveAuth(options) {
|
|
914
1091
|
const { env, envConfig, credentialsManager, profile } = options;
|
|
@@ -1443,6 +1620,7 @@ export {
|
|
|
1443
1620
|
CALLBACK_PATH,
|
|
1444
1621
|
REDIRECT_URI,
|
|
1445
1622
|
WorktreeConfigError,
|
|
1623
|
+
isRecord,
|
|
1446
1624
|
parsePublishedTarget,
|
|
1447
1625
|
classifyCheckout,
|
|
1448
1626
|
getEnvironmentConfig,
|
|
@@ -1470,4 +1648,4 @@ export {
|
|
|
1470
1648
|
getTextDelta,
|
|
1471
1649
|
getErrorMessage
|
|
1472
1650
|
};
|
|
1473
|
-
//# sourceMappingURL=chunk-
|
|
1651
|
+
//# sourceMappingURL=chunk-73INPBWK.js.map
|