@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bin.js +221 -97
  3. 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(name, approval, rerun) {
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: `signing in to ${name} waits for the code ${userCode} to be approved at ${verificationUriComplete ?? verificationUri}`,
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({ tokenEndpoint, clientId }) {
8019
- return createHash("sha256").update(`${tokenEndpoint}\n${clientId}`).digest("hex").slice(0, KEY_LENGTH);
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
- /** POSTs `form` to an OAuth endpoint and parses its JSON answer, whatever its status. */
8054
- async function postForm(fetch, url, form, { ignoreStop } = {}) {
8055
- const response = await fetch(url, {
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 JSON.parse(text);
8122
+ return {
8123
+ endpoint,
8124
+ status,
8125
+ body: JSON.parse(text)
8126
+ };
8065
8127
  } catch (error) {
8066
- throw new Error(`${url} answered ${response.status} with a body that is not JSON`, { cause: error });
8128
+ throw notOAuth(endpoint, status, error);
8067
8129
  }
8068
8130
  }
8069
- function parsedAnswer(schema, body, url) {
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 new Error(`${url} answered ${JSON.stringify(body)}, not an OAuth answer`);
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 body = await postForm(ctx.fetch, login.tokenEndpoint, form, { ignoreStop: true });
8098
- const granted = tokenSchema.safeParse(body);
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, body, login.tokenEndpoint);
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, login, runAs) {
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.entries(logins).map(([name, login]) => loginRow(name, login, runAs));
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": "7dd915f",
8275
+ "sha": "3716c19",
8208
8276
  "dirty": false,
8209
- "builtAt": "2026-11-10T03:40:31.000Z"
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 device = parsedAnswer(deviceSchema, await postForm(fetch, login.deviceEndpoint, form), login.deviceEndpoint);
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(name, login, { approval, takenUp }, scope, signal) {
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 body = await postForm(scope.fetch, login.tokenEndpoint, form);
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, body, login.tokenEndpoint);
10172
+ const { error } = parsedAnswer(errorSchema, answer);
10102
10173
  if (error === "slow_down") intervalS += SLOW_DOWN_S;
10103
- else if (error !== "authorization_pending") throw refusal(name, login, error);
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(name, login, error) {
10108
- if (error === "access_denied") return approvalDenied(name);
10109
- if (error === "expired_token" || error === "invalid_grant") return approvalExpired(name);
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, files, scope) {
10117
- const pending = await readUnexpired(files.approval, scope.clock);
10118
- const approval = pending ?? await startApproval(login, scope);
10119
- if (pending === void 0) await files.approval.write(approval);
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 Approval, or a new one, until the person approves, and stores the Login. A run
10133
- * that stops first leaves the Approval for the next.
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 signIn(name, login, files, scope, { waitS, rerun }) {
10136
- const asked = await askedApproval(login, files, scope);
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 stored;
10209
+ let granted;
10139
10210
  try {
10140
- stored = await pollApproval(name, login, asked, scope, wait);
10211
+ granted = await pollApproval(asking, login, asked, scope, wait);
10141
10212
  } catch (error) {
10142
- if (wait.aborted && !scope.signal.aborted) throw approvalPending(name, asked.approval, rerun);
10143
- if (endsApproval(error)) await files.approval.remove();
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
- async function tokensOf(names, logins, ctx, waitS) {
12183
- const found = await Promise.all(names.map(async (name) => {
12184
- const login = declaredLogin(logins, name);
12185
- return {
12186
- name,
12187
- token: ctx.dryRun ? await currentTokenOf(login, tokenScopeOf(ctx)) : await freshTokenOf(name, login, ctx)
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 tokens;
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, tokens) {
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] = tokens.keys();
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, tokens, logins, ctx) {
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, tokens);
12222
- const token = name === void 0 ? void 0 : tokens.get(name);
12223
- if (name === void 0 || token === void 0) return new Error(`the Command threw ${LOGIN_REJECTED} naming no Login it declares in details.login`, { cause: error });
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` gives each named Login's token, signing in first, for at
12233
- * most `waitS` seconds, where one is missing. A `LOGIN_REJECTED` from `work` drops the stored Login.
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 tokens = names.length === 0 ? /* @__PURE__ */ new Map() : await tokensOf(names, logins, ctx, waitS ?? 0);
12237
- const loggedIn = derived(ctx, { login: (name) => tokens.get(name)?.value ?? undeclaredLogin(name) });
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, tokens, logins, ctx);
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, command.logins ?? [], run.logins, run.flags.wait, (ctx) => executeLoggedIn(command, input, ctx, run));
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.31.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.31.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.31.0",
24
+ "@duyquangnvx/agent-cli": "0.33.0",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },