@duyquangnvx/create-agent-cli 0.32.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 +15 -0
  2. package/dist/bin.js +113 -58
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ 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
+
7
22
  ## v0.32.0 (2026-11-10)
8
23
 
9
24
  ### Added
package/dist/bin.js CHANGED
@@ -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
  }
@@ -7831,6 +7832,7 @@ function loginRequired(names, rerun) {
7831
7832
  }
7832
7833
  function loginAsking(name) {
7833
7834
  return {
7835
+ login: name,
7834
7836
  subject: `signing in to ${name}`,
7835
7837
  denied: () => approvalDenied(name),
7836
7838
  expired: () => approvalExpired(name)
@@ -7840,6 +7842,7 @@ function loginAsking(name) {
7840
7842
  function grantAsking(name, scope, rerun) {
7841
7843
  const subject = `the grant of ${scope} from ${name}`;
7842
7844
  return {
7845
+ login: name,
7843
7846
  subject,
7844
7847
  denied: () => new CliError({
7845
7848
  code: APPROVAL_DENIED,
@@ -7897,6 +7900,19 @@ function approvalExpired(name) {
7897
7900
  next: [loginStep(name)]
7898
7901
  });
7899
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
+ }
7900
7916
  //#endregion
7901
7917
  //#region ../agent-cli/dist/runtime-codes.js
7902
7918
  const DOCTOR_PROBLEMS = "DOCTOR_PROBLEMS";
@@ -7940,7 +7956,8 @@ const anyCommandErrors = {
7940
7956
  [APPROVAL_PENDING]: { class: "wait" },
7941
7957
  [APPROVAL_DENIED]: { class: "operator" },
7942
7958
  [APPROVAL_EXPIRED]: { class: "operator" },
7943
- [LOGIN_REJECTED]: { class: "operator" }
7959
+ [LOGIN_REJECTED]: { class: "operator" },
7960
+ [NOT_OAUTH]: { class: "source" }
7944
7961
  };
7945
7962
  /**
7946
7963
  * The Runtime codes only some Commands reach: each Command that throws one, Built-in or the CLI's
@@ -8087,9 +8104,12 @@ const errorSchema = object({ error: string() });
8087
8104
  function isoAfter(clock, seconds) {
8088
8105
  return new Date(clock.now() + seconds * MS_PER_S).toISOString();
8089
8106
  }
8090
- /** POSTs `form` to an OAuth endpoint and parses its JSON answer, whatever its status. */
8091
- async function postForm(fetch, url, form, { ignoreStop } = {}) {
8092
- 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, {
8093
8113
  method: "POST",
8094
8114
  once: false,
8095
8115
  ...ignoreStop === void 0 ? {} : { ignoreStop },
@@ -8097,15 +8117,21 @@ async function postForm(fetch, url, form, { ignoreStop } = {}) {
8097
8117
  body: new URLSearchParams(form)
8098
8118
  });
8099
8119
  const text = await response.text();
8120
+ const { status } = response;
8100
8121
  try {
8101
- return JSON.parse(text);
8122
+ return {
8123
+ endpoint,
8124
+ status,
8125
+ body: JSON.parse(text)
8126
+ };
8102
8127
  } catch (error) {
8103
- throw new Error(`${url} answered ${response.status} with a body that is not JSON`, { cause: error });
8128
+ throw notOAuth(endpoint, status, error);
8104
8129
  }
8105
8130
  }
8106
- 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 }) {
8107
8133
  const parsed = schema.safeParse(body);
8108
- if (!parsed.success) throw new Error(`${url} answered ${JSON.stringify(body)}, not an OAuth answer`);
8134
+ if (!parsed.success) throw notOAuth(endpoint, status);
8109
8135
  return parsed.data;
8110
8136
  }
8111
8137
  function storedLoginOf({ access_token, refresh_token, expires_in }, clock) {
@@ -8131,8 +8157,12 @@ async function refreshed(name, login, file, refreshToken, ctx) {
8131
8157
  client_id: login.clientId
8132
8158
  };
8133
8159
  return await ctx.runToEnd(`refreshing the Login ${name}`, async () => {
8134
- const body = await postForm(ctx.fetch, login.tokenEndpoint, form, { ignoreStop: true });
8135
- 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);
8136
8166
  if (granted.success) {
8137
8167
  const stored = {
8138
8168
  refreshToken,
@@ -8141,7 +8171,7 @@ async function refreshed(name, login, file, refreshToken, ctx) {
8141
8171
  await file.write(stored);
8142
8172
  return stored;
8143
8173
  }
8144
- const { error } = parsedAnswer(errorSchema, body, login.tokenEndpoint);
8174
+ const { error } = parsedAnswer(errorSchema, answer);
8145
8175
  if (error !== "invalid_grant") throw new Error(`${login.tokenEndpoint} refused the refresh of ${name} with ${error}`);
8146
8176
  await file.remove();
8147
8177
  });
@@ -8242,9 +8272,9 @@ function loginRows(logins, runAs) {
8242
8272
  /** The bundler's stamp, absent when the CLI runs unbundled. Read per call, so a test can define it. */
8243
8273
  function buildStamp() {
8244
8274
  return {
8245
- "sha": "8907e9d",
8275
+ "sha": "3716c19",
8246
8276
  "dirty": false,
8247
- "builtAt": "2026-11-10T09:08:31.000Z"
8277
+ "builtAt": "2026-11-10T16:12:55.000Z"
8248
8278
  };
8249
8279
  }
8250
8280
  /** The first line of `--version`: the version, and the commit and time of a stamped build. */
@@ -10102,12 +10132,18 @@ const deviceSchema = object({
10102
10132
  expires_in: number$1(),
10103
10133
  interval: number$1().optional()
10104
10134
  });
10105
- async function startApproval(login, { fetch, clock }) {
10135
+ async function startApproval(name, login, { fetch, clock }) {
10106
10136
  const form = {
10107
10137
  client_id: login.clientId,
10108
10138
  ...login.scope === void 0 ? {} : { scope: login.scope }
10109
10139
  };
10110
- 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);
10111
10147
  return {
10112
10148
  deviceCode: device.device_code,
10113
10149
  userCode: device.user_code,
@@ -10123,13 +10159,17 @@ async function pollApproval(asking, login, { approval, takenUp }, scope, signal)
10123
10159
  device_code: approval.deviceCode,
10124
10160
  client_id: login.clientId
10125
10161
  };
10162
+ const endpoint = {
10163
+ login: asking.login,
10164
+ url: login.tokenEndpoint
10165
+ };
10126
10166
  let intervalS = approval.interval;
10127
10167
  for (let first = true;; first = false) {
10128
10168
  if (!(first && takenUp)) await scope.clock.sleep(intervalS * MS_PER_S, signal);
10129
- const body = await postForm(scope.fetch, login.tokenEndpoint, form);
10130
- const granted = tokenSchema.safeParse(body);
10169
+ const answer = await postForm(scope.fetch, endpoint, form);
10170
+ const granted = tokenSchema.safeParse(answer.body);
10131
10171
  if (granted.success) return storedLoginOf(granted.data, scope.clock);
10132
- const { error } = parsedAnswer(errorSchema, body, login.tokenEndpoint);
10172
+ const { error } = parsedAnswer(errorSchema, answer);
10133
10173
  if (error === "slow_down") intervalS += SLOW_DOWN_S;
10134
10174
  else if (error !== "authorization_pending") throw refusal(asking, login, error);
10135
10175
  }
@@ -10144,9 +10184,9 @@ function endsApproval(error) {
10144
10184
  return error instanceof CliError && (error.code === "APPROVAL_DENIED" || error.code === "APPROVAL_EXPIRED");
10145
10185
  }
10146
10186
  /** Takes up the pending Approval, or asks for one and keeps it as a User file, and prints its code. */
10147
- async function askedApproval(login, file, scope) {
10187
+ async function askedApproval(name, login, file, scope) {
10148
10188
  const pending = await readUnexpired(file, scope.clock);
10149
- const approval = pending ?? await startApproval(login, scope);
10189
+ const approval = pending ?? await startApproval(name, login, scope);
10150
10190
  if (pending === void 0) await file.write(approval);
10151
10191
  scope.progress(`open ${approval.verificationUriComplete ?? approval.verificationUri} and approve code ${approval.userCode}`);
10152
10192
  return {
@@ -10156,7 +10196,7 @@ async function askedApproval(login, file, scope) {
10156
10196
  }
10157
10197
  /** The `APPROVAL_PENDING` of the pending Approval, or of a new one, without polling it. */
10158
10198
  async function pendingApproval(name, login, files, scope, rerun) {
10159
- const { approval } = await askedApproval(login, files.approval, scope);
10199
+ const { approval } = await askedApproval(name, login, files.approval, scope);
10160
10200
  return approvalPending(loginAsking(name), approval, rerun);
10161
10201
  }
10162
10202
  /**
@@ -10164,7 +10204,7 @@ async function pendingApproval(name, login, files, scope, rerun) {
10164
10204
  * refused or expired. An approved one stays until what it granted is kept, so a cut-off run loses nothing.
10165
10205
  */
10166
10206
  async function approved(asking, login, file, scope, { waitS, rerun }) {
10167
- const asked = await askedApproval(login, file, scope);
10207
+ const asked = await askedApproval(asking.login, login, file, scope);
10168
10208
  const wait = scope.deadline(waitS * MS_PER_S);
10169
10209
  let granted;
10170
10210
  try {
@@ -12221,48 +12261,60 @@ function approvalRerun(ctx) {
12221
12261
  add: [WAIT_FLAG.flag, String(100)]
12222
12262
  });
12223
12263
  }
12224
- async function tokensOf(names, logins, ctx, waitS) {
12225
- const found = await Promise.all(names.map(async (name) => {
12226
- const login = declaredLogin(logins, name, ctx.env);
12227
- return {
12228
- name,
12229
- token: ctx.dryRun ? await currentTokenOf(login, tokenScopeOf(ctx)) : await freshTokenOf(name, login, ctx)
12230
- };
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)
12231
12272
  }));
12232
- const missing = found.flatMap(({ name, token }) => token === void 0 ? [name] : []);
12233
- if (missing.length > 0 && ctx.dryRun) throw loginRequired(missing, ctx.rerun());
12234
- const tokens = /* @__PURE__ */ new Map();
12235
- for (const { name, token } of found) {
12236
- if (token !== void 0) {
12237
- tokens.set(name, token);
12238
- continue;
12239
- }
12240
- const login = declaredLogin(logins, name, ctx.env);
12241
- const stored = await signIn(name, login, loginFilesOf(ctx.userFile, login), ctx, {
12242
- waitS,
12243
- rerun: approvalRerun(ctx)
12244
- });
12245
- tokens.set(name, storedToken(stored));
12246
- }
12247
12273
  ctx.signal.throwIfAborted();
12248
- 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
+ };
12249
12301
  }
12250
12302
  /** The Login a `LOGIN_REJECTED` names in `details.login`, or the Command's only one. */
12251
- function rejectedName(rejected, tokens) {
12303
+ function rejectedName(rejected, names) {
12252
12304
  const { details } = rejected;
12253
12305
  if (typeof details === "object" && details !== null && "login" in details) return typeof details.login === "string" ? details.login : void 0;
12254
- const [only, ...rest] = tokens.keys();
12306
+ const [only, ...rest] = names;
12255
12307
  return rest.length === 0 ? only : void 0;
12256
12308
  }
12257
12309
  /**
12258
12310
  * What a run whose Command threw `error` fails with: a `LOGIN_REJECTED` of a stored Login drops
12259
12311
  * it for a new Approval, one of a Secret's token fails as the operator's to fix.
12260
12312
  */
12261
- async function afterRejection(error, tokens, logins, ctx) {
12313
+ async function afterRejection(error, { names, given }, logins, ctx) {
12262
12314
  if (!(error instanceof CliError && error.code === "LOGIN_REJECTED")) return error;
12263
- const name = rejectedName(error, tokens);
12264
- const token = name === void 0 ? void 0 : tokens.get(name);
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 });
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 });
12266
12318
  const login = declaredLogin(logins, name, ctx.env);
12267
12319
  if (token.source.kind === "secret" && login.secret !== void 0) return secretRejected(name, login.secret, error);
12268
12320
  const files = loginFilesOf(ctx.userFile, login);
@@ -12286,20 +12338,23 @@ function granting(ctx, grants, logins, waitS) {
12286
12338
  };
12287
12339
  }
12288
12340
  /**
12289
- * Runs `work` with a `ctx` whose `login` gives each named Login's token, signing in first, for at
12290
- * most `waitS` seconds, where one is missing, and whose `grant` asks for a Grant. A
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
12291
12343
  * `LOGIN_REJECTED` from `work` drops the stored Login.
12292
12344
  */
12293
12345
  async function withLogins(ctx, { logins: names, grants }, logins, waitS, work) {
12294
- const tokens = names.length === 0 ? /* @__PURE__ */ new Map() : await tokensOf(names, logins, ctx, waitS ?? 0);
12346
+ const { login, given } = loggingIn(ctx, names, logins, waitS ?? 0);
12295
12347
  const loggedIn = derived(ctx, {
12296
- login: (name) => tokens.get(name)?.value ?? undeclaredLogin(name),
12348
+ login,
12297
12349
  grant: granting(ctx, grants, logins, waitS ?? 0)
12298
12350
  });
12299
12351
  try {
12300
12352
  return await work(loggedIn);
12301
12353
  } catch (error) {
12302
- throw await afterRejection(error, tokens, logins, ctx);
12354
+ throw await afterRejection(error, {
12355
+ names,
12356
+ given
12357
+ }, logins, ctx);
12303
12358
  }
12304
12359
  }
12305
12360
  /** `ctx.grant` of a name the Command does not declare in `grants`, which only a cast reaches. */
@@ -12607,7 +12662,7 @@ function contextOf(scope, control, { command, flags, project, deliver, latestRel
12607
12662
  return {
12608
12663
  ...portsBase,
12609
12664
  secret,
12610
- login: undeclaredLogin,
12665
+ login: async (name) => undeclaredLogin(name),
12611
12666
  grant: async (name) => undeclaredGrant(name),
12612
12667
  userFile: scope.userFile,
12613
12668
  fail: cliErrorOf,
@@ -12967,7 +13022,7 @@ async function runProcess(cli) {
12967
13022
  //#region package.json
12968
13023
  var package_default = {
12969
13024
  name: "@duyquangnvx/create-agent-cli",
12970
- version: "0.32.0",
13025
+ version: "0.33.0",
12971
13026
  description: "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
12972
13027
  type: "module",
12973
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.32.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.32.0",
24
+ "@duyquangnvx/agent-cli": "0.33.0",
25
25
  "tsdown": "0.23.0",
26
26
  "zod": "4.6.7"
27
27
  },