@duyquangnvx/create-agent-cli 0.31.0 → 0.33.0
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/CHANGELOG.md +22 -0
- package/dist/bin.js +221 -97
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## v0.33.0 (2026-11-10)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Breaking:** `ctx.login(name)` returns `Promise<string>`, and the Runtime finds a Login when the Command first asks for it, once per run, no longer before `plan`, `run` or `start`: write `await ctx.login(name)`. A Command that lists a Login and returns before it asks runs on a machine not signed in, and one that asks with none stored fails with `APPROVAL_PENDING` from that call, or with `LOGIN_REQUIRED` in a dry run, which names that Login alone; of Logins asked for at once, only the first one missing starts an Approval. Ask before any effect a rerun must not repeat.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- A Login's device or token endpoint that answers with a body that is not JSON, or with JSON that is no OAuth answer, such as a proxy or a password page in front of the server, fails the run with the new Runtime code `NOT_OAUTH` (exit 3), naming the endpoint and its status in the message and in `details` (`login`, `endpoint`, `status`), where it failed with `INTERNAL` (exit 70). `runtimeCodes.NOT_OAUTH` is exported.
|
|
16
|
+
|
|
17
|
+
### Upgrade from v0.32.0
|
|
18
|
+
|
|
19
|
+
- Every call of `ctx.login(name)` becomes `await ctx.login(name)`: a call left without it still type-checks inside a template literal, and sends `[object Promise]` as the token.
|
|
20
|
+
- A Command with an effect before its first `ctx.login` asks first, since the rerun after an Approval starts the Command over.
|
|
21
|
+
|
|
22
|
+
## v0.32.0 (2026-11-10)
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- A Login in `defineCli({ logins })` may be a function of the run's env that returns its declaration, for a CLI whose user picks its server with a variable: `ctx.login`, its refresh, `login`, `logout` and `doctor`'s row build it from each run's `env`, and an error it throws fails the run before any request. A stored Login is kept per token endpoint and client id, so a token from one server is never sent to another. `LoginSource` and `LoginDeclarations` are exported.
|
|
27
|
+
- A Grant: a Command lists in `grants` the Logins it asks a token for one action from, and `ctx.grant(name, { scope })`, typed to those names, asks the Login's server for an Approval of that scope through the device flow, taking up the one pending for the same scope. Until the person approves it fails with `APPROVAL_PENDING` (exit 75) and the rerun with `--wait 100`; a refused one fails with `APPROVAL_DENIED` and an expired one with `APPROVAL_EXPIRED` and the rerun, both exit 2. The token is given to the run only, never stored or refreshed, and the Login's Secret does not stand in for it. `--wait <seconds>` is a Standard flag of a Command with `grants`, `defineCli` throws on a name the CLI does not declare, and `ctx.grant` throws in a dry run. `GrantOptions` is exported.
|
|
28
|
+
|
|
7
29
|
## v0.31.0 (2026-11-10)
|
|
8
30
|
|
|
9
31
|
### Added
|
package/dist/bin.js
CHANGED
|
@@ -7198,9 +7198,9 @@ const WAIT_FLAG = standardFlag("--wait", {
|
|
|
7198
7198
|
takes: "value",
|
|
7199
7199
|
schema: WAIT_SECONDS
|
|
7200
7200
|
});
|
|
7201
|
-
/** Whether `--wait` is a Standard flag of the Command: it is of one that declares a Login. */
|
|
7201
|
+
/** Whether `--wait` is a Standard flag of the Command: it is of one that declares a Login or Grant. */
|
|
7202
7202
|
function takesWait(command) {
|
|
7203
|
-
return (command.logins ?? []).length > 0;
|
|
7203
|
+
return (command.logins ?? []).length > 0 || (command.grants ?? []).length > 0;
|
|
7204
7204
|
}
|
|
7205
7205
|
const EFFECT_FLAGS = {
|
|
7206
7206
|
read: [JSON_FLAG],
|
|
@@ -7815,6 +7815,7 @@ const APPROVAL_PENDING = "APPROVAL_PENDING";
|
|
|
7815
7815
|
const APPROVAL_DENIED = "APPROVAL_DENIED";
|
|
7816
7816
|
const APPROVAL_EXPIRED = "APPROVAL_EXPIRED";
|
|
7817
7817
|
const LOGIN_REJECTED = "LOGIN_REJECTED";
|
|
7818
|
+
const NOT_OAUTH = "NOT_OAUTH";
|
|
7818
7819
|
function loginStep(name) {
|
|
7819
7820
|
return ["login", name];
|
|
7820
7821
|
}
|
|
@@ -7829,12 +7830,39 @@ function loginRequired(names, rerun) {
|
|
|
7829
7830
|
next: [...names.map(loginStep), rerun]
|
|
7830
7831
|
});
|
|
7831
7832
|
}
|
|
7833
|
+
function loginAsking(name) {
|
|
7834
|
+
return {
|
|
7835
|
+
login: name,
|
|
7836
|
+
subject: `signing in to ${name}`,
|
|
7837
|
+
denied: () => approvalDenied(name),
|
|
7838
|
+
expired: () => approvalExpired(name)
|
|
7839
|
+
};
|
|
7840
|
+
}
|
|
7841
|
+
/** A Grant's Approval, whose expiry the rerun of the Command heals with a new code. */
|
|
7842
|
+
function grantAsking(name, scope, rerun) {
|
|
7843
|
+
const subject = `the grant of ${scope} from ${name}`;
|
|
7844
|
+
return {
|
|
7845
|
+
login: name,
|
|
7846
|
+
subject,
|
|
7847
|
+
denied: () => new CliError({
|
|
7848
|
+
code: APPROVAL_DENIED,
|
|
7849
|
+
message: `the person refused ${subject}`,
|
|
7850
|
+
hint: "ask the person whether to go ahead, and run the Command again only if they agree"
|
|
7851
|
+
}),
|
|
7852
|
+
expired: () => new CliError({
|
|
7853
|
+
code: APPROVAL_EXPIRED,
|
|
7854
|
+
message: `the code for ${subject} expired before it was approved`,
|
|
7855
|
+
hint: "run the Next step for a new code, and pass its link and code to the person",
|
|
7856
|
+
next: [rerun]
|
|
7857
|
+
})
|
|
7858
|
+
};
|
|
7859
|
+
}
|
|
7832
7860
|
/** The wait ran out before the person approved: the same Approval stays pending for the rerun. */
|
|
7833
|
-
function approvalPending(
|
|
7861
|
+
function approvalPending({ subject }, approval, rerun) {
|
|
7834
7862
|
const { verificationUri, userCode, verificationUriComplete } = approval;
|
|
7835
7863
|
return new CliError({
|
|
7836
7864
|
code: APPROVAL_PENDING,
|
|
7837
|
-
message:
|
|
7865
|
+
message: `${subject} waits for the code ${userCode} to be approved at ${verificationUriComplete ?? verificationUri}`,
|
|
7838
7866
|
hint: "the person sees only your reply: write the link and the code in it, then run the Next step, which waits for the approval",
|
|
7839
7867
|
details: {
|
|
7840
7868
|
verificationUri,
|
|
@@ -7872,6 +7900,19 @@ function approvalExpired(name) {
|
|
|
7872
7900
|
next: [loginStep(name)]
|
|
7873
7901
|
});
|
|
7874
7902
|
}
|
|
7903
|
+
/** An endpoint of a Login that answered as something other than its OAuth server. */
|
|
7904
|
+
function notOAuth({ login, url: endpoint }, status, cause) {
|
|
7905
|
+
return new CliError({
|
|
7906
|
+
code: NOT_OAUTH,
|
|
7907
|
+
message: `${endpoint} answered ${status}, which is no OAuth answer`,
|
|
7908
|
+
hint: `${endpoint} does not reach the OAuth server the Login ${login} declares: a proxy or a password page may stand in front of it, or the CLI points at the wrong server; ask whoever runs ${new URL(endpoint).origin} for the URL that reaches it`,
|
|
7909
|
+
details: {
|
|
7910
|
+
login,
|
|
7911
|
+
endpoint,
|
|
7912
|
+
status
|
|
7913
|
+
}
|
|
7914
|
+
}, { cause });
|
|
7915
|
+
}
|
|
7875
7916
|
//#endregion
|
|
7876
7917
|
//#region ../agent-cli/dist/runtime-codes.js
|
|
7877
7918
|
const DOCTOR_PROBLEMS = "DOCTOR_PROBLEMS";
|
|
@@ -7915,7 +7956,8 @@ const anyCommandErrors = {
|
|
|
7915
7956
|
[APPROVAL_PENDING]: { class: "wait" },
|
|
7916
7957
|
[APPROVAL_DENIED]: { class: "operator" },
|
|
7917
7958
|
[APPROVAL_EXPIRED]: { class: "operator" },
|
|
7918
|
-
[LOGIN_REJECTED]: { class: "operator" }
|
|
7959
|
+
[LOGIN_REJECTED]: { class: "operator" },
|
|
7960
|
+
[NOT_OAUTH]: { class: "source" }
|
|
7919
7961
|
};
|
|
7920
7962
|
/**
|
|
7921
7963
|
* The Runtime codes only some Commands reach: each Command that throws one, Built-in or the CLI's
|
|
@@ -7951,7 +7993,7 @@ function mustDeclare(code, table) {
|
|
|
7951
7993
|
//#region ../agent-cli/dist/define-command.js
|
|
7952
7994
|
/** Throws when the Command's declaration breaks a rule its type cannot express; `label` names it. */
|
|
7953
7995
|
function checkCommand(command, label, table, logins) {
|
|
7954
|
-
checkLogins(command.logins ?? [], label, logins);
|
|
7996
|
+
checkLogins([...command.logins ?? [], ...command.grants ?? []], label, logins);
|
|
7955
7997
|
checkNoStandardFlags(command, label);
|
|
7956
7998
|
checkForce(command, label);
|
|
7957
7999
|
checkArrayArgLast(label, command.args);
|
|
@@ -7999,6 +8041,14 @@ function cliErrorOf(code, init, options) {
|
|
|
7999
8041
|
}, options);
|
|
8000
8042
|
}
|
|
8001
8043
|
//#endregion
|
|
8044
|
+
//#region ../agent-cli/dist/login/declared.js
|
|
8045
|
+
/** The declaration of the Login `name` for a run in `env`. */
|
|
8046
|
+
function declaredLogin(logins, name, env) {
|
|
8047
|
+
const login = logins[name];
|
|
8048
|
+
if (login === void 0) throw new Error(`${name} is not a Login the CLI declares`);
|
|
8049
|
+
return typeof login === "function" ? login(env) : login;
|
|
8050
|
+
}
|
|
8051
|
+
//#endregion
|
|
8002
8052
|
//#region ../agent-cli/dist/login/store.js
|
|
8003
8053
|
const storedLoginSchema = object({
|
|
8004
8054
|
accessToken: string(),
|
|
@@ -8015,16 +8065,20 @@ const approvalSchema = object({
|
|
|
8015
8065
|
interval: number$1()
|
|
8016
8066
|
});
|
|
8017
8067
|
const KEY_LENGTH = 16;
|
|
8018
|
-
function keyOf(
|
|
8019
|
-
return createHash("sha256").update(
|
|
8068
|
+
function keyOf(...parts) {
|
|
8069
|
+
return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, KEY_LENGTH);
|
|
8020
8070
|
}
|
|
8021
8071
|
function loginFilesOf(userFile, login) {
|
|
8022
|
-
const key = keyOf(login);
|
|
8072
|
+
const key = keyOf(login.tokenEndpoint, login.clientId);
|
|
8023
8073
|
return {
|
|
8024
8074
|
stored: userFile(`login-${key}`, storedLoginSchema),
|
|
8025
8075
|
approval: userFile(`approval-${key}`, approvalSchema)
|
|
8026
8076
|
};
|
|
8027
8077
|
}
|
|
8078
|
+
/** The Approval pending for a Grant of `scope`, kept apart from the Login's and every other scope's. */
|
|
8079
|
+
function grantApprovalOf(userFile, { tokenEndpoint, clientId }, scope) {
|
|
8080
|
+
return userFile(`grant-${keyOf(tokenEndpoint, clientId, scope)}`, approvalSchema);
|
|
8081
|
+
}
|
|
8028
8082
|
/** The file's value, or `undefined` once its `expiresAt` has passed, removing the file then. */
|
|
8029
8083
|
async function readUnexpired(file, clock) {
|
|
8030
8084
|
const value = await file.read();
|
|
@@ -8050,9 +8104,12 @@ const errorSchema = object({ error: string() });
|
|
|
8050
8104
|
function isoAfter(clock, seconds) {
|
|
8051
8105
|
return new Date(clock.now() + seconds * MS_PER_S).toISOString();
|
|
8052
8106
|
}
|
|
8053
|
-
/**
|
|
8054
|
-
|
|
8055
|
-
|
|
8107
|
+
/**
|
|
8108
|
+
* POSTs `form` to an OAuth endpoint and parses its JSON answer, whatever its status. A body that
|
|
8109
|
+
* is not JSON fails with `NOT_OAUTH`.
|
|
8110
|
+
*/
|
|
8111
|
+
async function postForm(fetch, endpoint, form, { ignoreStop } = {}) {
|
|
8112
|
+
const response = await fetch(endpoint.url, {
|
|
8056
8113
|
method: "POST",
|
|
8057
8114
|
once: false,
|
|
8058
8115
|
...ignoreStop === void 0 ? {} : { ignoreStop },
|
|
@@ -8060,15 +8117,21 @@ async function postForm(fetch, url, form, { ignoreStop } = {}) {
|
|
|
8060
8117
|
body: new URLSearchParams(form)
|
|
8061
8118
|
});
|
|
8062
8119
|
const text = await response.text();
|
|
8120
|
+
const { status } = response;
|
|
8063
8121
|
try {
|
|
8064
|
-
return
|
|
8122
|
+
return {
|
|
8123
|
+
endpoint,
|
|
8124
|
+
status,
|
|
8125
|
+
body: JSON.parse(text)
|
|
8126
|
+
};
|
|
8065
8127
|
} catch (error) {
|
|
8066
|
-
throw
|
|
8128
|
+
throw notOAuth(endpoint, status, error);
|
|
8067
8129
|
}
|
|
8068
8130
|
}
|
|
8069
|
-
|
|
8131
|
+
/** The answer's body as `schema` has it, or a `NOT_OAUTH` for one of another shape. */
|
|
8132
|
+
function parsedAnswer(schema, { endpoint, status, body }) {
|
|
8070
8133
|
const parsed = schema.safeParse(body);
|
|
8071
|
-
if (!parsed.success) throw
|
|
8134
|
+
if (!parsed.success) throw notOAuth(endpoint, status);
|
|
8072
8135
|
return parsed.data;
|
|
8073
8136
|
}
|
|
8074
8137
|
function storedLoginOf({ access_token, refresh_token, expires_in }, clock) {
|
|
@@ -8094,8 +8157,12 @@ async function refreshed(name, login, file, refreshToken, ctx) {
|
|
|
8094
8157
|
client_id: login.clientId
|
|
8095
8158
|
};
|
|
8096
8159
|
return await ctx.runToEnd(`refreshing the Login ${name}`, async () => {
|
|
8097
|
-
const
|
|
8098
|
-
|
|
8160
|
+
const endpoint = {
|
|
8161
|
+
login: name,
|
|
8162
|
+
url: login.tokenEndpoint
|
|
8163
|
+
};
|
|
8164
|
+
const answer = await postForm(ctx.fetch, endpoint, form, { ignoreStop: true });
|
|
8165
|
+
const granted = tokenSchema.safeParse(answer.body);
|
|
8099
8166
|
if (granted.success) {
|
|
8100
8167
|
const stored = {
|
|
8101
8168
|
refreshToken,
|
|
@@ -8104,7 +8171,7 @@ async function refreshed(name, login, file, refreshToken, ctx) {
|
|
|
8104
8171
|
await file.write(stored);
|
|
8105
8172
|
return stored;
|
|
8106
8173
|
}
|
|
8107
|
-
const { error } = parsedAnswer(errorSchema,
|
|
8174
|
+
const { error } = parsedAnswer(errorSchema, answer);
|
|
8108
8175
|
if (error !== "invalid_grant") throw new Error(`${login.tokenEndpoint} refused the refresh of ${name} with ${error}`);
|
|
8109
8176
|
await file.remove();
|
|
8110
8177
|
});
|
|
@@ -8172,11 +8239,12 @@ function tokenScopeOf(ctx) {
|
|
|
8172
8239
|
}
|
|
8173
8240
|
//#endregion
|
|
8174
8241
|
//#region ../agent-cli/dist/login/doctor-rows.js
|
|
8175
|
-
function loginRow(name,
|
|
8242
|
+
function loginRow(name, logins, runAs) {
|
|
8176
8243
|
return {
|
|
8177
8244
|
name: `login ${name}`,
|
|
8178
8245
|
advisory: true,
|
|
8179
8246
|
run: async (ctx) => {
|
|
8247
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
8180
8248
|
const set = setSecretOf(login, tokenScopeOf(ctx).secret);
|
|
8181
8249
|
if (set !== void 0) return {
|
|
8182
8250
|
state: "ok",
|
|
@@ -8197,16 +8265,16 @@ function loginRow(name, login, runAs) {
|
|
|
8197
8265
|
}
|
|
8198
8266
|
/** One `doctor` row per Login: where its token comes from, and `login` as the fix with none. */
|
|
8199
8267
|
function loginRows(logins, runAs) {
|
|
8200
|
-
return Object.
|
|
8268
|
+
return Object.keys(logins).map((name) => loginRow(name, logins, runAs));
|
|
8201
8269
|
}
|
|
8202
8270
|
//#endregion
|
|
8203
8271
|
//#region ../agent-cli/dist/version.js
|
|
8204
8272
|
/** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
|
|
8205
8273
|
function buildStamp() {
|
|
8206
8274
|
return {
|
|
8207
|
-
"sha": "
|
|
8275
|
+
"sha": "3716c19",
|
|
8208
8276
|
"dirty": false,
|
|
8209
|
-
"builtAt": "2026-11-
|
|
8277
|
+
"builtAt": "2026-11-10T16:12:55.000Z"
|
|
8210
8278
|
};
|
|
8211
8279
|
}
|
|
8212
8280
|
/** The first line of `--version`: the version, and the commit and time of a stamped build. */
|
|
@@ -10050,13 +10118,6 @@ function doctorCommand(cli, launch) {
|
|
|
10050
10118
|
};
|
|
10051
10119
|
}
|
|
10052
10120
|
//#endregion
|
|
10053
|
-
//#region ../agent-cli/dist/login/declared.js
|
|
10054
|
-
function declaredLogin(logins, name) {
|
|
10055
|
-
const login = logins[name];
|
|
10056
|
-
if (login === void 0) throw new Error(`${name} is not a Login the CLI declares`);
|
|
10057
|
-
return login;
|
|
10058
|
-
}
|
|
10059
|
-
//#endregion
|
|
10060
10121
|
//#region ../agent-cli/dist/login/device-flow.js
|
|
10061
10122
|
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
10062
10123
|
/** RFC 8628 §3.2: the interval when the server gives none. */
|
|
@@ -10071,12 +10132,18 @@ const deviceSchema = object({
|
|
|
10071
10132
|
expires_in: number$1(),
|
|
10072
10133
|
interval: number$1().optional()
|
|
10073
10134
|
});
|
|
10074
|
-
async function startApproval(login, { fetch, clock }) {
|
|
10135
|
+
async function startApproval(name, login, { fetch, clock }) {
|
|
10075
10136
|
const form = {
|
|
10076
10137
|
client_id: login.clientId,
|
|
10077
10138
|
...login.scope === void 0 ? {} : { scope: login.scope }
|
|
10078
10139
|
};
|
|
10079
|
-
const
|
|
10140
|
+
const answer = await postForm(fetch, {
|
|
10141
|
+
login: name,
|
|
10142
|
+
url: login.deviceEndpoint
|
|
10143
|
+
}, form);
|
|
10144
|
+
const refused = errorSchema.safeParse(answer.body);
|
|
10145
|
+
if (refused.success) throw new Error(`${login.deviceEndpoint} refused the device request with ${refused.data.error}`);
|
|
10146
|
+
const device = parsedAnswer(deviceSchema, answer);
|
|
10080
10147
|
return {
|
|
10081
10148
|
deviceCode: device.device_code,
|
|
10082
10149
|
userCode: device.user_code,
|
|
@@ -10086,37 +10153,41 @@ async function startApproval(login, { fetch, clock }) {
|
|
|
10086
10153
|
interval: device.interval ?? DEFAULT_INTERVAL_S
|
|
10087
10154
|
};
|
|
10088
10155
|
}
|
|
10089
|
-
async function pollApproval(
|
|
10156
|
+
async function pollApproval(asking, login, { approval, takenUp }, scope, signal) {
|
|
10090
10157
|
const form = {
|
|
10091
10158
|
grant_type: DEVICE_CODE_GRANT,
|
|
10092
10159
|
device_code: approval.deviceCode,
|
|
10093
10160
|
client_id: login.clientId
|
|
10094
10161
|
};
|
|
10162
|
+
const endpoint = {
|
|
10163
|
+
login: asking.login,
|
|
10164
|
+
url: login.tokenEndpoint
|
|
10165
|
+
};
|
|
10095
10166
|
let intervalS = approval.interval;
|
|
10096
10167
|
for (let first = true;; first = false) {
|
|
10097
10168
|
if (!(first && takenUp)) await scope.clock.sleep(intervalS * MS_PER_S, signal);
|
|
10098
|
-
const
|
|
10099
|
-
const granted = tokenSchema.safeParse(body);
|
|
10169
|
+
const answer = await postForm(scope.fetch, endpoint, form);
|
|
10170
|
+
const granted = tokenSchema.safeParse(answer.body);
|
|
10100
10171
|
if (granted.success) return storedLoginOf(granted.data, scope.clock);
|
|
10101
|
-
const { error } = parsedAnswer(errorSchema,
|
|
10172
|
+
const { error } = parsedAnswer(errorSchema, answer);
|
|
10102
10173
|
if (error === "slow_down") intervalS += SLOW_DOWN_S;
|
|
10103
|
-
else if (error !== "authorization_pending") throw refusal(
|
|
10174
|
+
else if (error !== "authorization_pending") throw refusal(asking, login, error);
|
|
10104
10175
|
}
|
|
10105
10176
|
}
|
|
10106
10177
|
/** The error a token endpoint's answer other than pending or slow down ends the polling with. */
|
|
10107
|
-
function refusal(
|
|
10108
|
-
if (error === "access_denied") return
|
|
10109
|
-
if (error === "expired_token" || error === "invalid_grant") return
|
|
10178
|
+
function refusal(asking, login, error) {
|
|
10179
|
+
if (error === "access_denied") return asking.denied();
|
|
10180
|
+
if (error === "expired_token" || error === "invalid_grant") return asking.expired();
|
|
10110
10181
|
return /* @__PURE__ */ new Error(`${login.tokenEndpoint} refused the device code with ${error}`);
|
|
10111
10182
|
}
|
|
10112
10183
|
function endsApproval(error) {
|
|
10113
10184
|
return error instanceof CliError && (error.code === "APPROVAL_DENIED" || error.code === "APPROVAL_EXPIRED");
|
|
10114
10185
|
}
|
|
10115
10186
|
/** Takes up the pending Approval, or asks for one and keeps it as a User file, and prints its code. */
|
|
10116
|
-
async function askedApproval(login,
|
|
10117
|
-
const pending = await readUnexpired(
|
|
10118
|
-
const approval = pending ?? await startApproval(login, scope);
|
|
10119
|
-
if (pending === void 0) await
|
|
10187
|
+
async function askedApproval(name, login, file, scope) {
|
|
10188
|
+
const pending = await readUnexpired(file, scope.clock);
|
|
10189
|
+
const approval = pending ?? await startApproval(name, login, scope);
|
|
10190
|
+
if (pending === void 0) await file.write(approval);
|
|
10120
10191
|
scope.progress(`open ${approval.verificationUriComplete ?? approval.verificationUri} and approve code ${approval.userCode}`);
|
|
10121
10192
|
return {
|
|
10122
10193
|
approval,
|
|
@@ -10125,28 +10196,39 @@ async function askedApproval(login, files, scope) {
|
|
|
10125
10196
|
}
|
|
10126
10197
|
/** The `APPROVAL_PENDING` of the pending Approval, or of a new one, without polling it. */
|
|
10127
10198
|
async function pendingApproval(name, login, files, scope, rerun) {
|
|
10128
|
-
const { approval } = await askedApproval(login, files, scope);
|
|
10129
|
-
return approvalPending(name, approval, rerun);
|
|
10199
|
+
const { approval } = await askedApproval(name, login, files.approval, scope);
|
|
10200
|
+
return approvalPending(loginAsking(name), approval, rerun);
|
|
10130
10201
|
}
|
|
10131
10202
|
/**
|
|
10132
|
-
* Polls the pending
|
|
10133
|
-
*
|
|
10203
|
+
* Polls the Approval pending in `file`, or a new one, until the person approves, removing it once
|
|
10204
|
+
* refused or expired. An approved one stays until what it granted is kept, so a cut-off run loses nothing.
|
|
10134
10205
|
*/
|
|
10135
|
-
async function
|
|
10136
|
-
const asked = await askedApproval(login,
|
|
10206
|
+
async function approved(asking, login, file, scope, { waitS, rerun }) {
|
|
10207
|
+
const asked = await askedApproval(asking.login, login, file, scope);
|
|
10137
10208
|
const wait = scope.deadline(waitS * MS_PER_S);
|
|
10138
|
-
let
|
|
10209
|
+
let granted;
|
|
10139
10210
|
try {
|
|
10140
|
-
|
|
10211
|
+
granted = await pollApproval(asking, login, asked, scope, wait);
|
|
10141
10212
|
} catch (error) {
|
|
10142
|
-
if (wait.aborted && !scope.signal.aborted) throw approvalPending(
|
|
10143
|
-
if (endsApproval(error)) await
|
|
10213
|
+
if (wait.aborted && !scope.signal.aborted) throw approvalPending(asking, asked.approval, rerun);
|
|
10214
|
+
if (endsApproval(error)) await file.remove();
|
|
10144
10215
|
throw error;
|
|
10145
10216
|
}
|
|
10217
|
+
return granted;
|
|
10218
|
+
}
|
|
10219
|
+
/** Polls the pending Approval, or a new one, until the person approves, and stores the Login. */
|
|
10220
|
+
async function signIn(name, login, files, scope, wait) {
|
|
10221
|
+
const stored = await approved(loginAsking(name), login, files.approval, scope, wait);
|
|
10146
10222
|
await files.stored.write(stored);
|
|
10147
10223
|
await files.approval.remove();
|
|
10148
10224
|
return stored;
|
|
10149
10225
|
}
|
|
10226
|
+
/** Polls the Approval of the Grant's scope, or a new one, until the person approves: its token. */
|
|
10227
|
+
async function grant(asking, login, file, scope, wait) {
|
|
10228
|
+
const { accessToken } = await approved(asking, login, file, scope, wait);
|
|
10229
|
+
await file.remove();
|
|
10230
|
+
return accessToken;
|
|
10231
|
+
}
|
|
10150
10232
|
//#endregion
|
|
10151
10233
|
//#region ../agent-cli/dist/built-ins/login-name.js
|
|
10152
10234
|
/** The Login a `login` or `logout` run names, or the only one; with several, the name is needed. */
|
|
@@ -10231,7 +10313,7 @@ function logoutCommand(cli, logins, names) {
|
|
|
10231
10313
|
cliName: cli.name,
|
|
10232
10314
|
command: "logout"
|
|
10233
10315
|
}, given, names);
|
|
10234
|
-
const login = declaredLogin(logins, name);
|
|
10316
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
10235
10317
|
return {
|
|
10236
10318
|
login: name,
|
|
10237
10319
|
stored: await loginFilesOf(ctx.userFile, login).stored.read() !== void 0,
|
|
@@ -10240,7 +10322,7 @@ function logoutCommand(cli, logins, names) {
|
|
|
10240
10322
|
};
|
|
10241
10323
|
},
|
|
10242
10324
|
apply: async ({ login: name, secret }, ctx) => {
|
|
10243
|
-
const login = declaredLogin(logins, name);
|
|
10325
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
10244
10326
|
const file = loginFilesOf(ctx.userFile, login).stored;
|
|
10245
10327
|
const stored = await file.read();
|
|
10246
10328
|
const kept = secret === void 0 ? {} : { secret };
|
|
@@ -10317,7 +10399,7 @@ function loginCommand(cli, logins, names) {
|
|
|
10317
10399
|
cliName: cli.name,
|
|
10318
10400
|
command: "login"
|
|
10319
10401
|
}, given, names);
|
|
10320
|
-
const login = declaredLogin(logins, name);
|
|
10402
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
10321
10403
|
const token = await currentTokenOf(login, tokenScopeOf(ctx));
|
|
10322
10404
|
const stored = await readStored(loginFilesOf(ctx.userFile, login).stored, ctx.clock);
|
|
10323
10405
|
return {
|
|
@@ -10329,7 +10411,7 @@ function loginCommand(cli, logins, names) {
|
|
|
10329
10411
|
},
|
|
10330
10412
|
apply: async ({ login: name, wait, found }, ctx) => {
|
|
10331
10413
|
if (found !== void 0) return found;
|
|
10332
|
-
const login = declaredLogin(logins, name);
|
|
10414
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
10333
10415
|
const files = loginFilesOf(ctx.userFile, login);
|
|
10334
10416
|
const stored = await readStored(files.stored, ctx.clock);
|
|
10335
10417
|
if (stored?.refreshToken !== void 0 && await refreshed(name, login, files.stored, stored.refreshToken, ctx) !== void 0) return {
|
|
@@ -12179,68 +12261,106 @@ function approvalRerun(ctx) {
|
|
|
12179
12261
|
add: [WAIT_FLAG.flag, String(100)]
|
|
12180
12262
|
});
|
|
12181
12263
|
}
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12264
|
+
/** The token of the Login `name`, signing in first, for at most `waitS` seconds, when none is found. */
|
|
12265
|
+
async function tokenOf(name, logins, ctx, waitS) {
|
|
12266
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
12267
|
+
const found = ctx.dryRun ? await currentTokenOf(login, tokenScopeOf(ctx)) : await freshTokenOf(name, login, ctx);
|
|
12268
|
+
if (found === void 0 && ctx.dryRun) throw loginRequired([name], ctx.rerun());
|
|
12269
|
+
const token = found ?? storedToken(await signIn(name, login, loginFilesOf(ctx.userFile, login), ctx, {
|
|
12270
|
+
waitS,
|
|
12271
|
+
rerun: approvalRerun(ctx)
|
|
12189
12272
|
}));
|
|
12190
|
-
const missing = found.flatMap(({ name, token }) => token === void 0 ? [name] : []);
|
|
12191
|
-
if (missing.length > 0 && ctx.dryRun) throw loginRequired(missing, ctx.rerun());
|
|
12192
|
-
const tokens = /* @__PURE__ */ new Map();
|
|
12193
|
-
for (const { name, token } of found) {
|
|
12194
|
-
if (token !== void 0) {
|
|
12195
|
-
tokens.set(name, token);
|
|
12196
|
-
continue;
|
|
12197
|
-
}
|
|
12198
|
-
const login = declaredLogin(logins, name);
|
|
12199
|
-
const stored = await signIn(name, login, loginFilesOf(ctx.userFile, login), ctx, {
|
|
12200
|
-
waitS,
|
|
12201
|
-
rerun: approvalRerun(ctx)
|
|
12202
|
-
});
|
|
12203
|
-
tokens.set(name, storedToken(stored));
|
|
12204
|
-
}
|
|
12205
12273
|
ctx.signal.throwIfAborted();
|
|
12206
|
-
return
|
|
12274
|
+
return token;
|
|
12275
|
+
}
|
|
12276
|
+
/**
|
|
12277
|
+
* `ctx.login`, which finds each Login once, when first asked, and the tokens it gave. A Login it
|
|
12278
|
+
* fails to find fails every Login asked for after it the same way.
|
|
12279
|
+
*/
|
|
12280
|
+
function loggingIn(ctx, names, logins, waitS) {
|
|
12281
|
+
const asked = /* @__PURE__ */ new Map();
|
|
12282
|
+
const given = /* @__PURE__ */ new Map();
|
|
12283
|
+
let turn = Promise.resolve();
|
|
12284
|
+
const find = async (name, earlier) => {
|
|
12285
|
+
await earlier;
|
|
12286
|
+
const token = await tokenOf(name, logins, ctx, waitS);
|
|
12287
|
+
given.set(name, token);
|
|
12288
|
+
return token;
|
|
12289
|
+
};
|
|
12290
|
+
const login = async (name) => {
|
|
12291
|
+
if (!names.includes(name)) undeclaredLogin(name);
|
|
12292
|
+
const asking = asked.get(name) ?? find(name, turn);
|
|
12293
|
+
asked.set(name, asking);
|
|
12294
|
+
turn = asking;
|
|
12295
|
+
return (await asking).value;
|
|
12296
|
+
};
|
|
12297
|
+
return {
|
|
12298
|
+
login,
|
|
12299
|
+
given
|
|
12300
|
+
};
|
|
12207
12301
|
}
|
|
12208
12302
|
/** The Login a `LOGIN_REJECTED` names in `details.login`, or the Command's only one. */
|
|
12209
|
-
function rejectedName(rejected,
|
|
12303
|
+
function rejectedName(rejected, names) {
|
|
12210
12304
|
const { details } = rejected;
|
|
12211
12305
|
if (typeof details === "object" && details !== null && "login" in details) return typeof details.login === "string" ? details.login : void 0;
|
|
12212
|
-
const [only, ...rest] =
|
|
12306
|
+
const [only, ...rest] = names;
|
|
12213
12307
|
return rest.length === 0 ? only : void 0;
|
|
12214
12308
|
}
|
|
12215
12309
|
/**
|
|
12216
12310
|
* What a run whose Command threw `error` fails with: a `LOGIN_REJECTED` of a stored Login drops
|
|
12217
12311
|
* it for a new Approval, one of a Secret's token fails as the operator's to fix.
|
|
12218
12312
|
*/
|
|
12219
|
-
async function afterRejection(error,
|
|
12313
|
+
async function afterRejection(error, { names, given }, logins, ctx) {
|
|
12220
12314
|
if (!(error instanceof CliError && error.code === "LOGIN_REJECTED")) return error;
|
|
12221
|
-
const name = rejectedName(error,
|
|
12222
|
-
const token = name === void 0 ? void 0 :
|
|
12223
|
-
if (name === void 0 || token === void 0) return new Error(`the Command threw ${LOGIN_REJECTED} naming no Login it
|
|
12224
|
-
const login = declaredLogin(logins, name);
|
|
12315
|
+
const name = rejectedName(error, names);
|
|
12316
|
+
const token = name === void 0 ? void 0 : given.get(name);
|
|
12317
|
+
if (name === void 0 || token === void 0) return new Error(`the Command threw ${LOGIN_REJECTED} naming no Login ctx.login gave it in details.login`, { cause: error });
|
|
12318
|
+
const login = declaredLogin(logins, name, ctx.env);
|
|
12225
12319
|
if (token.source.kind === "secret" && login.secret !== void 0) return secretRejected(name, login.secret, error);
|
|
12226
12320
|
const files = loginFilesOf(ctx.userFile, login);
|
|
12227
12321
|
await files.stored.remove();
|
|
12228
12322
|
if (ctx.dryRun) return loginRequired([name], ctx.rerun());
|
|
12229
12323
|
return await pendingApproval(name, login, files, ctx, approvalRerun(ctx));
|
|
12230
12324
|
}
|
|
12325
|
+
/** `ctx.grant`: polls the Approval of `scope` for at most `waitS` seconds, and keeps no token. */
|
|
12326
|
+
function granting(ctx, grants, logins, waitS) {
|
|
12327
|
+
return async (name, { scope }) => {
|
|
12328
|
+
if (!grants.includes(name)) undeclaredGrant(name);
|
|
12329
|
+
if (ctx.dryRun) throw new Error(`ctx.grant("${name}") in a dry run, which asks for nothing: ask in apply`);
|
|
12330
|
+
const login = {
|
|
12331
|
+
...declaredLogin(logins, name, ctx.env),
|
|
12332
|
+
scope
|
|
12333
|
+
};
|
|
12334
|
+
return await grant(grantAsking(name, scope, ctx.rerun({ remove: [WAIT_FLAG.flag] })), login, grantApprovalOf(ctx.userFile, login, scope), ctx, {
|
|
12335
|
+
waitS,
|
|
12336
|
+
rerun: approvalRerun(ctx)
|
|
12337
|
+
});
|
|
12338
|
+
};
|
|
12339
|
+
}
|
|
12231
12340
|
/**
|
|
12232
|
-
* Runs `work` with a `ctx` whose `login`
|
|
12233
|
-
* most `waitS` seconds, where
|
|
12341
|
+
* Runs `work` with a `ctx` whose `login` finds a named Login's token when first asked, signing in
|
|
12342
|
+
* first, for at most `waitS` seconds, where it is missing, and whose `grant` asks for a Grant. A
|
|
12343
|
+
* `LOGIN_REJECTED` from `work` drops the stored Login.
|
|
12234
12344
|
*/
|
|
12235
|
-
async function withLogins(ctx, names, logins, waitS, work) {
|
|
12236
|
-
const
|
|
12237
|
-
const loggedIn = derived(ctx, {
|
|
12345
|
+
async function withLogins(ctx, { logins: names, grants }, logins, waitS, work) {
|
|
12346
|
+
const { login, given } = loggingIn(ctx, names, logins, waitS ?? 0);
|
|
12347
|
+
const loggedIn = derived(ctx, {
|
|
12348
|
+
login,
|
|
12349
|
+
grant: granting(ctx, grants, logins, waitS ?? 0)
|
|
12350
|
+
});
|
|
12238
12351
|
try {
|
|
12239
12352
|
return await work(loggedIn);
|
|
12240
12353
|
} catch (error) {
|
|
12241
|
-
throw await afterRejection(error,
|
|
12354
|
+
throw await afterRejection(error, {
|
|
12355
|
+
names,
|
|
12356
|
+
given
|
|
12357
|
+
}, logins, ctx);
|
|
12242
12358
|
}
|
|
12243
12359
|
}
|
|
12360
|
+
/** `ctx.grant` of a name the Command does not declare in `grants`, which only a cast reaches. */
|
|
12361
|
+
function undeclaredGrant(name) {
|
|
12362
|
+
throw new Error(`ctx.grant("${name}") names a Login the Command does not declare in grants`);
|
|
12363
|
+
}
|
|
12244
12364
|
/** `ctx.login` of a name the Command does not declare, which only a cast reaches. */
|
|
12245
12365
|
function undeclaredLogin(name) {
|
|
12246
12366
|
throw new Error(`ctx.login("${name}") names a Login the Command does not declare`);
|
|
@@ -12300,7 +12420,10 @@ async function execute(command, input, unlogged, run) {
|
|
|
12300
12420
|
envFile,
|
|
12301
12421
|
fileError
|
|
12302
12422
|
}, unlogged.rerun());
|
|
12303
|
-
return await withLogins(unlogged,
|
|
12423
|
+
return await withLogins(unlogged, {
|
|
12424
|
+
logins: command.logins ?? [],
|
|
12425
|
+
grants: command.grants ?? []
|
|
12426
|
+
}, run.logins, run.flags.wait, (ctx) => executeLoggedIn(command, input, ctx, run));
|
|
12304
12427
|
}
|
|
12305
12428
|
async function executeLoggedIn(command, input, ctx, { flags, commandLine, started, outputs: runOutputs }) {
|
|
12306
12429
|
if (command.rows !== void 0) {
|
|
@@ -12539,7 +12662,8 @@ function contextOf(scope, control, { command, flags, project, deliver, latestRel
|
|
|
12539
12662
|
return {
|
|
12540
12663
|
...portsBase,
|
|
12541
12664
|
secret,
|
|
12542
|
-
login: undeclaredLogin,
|
|
12665
|
+
login: async (name) => undeclaredLogin(name),
|
|
12666
|
+
grant: async (name) => undeclaredGrant(name),
|
|
12543
12667
|
userFile: scope.userFile,
|
|
12544
12668
|
fail: cliErrorOf,
|
|
12545
12669
|
deadline: (ms, options) => control.deadline(ms, options),
|
|
@@ -12898,7 +13022,7 @@ async function runProcess(cli) {
|
|
|
12898
13022
|
//#region package.json
|
|
12899
13023
|
var package_default = {
|
|
12900
13024
|
name: "@duyquangnvx/create-agent-cli",
|
|
12901
|
-
version: "0.
|
|
13025
|
+
version: "0.33.0",
|
|
12902
13026
|
description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
|
|
12903
13027
|
type: "module",
|
|
12904
13028
|
engines: { "node": ">=22.12.0" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duyquangnvx/create-agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@duyquangnvx/agent-cli": "0.
|
|
24
|
+
"@duyquangnvx/agent-cli": "0.33.0",
|
|
25
25
|
"tsdown": "0.23.0",
|
|
26
26
|
"zod": "4.6.7"
|
|
27
27
|
},
|