@duyquangnvx/create-agent-cli 0.31.0 → 0.32.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 +7 -0
  2. package/dist/bin.js +118 -49
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## v0.32.0 (2026-11-10)
8
+
9
+ ### Added
10
+
11
+ - 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.
12
+ - 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.
13
+
7
14
  ## v0.31.0 (2026-11-10)
8
15
 
9
16
  ### 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],
@@ -7829,12 +7829,37 @@ function loginRequired(names, rerun) {
7829
7829
  next: [...names.map(loginStep), rerun]
7830
7830
  });
7831
7831
  }
7832
+ function loginAsking(name) {
7833
+ return {
7834
+ subject: `signing in to ${name}`,
7835
+ denied: () => approvalDenied(name),
7836
+ expired: () => approvalExpired(name)
7837
+ };
7838
+ }
7839
+ /** A Grant's Approval, whose expiry the rerun of the Command heals with a new code. */
7840
+ function grantAsking(name, scope, rerun) {
7841
+ const subject = `the grant of ${scope} from ${name}`;
7842
+ return {
7843
+ subject,
7844
+ denied: () => new CliError({
7845
+ code: APPROVAL_DENIED,
7846
+ message: `the person refused ${subject}`,
7847
+ hint: "ask the person whether to go ahead, and run the Command again only if they agree"
7848
+ }),
7849
+ expired: () => new CliError({
7850
+ code: APPROVAL_EXPIRED,
7851
+ message: `the code for ${subject} expired before it was approved`,
7852
+ hint: "run the Next step for a new code, and pass its link and code to the person",
7853
+ next: [rerun]
7854
+ })
7855
+ };
7856
+ }
7832
7857
  /** The wait ran out before the person approved: the same Approval stays pending for the rerun. */
7833
- function approvalPending(name, approval, rerun) {
7858
+ function approvalPending({ subject }, approval, rerun) {
7834
7859
  const { verificationUri, userCode, verificationUriComplete } = approval;
7835
7860
  return new CliError({
7836
7861
  code: APPROVAL_PENDING,
7837
- message: `signing in to ${name} waits for the code ${userCode} to be approved at ${verificationUriComplete ?? verificationUri}`,
7862
+ message: `${subject} waits for the code ${userCode} to be approved at ${verificationUriComplete ?? verificationUri}`,
7838
7863
  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
7864
  details: {
7840
7865
  verificationUri,
@@ -7951,7 +7976,7 @@ function mustDeclare(code, table) {
7951
7976
  //#region ../agent-cli/dist/define-command.js
7952
7977
  /** Throws when the Command's declaration breaks a rule its type cannot express; `label` names it. */
7953
7978
  function checkCommand(command, label, table, logins) {
7954
- checkLogins(command.logins ?? [], label, logins);
7979
+ checkLogins([...command.logins ?? [], ...command.grants ?? []], label, logins);
7955
7980
  checkNoStandardFlags(command, label);
7956
7981
  checkForce(command, label);
7957
7982
  checkArrayArgLast(label, command.args);
@@ -7999,6 +8024,14 @@ function cliErrorOf(code, init, options) {
7999
8024
  }, options);
8000
8025
  }
8001
8026
  //#endregion
8027
+ //#region ../agent-cli/dist/login/declared.js
8028
+ /** The declaration of the Login `name` for a run in `env`. */
8029
+ function declaredLogin(logins, name, env) {
8030
+ const login = logins[name];
8031
+ if (login === void 0) throw new Error(`${name} is not a Login the CLI declares`);
8032
+ return typeof login === "function" ? login(env) : login;
8033
+ }
8034
+ //#endregion
8002
8035
  //#region ../agent-cli/dist/login/store.js
8003
8036
  const storedLoginSchema = object({
8004
8037
  accessToken: string(),
@@ -8015,16 +8048,20 @@ const approvalSchema = object({
8015
8048
  interval: number$1()
8016
8049
  });
8017
8050
  const KEY_LENGTH = 16;
8018
- function keyOf({ tokenEndpoint, clientId }) {
8019
- return createHash("sha256").update(`${tokenEndpoint}\n${clientId}`).digest("hex").slice(0, KEY_LENGTH);
8051
+ function keyOf(...parts) {
8052
+ return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, KEY_LENGTH);
8020
8053
  }
8021
8054
  function loginFilesOf(userFile, login) {
8022
- const key = keyOf(login);
8055
+ const key = keyOf(login.tokenEndpoint, login.clientId);
8023
8056
  return {
8024
8057
  stored: userFile(`login-${key}`, storedLoginSchema),
8025
8058
  approval: userFile(`approval-${key}`, approvalSchema)
8026
8059
  };
8027
8060
  }
8061
+ /** The Approval pending for a Grant of `scope`, kept apart from the Login's and every other scope's. */
8062
+ function grantApprovalOf(userFile, { tokenEndpoint, clientId }, scope) {
8063
+ return userFile(`grant-${keyOf(tokenEndpoint, clientId, scope)}`, approvalSchema);
8064
+ }
8028
8065
  /** The file's value, or `undefined` once its `expiresAt` has passed, removing the file then. */
8029
8066
  async function readUnexpired(file, clock) {
8030
8067
  const value = await file.read();
@@ -8172,11 +8209,12 @@ function tokenScopeOf(ctx) {
8172
8209
  }
8173
8210
  //#endregion
8174
8211
  //#region ../agent-cli/dist/login/doctor-rows.js
8175
- function loginRow(name, login, runAs) {
8212
+ function loginRow(name, logins, runAs) {
8176
8213
  return {
8177
8214
  name: `login ${name}`,
8178
8215
  advisory: true,
8179
8216
  run: async (ctx) => {
8217
+ const login = declaredLogin(logins, name, ctx.env);
8180
8218
  const set = setSecretOf(login, tokenScopeOf(ctx).secret);
8181
8219
  if (set !== void 0) return {
8182
8220
  state: "ok",
@@ -8197,16 +8235,16 @@ function loginRow(name, login, runAs) {
8197
8235
  }
8198
8236
  /** One `doctor` row per Login: where its token comes from, and `login` as the fix with none. */
8199
8237
  function loginRows(logins, runAs) {
8200
- return Object.entries(logins).map(([name, login]) => loginRow(name, login, runAs));
8238
+ return Object.keys(logins).map((name) => loginRow(name, logins, runAs));
8201
8239
  }
8202
8240
  //#endregion
8203
8241
  //#region ../agent-cli/dist/version.js
8204
8242
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
8205
8243
  function buildStamp() {
8206
8244
  return {
8207
- "sha": "7dd915f",
8245
+ "sha": "8907e9d",
8208
8246
  "dirty": false,
8209
- "builtAt": "2026-11-10T03:40:31.000Z"
8247
+ "builtAt": "2026-11-10T09:08:31.000Z"
8210
8248
  };
8211
8249
  }
8212
8250
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -10050,13 +10088,6 @@ function doctorCommand(cli, launch) {
10050
10088
  };
10051
10089
  }
10052
10090
  //#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
10091
  //#region ../agent-cli/dist/login/device-flow.js
10061
10092
  const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
10062
10093
  /** RFC 8628 §3.2: the interval when the server gives none. */
@@ -10086,7 +10117,7 @@ async function startApproval(login, { fetch, clock }) {
10086
10117
  interval: device.interval ?? DEFAULT_INTERVAL_S
10087
10118
  };
10088
10119
  }
10089
- async function pollApproval(name, login, { approval, takenUp }, scope, signal) {
10120
+ async function pollApproval(asking, login, { approval, takenUp }, scope, signal) {
10090
10121
  const form = {
10091
10122
  grant_type: DEVICE_CODE_GRANT,
10092
10123
  device_code: approval.deviceCode,
@@ -10100,23 +10131,23 @@ async function pollApproval(name, login, { approval, takenUp }, scope, signal) {
10100
10131
  if (granted.success) return storedLoginOf(granted.data, scope.clock);
10101
10132
  const { error } = parsedAnswer(errorSchema, body, login.tokenEndpoint);
10102
10133
  if (error === "slow_down") intervalS += SLOW_DOWN_S;
10103
- else if (error !== "authorization_pending") throw refusal(name, login, error);
10134
+ else if (error !== "authorization_pending") throw refusal(asking, login, error);
10104
10135
  }
10105
10136
  }
10106
10137
  /** 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);
10138
+ function refusal(asking, login, error) {
10139
+ if (error === "access_denied") return asking.denied();
10140
+ if (error === "expired_token" || error === "invalid_grant") return asking.expired();
10110
10141
  return /* @__PURE__ */ new Error(`${login.tokenEndpoint} refused the device code with ${error}`);
10111
10142
  }
10112
10143
  function endsApproval(error) {
10113
10144
  return error instanceof CliError && (error.code === "APPROVAL_DENIED" || error.code === "APPROVAL_EXPIRED");
10114
10145
  }
10115
10146
  /** 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);
10147
+ async function askedApproval(login, file, scope) {
10148
+ const pending = await readUnexpired(file, scope.clock);
10118
10149
  const approval = pending ?? await startApproval(login, scope);
10119
- if (pending === void 0) await files.approval.write(approval);
10150
+ if (pending === void 0) await file.write(approval);
10120
10151
  scope.progress(`open ${approval.verificationUriComplete ?? approval.verificationUri} and approve code ${approval.userCode}`);
10121
10152
  return {
10122
10153
  approval,
@@ -10125,28 +10156,39 @@ async function askedApproval(login, files, scope) {
10125
10156
  }
10126
10157
  /** The `APPROVAL_PENDING` of the pending Approval, or of a new one, without polling it. */
10127
10158
  async function pendingApproval(name, login, files, scope, rerun) {
10128
- const { approval } = await askedApproval(login, files, scope);
10129
- return approvalPending(name, approval, rerun);
10159
+ const { approval } = await askedApproval(login, files.approval, scope);
10160
+ return approvalPending(loginAsking(name), approval, rerun);
10130
10161
  }
10131
10162
  /**
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.
10163
+ * Polls the Approval pending in `file`, or a new one, until the person approves, removing it once
10164
+ * refused or expired. An approved one stays until what it granted is kept, so a cut-off run loses nothing.
10134
10165
  */
10135
- async function signIn(name, login, files, scope, { waitS, rerun }) {
10136
- const asked = await askedApproval(login, files, scope);
10166
+ async function approved(asking, login, file, scope, { waitS, rerun }) {
10167
+ const asked = await askedApproval(login, file, scope);
10137
10168
  const wait = scope.deadline(waitS * MS_PER_S);
10138
- let stored;
10169
+ let granted;
10139
10170
  try {
10140
- stored = await pollApproval(name, login, asked, scope, wait);
10171
+ granted = await pollApproval(asking, login, asked, scope, wait);
10141
10172
  } catch (error) {
10142
- if (wait.aborted && !scope.signal.aborted) throw approvalPending(name, asked.approval, rerun);
10143
- if (endsApproval(error)) await files.approval.remove();
10173
+ if (wait.aborted && !scope.signal.aborted) throw approvalPending(asking, asked.approval, rerun);
10174
+ if (endsApproval(error)) await file.remove();
10144
10175
  throw error;
10145
10176
  }
10177
+ return granted;
10178
+ }
10179
+ /** Polls the pending Approval, or a new one, until the person approves, and stores the Login. */
10180
+ async function signIn(name, login, files, scope, wait) {
10181
+ const stored = await approved(loginAsking(name), login, files.approval, scope, wait);
10146
10182
  await files.stored.write(stored);
10147
10183
  await files.approval.remove();
10148
10184
  return stored;
10149
10185
  }
10186
+ /** Polls the Approval of the Grant's scope, or a new one, until the person approves: its token. */
10187
+ async function grant(asking, login, file, scope, wait) {
10188
+ const { accessToken } = await approved(asking, login, file, scope, wait);
10189
+ await file.remove();
10190
+ return accessToken;
10191
+ }
10150
10192
  //#endregion
10151
10193
  //#region ../agent-cli/dist/built-ins/login-name.js
10152
10194
  /** The Login a `login` or `logout` run names, or the only one; with several, the name is needed. */
@@ -10231,7 +10273,7 @@ function logoutCommand(cli, logins, names) {
10231
10273
  cliName: cli.name,
10232
10274
  command: "logout"
10233
10275
  }, given, names);
10234
- const login = declaredLogin(logins, name);
10276
+ const login = declaredLogin(logins, name, ctx.env);
10235
10277
  return {
10236
10278
  login: name,
10237
10279
  stored: await loginFilesOf(ctx.userFile, login).stored.read() !== void 0,
@@ -10240,7 +10282,7 @@ function logoutCommand(cli, logins, names) {
10240
10282
  };
10241
10283
  },
10242
10284
  apply: async ({ login: name, secret }, ctx) => {
10243
- const login = declaredLogin(logins, name);
10285
+ const login = declaredLogin(logins, name, ctx.env);
10244
10286
  const file = loginFilesOf(ctx.userFile, login).stored;
10245
10287
  const stored = await file.read();
10246
10288
  const kept = secret === void 0 ? {} : { secret };
@@ -10317,7 +10359,7 @@ function loginCommand(cli, logins, names) {
10317
10359
  cliName: cli.name,
10318
10360
  command: "login"
10319
10361
  }, given, names);
10320
- const login = declaredLogin(logins, name);
10362
+ const login = declaredLogin(logins, name, ctx.env);
10321
10363
  const token = await currentTokenOf(login, tokenScopeOf(ctx));
10322
10364
  const stored = await readStored(loginFilesOf(ctx.userFile, login).stored, ctx.clock);
10323
10365
  return {
@@ -10329,7 +10371,7 @@ function loginCommand(cli, logins, names) {
10329
10371
  },
10330
10372
  apply: async ({ login: name, wait, found }, ctx) => {
10331
10373
  if (found !== void 0) return found;
10332
- const login = declaredLogin(logins, name);
10374
+ const login = declaredLogin(logins, name, ctx.env);
10333
10375
  const files = loginFilesOf(ctx.userFile, login);
10334
10376
  const stored = await readStored(files.stored, ctx.clock);
10335
10377
  if (stored?.refreshToken !== void 0 && await refreshed(name, login, files.stored, stored.refreshToken, ctx) !== void 0) return {
@@ -12181,7 +12223,7 @@ function approvalRerun(ctx) {
12181
12223
  }
12182
12224
  async function tokensOf(names, logins, ctx, waitS) {
12183
12225
  const found = await Promise.all(names.map(async (name) => {
12184
- const login = declaredLogin(logins, name);
12226
+ const login = declaredLogin(logins, name, ctx.env);
12185
12227
  return {
12186
12228
  name,
12187
12229
  token: ctx.dryRun ? await currentTokenOf(login, tokenScopeOf(ctx)) : await freshTokenOf(name, login, ctx)
@@ -12195,7 +12237,7 @@ async function tokensOf(names, logins, ctx, waitS) {
12195
12237
  tokens.set(name, token);
12196
12238
  continue;
12197
12239
  }
12198
- const login = declaredLogin(logins, name);
12240
+ const login = declaredLogin(logins, name, ctx.env);
12199
12241
  const stored = await signIn(name, login, loginFilesOf(ctx.userFile, login), ctx, {
12200
12242
  waitS,
12201
12243
  rerun: approvalRerun(ctx)
@@ -12221,26 +12263,49 @@ async function afterRejection(error, tokens, logins, ctx) {
12221
12263
  const name = rejectedName(error, tokens);
12222
12264
  const token = name === void 0 ? void 0 : tokens.get(name);
12223
12265
  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);
12266
+ const login = declaredLogin(logins, name, ctx.env);
12225
12267
  if (token.source.kind === "secret" && login.secret !== void 0) return secretRejected(name, login.secret, error);
12226
12268
  const files = loginFilesOf(ctx.userFile, login);
12227
12269
  await files.stored.remove();
12228
12270
  if (ctx.dryRun) return loginRequired([name], ctx.rerun());
12229
12271
  return await pendingApproval(name, login, files, ctx, approvalRerun(ctx));
12230
12272
  }
12273
+ /** `ctx.grant`: polls the Approval of `scope` for at most `waitS` seconds, and keeps no token. */
12274
+ function granting(ctx, grants, logins, waitS) {
12275
+ return async (name, { scope }) => {
12276
+ if (!grants.includes(name)) undeclaredGrant(name);
12277
+ if (ctx.dryRun) throw new Error(`ctx.grant("${name}") in a dry run, which asks for nothing: ask in apply`);
12278
+ const login = {
12279
+ ...declaredLogin(logins, name, ctx.env),
12280
+ scope
12281
+ };
12282
+ return await grant(grantAsking(name, scope, ctx.rerun({ remove: [WAIT_FLAG.flag] })), login, grantApprovalOf(ctx.userFile, login, scope), ctx, {
12283
+ waitS,
12284
+ rerun: approvalRerun(ctx)
12285
+ });
12286
+ };
12287
+ }
12231
12288
  /**
12232
12289
  * 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.
12290
+ * most `waitS` seconds, where one is missing, and whose `grant` asks for a Grant. A
12291
+ * `LOGIN_REJECTED` from `work` drops the stored Login.
12234
12292
  */
12235
- async function withLogins(ctx, names, logins, waitS, work) {
12293
+ async function withLogins(ctx, { logins: names, grants }, logins, waitS, work) {
12236
12294
  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) });
12295
+ const loggedIn = derived(ctx, {
12296
+ login: (name) => tokens.get(name)?.value ?? undeclaredLogin(name),
12297
+ grant: granting(ctx, grants, logins, waitS ?? 0)
12298
+ });
12238
12299
  try {
12239
12300
  return await work(loggedIn);
12240
12301
  } catch (error) {
12241
12302
  throw await afterRejection(error, tokens, logins, ctx);
12242
12303
  }
12243
12304
  }
12305
+ /** `ctx.grant` of a name the Command does not declare in `grants`, which only a cast reaches. */
12306
+ function undeclaredGrant(name) {
12307
+ throw new Error(`ctx.grant("${name}") names a Login the Command does not declare in grants`);
12308
+ }
12244
12309
  /** `ctx.login` of a name the Command does not declare, which only a cast reaches. */
12245
12310
  function undeclaredLogin(name) {
12246
12311
  throw new Error(`ctx.login("${name}") names a Login the Command does not declare`);
@@ -12300,7 +12365,10 @@ async function execute(command, input, unlogged, run) {
12300
12365
  envFile,
12301
12366
  fileError
12302
12367
  }, unlogged.rerun());
12303
- return await withLogins(unlogged, command.logins ?? [], run.logins, run.flags.wait, (ctx) => executeLoggedIn(command, input, ctx, run));
12368
+ return await withLogins(unlogged, {
12369
+ logins: command.logins ?? [],
12370
+ grants: command.grants ?? []
12371
+ }, run.logins, run.flags.wait, (ctx) => executeLoggedIn(command, input, ctx, run));
12304
12372
  }
12305
12373
  async function executeLoggedIn(command, input, ctx, { flags, commandLine, started, outputs: runOutputs }) {
12306
12374
  if (command.rows !== void 0) {
@@ -12540,6 +12608,7 @@ function contextOf(scope, control, { command, flags, project, deliver, latestRel
12540
12608
  ...portsBase,
12541
12609
  secret,
12542
12610
  login: undeclaredLogin,
12611
+ grant: async (name) => undeclaredGrant(name),
12543
12612
  userFile: scope.userFile,
12544
12613
  fail: cliErrorOf,
12545
12614
  deadline: (ms, options) => control.deadline(ms, options),
@@ -12898,7 +12967,7 @@ async function runProcess(cli) {
12898
12967
  //#region package.json
12899
12968
  var package_default = {
12900
12969
  name: "@duyquangnvx/create-agent-cli",
12901
- version: "0.31.0",
12970
+ version: "0.32.0",
12902
12971
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
12903
12972
  type: "module",
12904
12973
  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.32.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.32.0",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },