letthemquibble 1.1.0 → 2.0.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/README.md +64 -14
  2. package/dist/quibble.js +1229 -307
  3. package/package.json +2 -1
package/dist/quibble.js CHANGED
@@ -51,11 +51,11 @@ var require_error = __commonJS({
51
51
  * @param {string} code an id string representing the error
52
52
  * @param {string} message human-readable description of the error
53
53
  */
54
- constructor(exitCode, code, message) {
54
+ constructor(exitCode, code2, message) {
55
55
  super(message);
56
56
  Error.captureStackTrace(this, this.constructor);
57
57
  this.name = this.constructor.name;
58
- this.code = code;
58
+ this.code = code2;
59
59
  this.exitCode = exitCode;
60
60
  this.nestedError = void 0;
61
61
  }
@@ -1649,9 +1649,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1649
1649
  * @return never
1650
1650
  * @private
1651
1651
  */
1652
- _exit(exitCode, code, message) {
1652
+ _exit(exitCode, code2, message) {
1653
1653
  if (this._exitCallback) {
1654
- this._exitCallback(new CommanderError2(exitCode, code, message));
1654
+ this._exitCallback(new CommanderError2(exitCode, code2, message));
1655
1655
  }
1656
1656
  process3.exit(exitCode);
1657
1657
  }
@@ -2289,14 +2289,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
2289
2289
  });
2290
2290
  }
2291
2291
  const exitCallback = this._exitCallback;
2292
- proc.on("close", (code) => {
2293
- code = code ?? 1;
2292
+ proc.on("close", (code2) => {
2293
+ code2 = code2 ?? 1;
2294
2294
  if (!exitCallback) {
2295
- process3.exit(code);
2295
+ process3.exit(code2);
2296
2296
  } else {
2297
2297
  exitCallback(
2298
2298
  new CommanderError2(
2299
- code,
2299
+ code2,
2300
2300
  "commander.executeSubCommandAsync",
2301
2301
  "(close)"
2302
2302
  )
@@ -2803,8 +2803,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
2803
2803
  }
2804
2804
  const config2 = errorOptions || {};
2805
2805
  const exitCode = config2.exitCode || 1;
2806
- const code = config2.code || "commander.error";
2807
- this._exit(exitCode, code, message);
2806
+ const code2 = config2.code || "commander.error";
2807
+ this._exit(exitCode, code2, message);
2808
2808
  }
2809
2809
  /**
2810
2810
  * Apply any option related environment variables, if option does
@@ -6303,9 +6303,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
6303
6303
  function stripTabAndNewline(value) {
6304
6304
  return value.replace(asciiTabOrNewline, "");
6305
6305
  }
6306
- function urlHostnameOk(url2, hostname4) {
6307
- hostname4.lastIndex = 0;
6308
- return hostname4.test(url2.hostname);
6306
+ function urlHostnameOk(url2, hostname5) {
6307
+ hostname5.lastIndex = 0;
6308
+ return hostname5.test(url2.hostname);
6309
6309
  }
6310
6310
  function urlProtocolOk(url2, protocol) {
6311
6311
  protocol.lastIndex = 0;
@@ -6578,12 +6578,12 @@ function isIso7064Mod97(iban3) {
6578
6578
  let remainder = 0;
6579
6579
  const len = iban3.length;
6580
6580
  for (let i = 4; i < len; i++) {
6581
- const code = iban3.charCodeAt(i);
6582
- remainder = (code >= 65 ? remainder * 100 + (code - 55) : remainder * 10 + (code - 48)) % 97;
6581
+ const code2 = iban3.charCodeAt(i);
6582
+ remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
6583
6583
  }
6584
6584
  for (let i = 0; i < 4; i++) {
6585
- const code = iban3.charCodeAt(i);
6586
- remainder = (code >= 65 ? remainder * 100 + (code - 55) : remainder * 10 + (code - 48)) % 97;
6585
+ const code2 = iban3.charCodeAt(i);
6586
+ remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
6587
6587
  }
6588
6588
  return remainder === 1;
6589
6589
  }
@@ -7013,7 +7013,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
7013
7013
  }
7014
7014
  return propValues;
7015
7015
  });
7016
- const isObject2 = isObject;
7016
+ const isObject3 = isObject;
7017
7017
  const catchall = def.catchall;
7018
7018
  let value;
7019
7019
  const memo2 = globalConfig.memoizer;
@@ -7021,7 +7021,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
7021
7021
  inst._zod.parse = (payload, ctx) => {
7022
7022
  value ?? (value = _normalized.value);
7023
7023
  const input2 = payload.value;
7024
- if (!isObject2(input2)) {
7024
+ if (!isObject3(input2)) {
7025
7025
  payload.issues.push({
7026
7026
  expected: "object",
7027
7027
  code: "invalid_type",
@@ -7157,7 +7157,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
7157
7157
  return doc.compile();
7158
7158
  };
7159
7159
  let fastpass;
7160
- const isObject2 = isObject;
7160
+ const isObject3 = isObject;
7161
7161
  const jit = !globalConfig.jitless;
7162
7162
  const allowsEval2 = allowsEval;
7163
7163
  const fastEnabled = jit && allowsEval2.value;
@@ -7166,7 +7166,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
7166
7166
  inst._zod.parse = (payload, ctx) => {
7167
7167
  value ?? (value = _normalized.value);
7168
7168
  const input2 = payload.value;
7169
- if (!isObject2(input2)) {
7169
+ if (!isObject3(input2)) {
7170
7170
  payload.issues.push({
7171
7171
  expected: "object",
7172
7172
  code: "invalid_type",
@@ -16501,12 +16501,12 @@ function compileFn(schema, options) {
16501
16501
  doc.write(outputAccessor === null ? `return true;` : `return ${outputAccessor};`);
16502
16502
  const constantNames = ["INVALID", ...ctx.constants.keys()];
16503
16503
  const constantValues = [INVALID, ...ctx.constants.values()];
16504
- const code = doc.content.join("\n");
16504
+ const code2 = doc.content.join("\n");
16505
16505
  const fullCode = options?.debug ? constantNames.length > 0 ? `// Constants: ${constantNames.join(", ")}
16506
- ${code}` : code : "";
16506
+ ${code2}` : code2 : "";
16507
16507
  const F = Function;
16508
16508
  const factoryCode = `return (input) => {
16509
- ${code}
16509
+ ${code2}
16510
16510
  }`;
16511
16511
  let fn;
16512
16512
  try {
@@ -19432,8 +19432,8 @@ function finalize(ctx, schema) {
19432
19432
  compactTypeUnion(entry[1].def ?? entry[1].schema);
19433
19433
  }
19434
19434
  }
19435
- for (const rewrite of ctx.deferred)
19436
- rewrite();
19435
+ for (const rewrite2 of ctx.deferred)
19436
+ rewrite2();
19437
19437
  if (ctx.intersections.length) {
19438
19438
  const carriers = /* @__PURE__ */ new Map();
19439
19439
  for (const seen of ctx.seen.values()) {
@@ -23222,7 +23222,7 @@ var RESERVED_SLUGS = [
23222
23222
  ];
23223
23223
  var LOCAL_ORG = "local";
23224
23224
  var DISPLAY_NAME_MAX = 64;
23225
- var PROJECT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
23225
+ var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
23226
23226
  var DEFAULT_LIMIT = 50;
23227
23227
  var MAX_LIMIT = 200;
23228
23228
  var DEFAULT_SEVERITY = "medium";
@@ -23233,14 +23233,22 @@ var ACT_AS_HEADER = "x-quibble-act-as";
23233
23233
  var TRUNCATED_HEADER = "x-quibble-truncated";
23234
23234
  var IDEMPOTENCY_HEADER = "idempotency-key";
23235
23235
  var IDEMPOTENCY_KEY_MAX = 128;
23236
+ var CLI_LOGIN_TTL_SECONDS = 600;
23237
+ var CLI_LOGIN_POLL_SECONDS = 3;
23238
+ var CLI_LOGIN_CODE_ALPHABET = "BCDFGHJKMNPQRSTVWXZ";
23239
+ var CLI_LOGIN_CODE_LENGTH = 8;
23240
+ var CLI_LOGIN_HOST_MAX = 64;
23241
+ var CLI_LOGIN_OS_MAX = 32;
23242
+ var CLI_LOGIN_VERSION_MAX = 32;
23243
+ var CLI_LOGIN_PAGE_PATH = "/cli-login";
23236
23244
 
23237
23245
  // ../core/dist/errors.js
23238
23246
  var QuibbleError = class extends Error {
23239
23247
  /** Stable machine-readable code, also used by the API error body. */
23240
23248
  code;
23241
- constructor(code, message, options) {
23249
+ constructor(code2, message, options) {
23242
23250
  super(message, options);
23243
- this.code = code;
23251
+ this.code = code2;
23244
23252
  this.name = new.target.name;
23245
23253
  }
23246
23254
  };
@@ -23287,6 +23295,35 @@ function describeError(error62) {
23287
23295
  return error62 instanceof Error ? error62.message : String(error62);
23288
23296
  }
23289
23297
 
23298
+ // ../core/dist/cli-login.js
23299
+ var CLI_LOGIN_CONTROL_RE = /[\u0000-\u001F\u007F-\u009F\p{Cf}]/u;
23300
+ var CONTROL_GLOBAL_RE = /[\u0000-\u001F\u007F-\u009F\p{Cf}]/gu;
23301
+ var CLI_LOGIN_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
23302
+ var CLI_LOGIN_CODE_MESSAGE = "code: not a login code";
23303
+ var HALF = CLI_LOGIN_CODE_LENGTH / 2;
23304
+ var CODE_RE = new RegExp(`^([${CLI_LOGIN_CODE_ALPHABET}]{${HALF}})-?([${CLI_LOGIN_CODE_ALPHABET}]{${HALF}})$`);
23305
+ var UNKNOWN_HOST = "unknown";
23306
+ function parseCliLoginCode(value) {
23307
+ const match = CODE_RE.exec(value.trim().toUpperCase());
23308
+ if (match === null)
23309
+ throw new ValidationError([CLI_LOGIN_CODE_MESSAGE]);
23310
+ return `${match[1] ?? ""}-${match[2] ?? ""}`;
23311
+ }
23312
+ function cliLoginUrl(webUrl, code2) {
23313
+ return `${webUrl.replace(/\/+$/, "")}${CLI_LOGIN_PAGE_PATH}?code=${code2}`;
23314
+ }
23315
+ function cut(value, max) {
23316
+ if (value.length <= max)
23317
+ return value;
23318
+ const head = value.slice(0, max);
23319
+ const last = head.charCodeAt(head.length - 1);
23320
+ return last >= 55296 && last <= 56319 ? head.slice(0, -1) : head;
23321
+ }
23322
+ function cliLoginHost(raw) {
23323
+ const host = cut(raw.replace(CONTROL_GLOBAL_RE, "").trim(), CLI_LOGIN_HOST_MAX).trim();
23324
+ return host === "" ? UNKNOWN_HOST : host;
23325
+ }
23326
+
23290
23327
  // ../core/dist/fingerprint.js
23291
23328
  function headlineOf(message) {
23292
23329
  for (const line of message.split("\n")) {
@@ -23307,7 +23344,7 @@ var PUNCTUATION_RE = /[^\p{L}\p{N}\s<>]/gu;
23307
23344
  function isHexId(run2) {
23308
23345
  return /[a-f]/.test(run2) && /[0-9]/.test(run2);
23309
23346
  }
23310
- function cut(text, max) {
23347
+ function cut2(text, max) {
23311
23348
  if (text.length <= max)
23312
23349
  return text;
23313
23350
  const head = text.slice(0, max);
@@ -23325,7 +23362,7 @@ function fingerprintOf(message) {
23325
23362
  text = text.replace(NUMBER_RE, "<n>");
23326
23363
  text = text.replace(PUNCTUATION_RE, " ");
23327
23364
  text = text.replace(/\s+/gu, " ").trim();
23328
- text = cut(text, FINGERPRINT_MAX).trim();
23365
+ text = cut2(text, FINGERPRINT_MAX).trim();
23329
23366
  return text === "" ? EMPTY_FINGERPRINT : text;
23330
23367
  }
23331
23368
  function groupComplaints(complaints) {
@@ -23476,7 +23513,10 @@ var timestampSchema = external_exports.string({ message: TIMESTAMP_MESSAGE }).re
23476
23513
  function toMilliseconds(timestamp) {
23477
23514
  return new Date(timestamp).toISOString();
23478
23515
  }
23479
- var projectNameSchema = external_exports.string({ message: PROJECT_MESSAGE }).regex(PROJECT_NAME_RE, { message: PROJECT_MESSAGE });
23516
+ function foldProjectName(name) {
23517
+ return name.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
23518
+ }
23519
+ var projectNameSchema = external_exports.string({ message: PROJECT_MESSAGE }).overwrite(foldProjectName).regex(PROJECT_NAME_RE, { message: PROJECT_MESSAGE });
23480
23520
  var orgSlugSchema = external_exports.string({ message: SLUG_MESSAGE }).refine((value) => ORG_SLUG_RE.test(value) && !value.includes("--"), {
23481
23521
  message: SLUG_MESSAGE
23482
23522
  });
@@ -23903,6 +23943,23 @@ function newTokenSchema(now) {
23903
23943
  ...input2.expiresAt === void 0 ? {} : { expiresAt: input2.expiresAt }
23904
23944
  }));
23905
23945
  }
23946
+ function reportedField(max) {
23947
+ const message = lengthMessage(1, max);
23948
+ return external_exports.string({ message }).trim().min(1, { message }).max(max, { message }).refine((value) => !CLI_LOGIN_CONTROL_RE.test(value), { message: CONTROL_MESSAGE });
23949
+ }
23950
+ var newCliLoginSchema = external_exports.strictObject({
23951
+ host: reportedField(CLI_LOGIN_HOST_MAX),
23952
+ os: reportedField(CLI_LOGIN_OS_MAX),
23953
+ version: reportedField(CLI_LOGIN_VERSION_MAX)
23954
+ }).transform((input2) => ({
23955
+ host: input2.host,
23956
+ os: input2.os,
23957
+ version: input2.version
23958
+ }));
23959
+ var LOGIN_KEY_MESSAGE = "must be 43 base64url characters";
23960
+ var cliLoginPollSchema = external_exports.strictObject({
23961
+ loginKey: external_exports.string({ message: LOGIN_KEY_MESSAGE }).regex(CLI_LOGIN_KEY_RE, { message: LOGIN_KEY_MESSAGE })
23962
+ }).transform((input2) => ({ loginKey: input2.loginKey }));
23906
23963
  var UNKNOWN_KEY_MESSAGE = "unknown key";
23907
23964
  var emptyBodySchema = external_exports.strictObject({});
23908
23965
  function formatIssues(error62) {
@@ -24046,6 +24103,34 @@ function listQueryOf(filter) {
24046
24103
  return pairs;
24047
24104
  }
24048
24105
 
24106
+ // ../core/dist/endpoint.js
24107
+ var DEFAULT_ENDPOINT = "https://api.letthemquibble.com";
24108
+ var DISCOVERY_PATH = "/.well-known/quibble.json";
24109
+ var LOCAL_ENDPOINT = "local";
24110
+ var LOOPBACK_HOSTS = ["localhost", "127.0.0.1"];
24111
+ function trimSlash(url2) {
24112
+ return url2.replace(/\/+$/, "");
24113
+ }
24114
+ function normalizeEndpoint(url2) {
24115
+ const text = trimSlash(url2.trim());
24116
+ try {
24117
+ const parsed = new URL(text);
24118
+ return `${parsed.protocol}//${parsed.host}${trimSlash(parsed.pathname)}`;
24119
+ } catch {
24120
+ return text;
24121
+ }
24122
+ }
24123
+ function isDefaultEndpoint(url2) {
24124
+ return normalizeEndpoint(url2) === DEFAULT_ENDPOINT;
24125
+ }
24126
+ function isLoopbackUrl(url2) {
24127
+ try {
24128
+ return LOOPBACK_HOSTS.includes(new URL(url2).hostname);
24129
+ } catch {
24130
+ return false;
24131
+ }
24132
+ }
24133
+
24049
24134
  // ../core/dist/service.js
24050
24135
  import { createHash, randomUUID } from "node:crypto";
24051
24136
  function applyTagRules(tags, allowedTags) {
@@ -24459,7 +24544,7 @@ function defaultConfigPath(env = process.env) {
24459
24544
  }
24460
24545
 
24461
24546
  // src/config.ts
24462
- import { readFileSync as readFileSync2 } from "node:fs";
24547
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
24463
24548
  import { dirname as dirname3, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
24464
24549
 
24465
24550
  // src/credentials.ts
@@ -24473,6 +24558,92 @@ import {
24473
24558
  writeFileSync
24474
24559
  } from "node:fs";
24475
24560
  import { dirname as dirname2, join as join2 } from "node:path";
24561
+ var CREDENTIALS_FILENAME = "credentials.json";
24562
+ var CREDENTIALS_MODE = 384;
24563
+ var HOME_MODE = 448;
24564
+ function defaultCredentialsPath(env) {
24565
+ return join2(defaultQuibbleHome(env), CREDENTIALS_FILENAME);
24566
+ }
24567
+ function readEntry(value) {
24568
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
24569
+ return void 0;
24570
+ }
24571
+ const raw = value;
24572
+ const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
24573
+ for (const key of keys) {
24574
+ if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
24575
+ }
24576
+ return {
24577
+ endpoint: raw["endpoint"],
24578
+ refreshToken: raw["refreshToken"],
24579
+ accessToken: raw["accessToken"],
24580
+ expiresAt: raw["expiresAt"],
24581
+ sub: raw["sub"],
24582
+ email: raw["email"]
24583
+ };
24584
+ }
24585
+ function readCredentials(env) {
24586
+ const path = defaultCredentialsPath(env);
24587
+ let text;
24588
+ try {
24589
+ text = readFileSync(path, "utf8");
24590
+ } catch (error62) {
24591
+ if (isNotFound(error62)) return {};
24592
+ throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
24593
+ }
24594
+ let parsed;
24595
+ try {
24596
+ parsed = JSON.parse(text);
24597
+ } catch (error62) {
24598
+ throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
24599
+ }
24600
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24601
+ throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
24602
+ }
24603
+ const entries = {};
24604
+ for (const [key, value] of Object.entries(parsed)) {
24605
+ const entry = readEntry(value);
24606
+ if (entry !== void 0) entries[key] = entry;
24607
+ }
24608
+ return entries;
24609
+ }
24610
+ function findCredential(env, endpoint) {
24611
+ return readCredentials(env)[normalizeEndpoint(endpoint)];
24612
+ }
24613
+ function writeCredentials(env, entries) {
24614
+ const path = defaultCredentialsPath(env);
24615
+ mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
24616
+ const temp = join2(
24617
+ dirname2(path),
24618
+ `.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
24619
+ );
24620
+ try {
24621
+ writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
24622
+ `, {
24623
+ encoding: "utf8",
24624
+ mode: CREDENTIALS_MODE
24625
+ });
24626
+ chmodSync(temp, CREDENTIALS_MODE);
24627
+ renameSync(temp, path);
24628
+ } catch (error62) {
24629
+ rmSync(temp, { force: true });
24630
+ throw error62;
24631
+ }
24632
+ }
24633
+ function saveCredential(env, credential) {
24634
+ const key = normalizeEndpoint(credential.endpoint);
24635
+ const stored = { ...credential, endpoint: key };
24636
+ writeCredentials(env, { ...readCredentials(env), [key]: stored });
24637
+ return stored;
24638
+ }
24639
+ function removeCredential(env, endpoint) {
24640
+ const entries = readCredentials(env);
24641
+ const key = normalizeEndpoint(endpoint);
24642
+ if (entries[key] === void 0) return false;
24643
+ delete entries[key];
24644
+ writeCredentials(env, entries);
24645
+ return true;
24646
+ }
24476
24647
 
24477
24648
  // src/errors.ts
24478
24649
  var AMBIGUOUS_ID = "ambiguous_id";
@@ -24525,7 +24696,7 @@ var DEBUG_ENV = "QUIBBLE_DEBUG";
24525
24696
  function debugEnabled(env) {
24526
24697
  return env[DEBUG_ENV] === "1";
24527
24698
  }
24528
- function trimSlash(url2) {
24699
+ function trimSlash2(url2) {
24529
24700
  return url2.replace(/\/+$/, "");
24530
24701
  }
24531
24702
  function originOf(url2) {
@@ -24629,103 +24800,6 @@ function remoteFailure(site, response, text) {
24629
24800
  });
24630
24801
  }
24631
24802
 
24632
- // src/credentials.ts
24633
- var CREDENTIALS_FILENAME = "credentials.json";
24634
- var CREDENTIALS_MODE = 384;
24635
- var HOME_MODE = 448;
24636
- function defaultCredentialsPath(env) {
24637
- return join2(defaultQuibbleHome(env), CREDENTIALS_FILENAME);
24638
- }
24639
- function normalizeEndpoint(endpoint) {
24640
- const text = trimSlash(endpoint.trim());
24641
- try {
24642
- const url2 = new URL(text);
24643
- return `${url2.protocol}//${url2.host}${trimSlash(url2.pathname)}`;
24644
- } catch {
24645
- return text;
24646
- }
24647
- }
24648
- function readEntry(value) {
24649
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
24650
- return void 0;
24651
- }
24652
- const raw = value;
24653
- const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
24654
- for (const key of keys) {
24655
- if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
24656
- }
24657
- return {
24658
- endpoint: raw["endpoint"],
24659
- refreshToken: raw["refreshToken"],
24660
- accessToken: raw["accessToken"],
24661
- expiresAt: raw["expiresAt"],
24662
- sub: raw["sub"],
24663
- email: raw["email"]
24664
- };
24665
- }
24666
- function readCredentials(env) {
24667
- const path = defaultCredentialsPath(env);
24668
- let text;
24669
- try {
24670
- text = readFileSync(path, "utf8");
24671
- } catch (error62) {
24672
- if (isNotFound(error62)) return {};
24673
- throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
24674
- }
24675
- let parsed;
24676
- try {
24677
- parsed = JSON.parse(text);
24678
- } catch (error62) {
24679
- throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
24680
- }
24681
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24682
- throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
24683
- }
24684
- const entries = {};
24685
- for (const [key, value] of Object.entries(parsed)) {
24686
- const entry = readEntry(value);
24687
- if (entry !== void 0) entries[key] = entry;
24688
- }
24689
- return entries;
24690
- }
24691
- function findCredential(env, endpoint) {
24692
- return readCredentials(env)[normalizeEndpoint(endpoint)];
24693
- }
24694
- function writeCredentials(env, entries) {
24695
- const path = defaultCredentialsPath(env);
24696
- mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
24697
- const temp = join2(
24698
- dirname2(path),
24699
- `.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
24700
- );
24701
- try {
24702
- writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
24703
- `, {
24704
- encoding: "utf8",
24705
- mode: CREDENTIALS_MODE
24706
- });
24707
- chmodSync(temp, CREDENTIALS_MODE);
24708
- renameSync(temp, path);
24709
- } catch (error62) {
24710
- rmSync(temp, { force: true });
24711
- throw error62;
24712
- }
24713
- }
24714
- function saveCredential(env, credential) {
24715
- const key = normalizeEndpoint(credential.endpoint);
24716
- const stored = { ...credential, endpoint: key };
24717
- writeCredentials(env, { ...readCredentials(env), [key]: stored });
24718
- return stored;
24719
- }
24720
- function removeCredential(env, endpoint) {
24721
- const entries = readCredentials(env);
24722
- const key = normalizeEndpoint(endpoint);
24723
- if (entries[key] === void 0) return false;
24724
- delete entries[key];
24725
- writeCredentials(env, entries);
24726
- return true;
24727
- }
24728
-
24729
24803
  // src/cognito.ts
24730
24804
  var TARGET_PREFIX = "AWSCognitoIdentityProviderService.";
24731
24805
  var CONTENT_TYPE = "application/x-amz-json-1.1";
@@ -24736,6 +24810,7 @@ var TOO_MANY_LINE = "Too many attempts. Try again later.";
24736
24810
  var WRONG_CODE_LINE = "Wrong code.";
24737
24811
  var EXPIRED_CODE_LINE = "That code has expired. Request a new one.";
24738
24812
  var PASSWORD_POLICY_LINE = "Use at least 12 characters, with an uppercase letter, a lowercase letter, a number and a symbol.";
24813
+ var BROWSER_REFUSED_LINE = "the browser approval could not be used: run quibble login again";
24739
24814
  var UNREACHABLE_TYPE = "Unreachable";
24740
24815
  var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
24741
24816
  var CognitoError = class extends RemoteError {
@@ -24752,14 +24827,15 @@ var CognitoError = class extends RemoteError {
24752
24827
  function endpointUrl(region) {
24753
24828
  return `https://cognito-idp.${region}.amazonaws.com/`;
24754
24829
  }
24755
- function errorLineOf(type, detail2, signIn2) {
24830
+ function errorLineOf(type, detail2, step) {
24756
24831
  switch (type) {
24757
24832
  case UNREACHABLE_TYPE:
24758
24833
  return UNREACHABLE_LINE;
24759
24834
  case "UserNotFoundException":
24760
- return signIn2 ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24835
+ return step === "sign-in" ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24761
24836
  case "NotAuthorizedException":
24762
- return signIn2 && detail2 === INCORRECT_CREDENTIALS_MESSAGE ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24837
+ if (step === "browser") return BROWSER_REFUSED_LINE;
24838
+ return step === "sign-in" && detail2 === INCORRECT_CREDENTIALS_MESSAGE ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24763
24839
  case "LimitExceededException":
24764
24840
  case "TooManyRequestsException":
24765
24841
  case "TooManyFailedAttemptsException":
@@ -24780,7 +24856,7 @@ function typeOf(value) {
24780
24856
  const hash2 = value.indexOf("#");
24781
24857
  return hash2 === -1 ? value : value.slice(hash2 + 1);
24782
24858
  }
24783
- async function call(endpoint, operation, body, signIn2 = false) {
24859
+ async function call(endpoint, operation, body, step = "other") {
24784
24860
  const url2 = endpointUrl(endpoint.region);
24785
24861
  const send2 = endpoint.fetch ?? globalThis.fetch;
24786
24862
  const site = {
@@ -24792,7 +24868,7 @@ async function call(endpoint, operation, body, signIn2 = false) {
24792
24868
  throw new CognitoError(
24793
24869
  type,
24794
24870
  detail2,
24795
- errorLineOf(type, detail2, signIn2),
24871
+ errorLineOf(type, detail2, step),
24796
24872
  detail(site, status)
24797
24873
  );
24798
24874
  };
@@ -24875,14 +24951,27 @@ function authAnswerOf(body) {
24875
24951
  }
24876
24952
  };
24877
24953
  }
24954
+ function flowOf(input2) {
24955
+ switch (input2.flow) {
24956
+ case "USER_PASSWORD_AUTH":
24957
+ return {
24958
+ parameters: { USERNAME: input2.username, PASSWORD: input2.password },
24959
+ step: "sign-in"
24960
+ };
24961
+ case "REFRESH_TOKEN_AUTH":
24962
+ return { parameters: { REFRESH_TOKEN: input2.refreshToken }, step: "other" };
24963
+ case "CUSTOM_AUTH":
24964
+ return { parameters: { USERNAME: input2.username }, step: "browser" };
24965
+ }
24966
+ }
24878
24967
  async function initiateAuth(endpoint, input2) {
24879
- const parameters = input2.flow === "USER_PASSWORD_AUTH" ? { USERNAME: input2.username, PASSWORD: input2.password } : { REFRESH_TOKEN: input2.refreshToken };
24968
+ const { parameters, step } = flowOf(input2);
24880
24969
  return authAnswerOf(
24881
24970
  await call(
24882
24971
  endpoint,
24883
24972
  "InitiateAuth",
24884
24973
  { ClientId: input2.clientId, AuthFlow: input2.flow, AuthParameters: parameters },
24885
- input2.flow === "USER_PASSWORD_AUTH"
24974
+ step
24886
24975
  )
24887
24976
  );
24888
24977
  }
@@ -24897,6 +24986,22 @@ async function respondToAuthChallenge(endpoint, input2) {
24897
24986
  })
24898
24987
  );
24899
24988
  }
24989
+ async function respondToCustomChallenge(endpoint, input2) {
24990
+ const username = input2.challenge.challengeParameters["USERNAME"] ?? input2.username;
24991
+ return authAnswerOf(
24992
+ await call(
24993
+ endpoint,
24994
+ "RespondToAuthChallenge",
24995
+ {
24996
+ ClientId: input2.clientId,
24997
+ ChallengeName: input2.challenge.challengeName,
24998
+ Session: input2.challenge.session,
24999
+ ChallengeResponses: { USERNAME: username, ANSWER: input2.answer }
25000
+ },
25001
+ "browser"
25002
+ )
25003
+ );
25004
+ }
24900
25005
  async function revokeToken(endpoint, input2) {
24901
25006
  await call(endpoint, "RevokeToken", { ClientId: input2.clientId, Token: input2.token });
24902
25007
  }
@@ -24925,9 +25030,8 @@ function actAsRefused(sub, endpoint) {
24925
25030
  function clockOf(options) {
24926
25031
  return options.now ?? (() => /* @__PURE__ */ new Date());
24927
25032
  }
24928
- var LOCAL_HOSTS = ["localhost", "127.0.0.1"];
24929
25033
  async function fetchAuthConfig(endpoint, options = {}) {
24930
- const url2 = `${trimSlash(endpoint)}${AUTH_CONFIG_PATH}`;
25034
+ const url2 = `${trimSlash2(endpoint)}${AUTH_CONFIG_PATH}`;
24931
25035
  const site = { method: "GET", url: url2, debug: options.debug };
24932
25036
  const response = await send(url2, { method: "GET", ...options });
24933
25037
  const text = await readText(response, site);
@@ -24956,13 +25060,6 @@ function bearerAuthorizer(token) {
24956
25060
  };
24957
25061
  }
24958
25062
  var DEV_TOKEN = "dev";
24959
- function isLoopback(endpoint) {
24960
- try {
24961
- return LOCAL_HOSTS.includes(new URL(endpoint).hostname);
24962
- } catch {
24963
- return false;
24964
- }
24965
- }
24966
25063
  var DevAuthorizer = class {
24967
25064
  actAs;
24968
25065
  #endpoint;
@@ -25067,12 +25164,97 @@ function createAuthorizer(endpoint, options) {
25067
25164
  return new SessionAuthorizer(endpoint, options);
25068
25165
  }
25069
25166
 
25167
+ // src/endpoint.ts
25168
+ var DISCOVERY_TIMEOUT_MS = 5e3;
25169
+ var SCHEMES = ["http:", "https:"];
25170
+ function parseEndpoint(value, from) {
25171
+ const text = value.trim();
25172
+ if (text.toLowerCase() === LOCAL_ENDPOINT) return { kind: "local" };
25173
+ const invalid = new ConfigError(
25174
+ `invalid endpoint "${text}" from ${from}: pass an https URL, a host, or local`
25175
+ );
25176
+ let url2 = text;
25177
+ if (!text.includes("://")) {
25178
+ url2 = isLoopbackUrl(`http://${text}`) ? `http://${text}` : `https://${text}`;
25179
+ }
25180
+ let parsed;
25181
+ try {
25182
+ parsed = new URL(url2);
25183
+ } catch {
25184
+ throw invalid;
25185
+ }
25186
+ if (!SCHEMES.includes(parsed.protocol) || parsed.hostname === "") throw invalid;
25187
+ return { kind: "url", url: normalizeEndpoint(url2) };
25188
+ }
25189
+ var discoveries = /* @__PURE__ */ new Map();
25190
+ function resetDiscovery() {
25191
+ discoveries.clear();
25192
+ }
25193
+ function discoverApiUrl(url2, options = {}) {
25194
+ const key = normalizeEndpoint(url2);
25195
+ if (isDefaultEndpoint(key)) return Promise.resolve(DEFAULT_ENDPOINT);
25196
+ const known = discoveries.get(key);
25197
+ if (known !== void 0) return known;
25198
+ const found = lookup(key, options).then((api) => {
25199
+ if (!discoveries.has(api)) discoveries.set(api, Promise.resolve(api));
25200
+ return api;
25201
+ });
25202
+ discoveries.set(key, found);
25203
+ return found;
25204
+ }
25205
+ async function lookup(url2, options) {
25206
+ const target = `${url2}${DISCOVERY_PATH}`;
25207
+ try {
25208
+ const response = await send(target, {
25209
+ method: "GET",
25210
+ headers: { accept: "application/json" },
25211
+ fetch: options.fetch,
25212
+ debug: options.debug,
25213
+ timeoutMs: options.timeoutMs ?? DISCOVERY_TIMEOUT_MS
25214
+ });
25215
+ const text = await readText(response, { method: "GET", url: target });
25216
+ if (!response.ok) return url2;
25217
+ const api = stringOf(asRecord(parseQuietly(text)), "api");
25218
+ if (api === void 0) return url2;
25219
+ const parsed = new URL(api.trim());
25220
+ if (!SCHEMES.includes(parsed.protocol)) return url2;
25221
+ return normalizeEndpoint(api);
25222
+ } catch {
25223
+ return url2;
25224
+ }
25225
+ }
25226
+
25070
25227
  // src/config.ts
25071
- var NO_ENDPOINT_MESSAGE = "no endpoint: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
25072
25228
  function requireEndpoint(context) {
25073
- if (context.endpoint === void 0) throw new ConfigError(NO_ENDPOINT_MESSAGE);
25229
+ if (context.endpoint === void 0) {
25230
+ throw new ConfigError(`local mode has no endpoint: ${context.localFix ?? ""}`);
25231
+ }
25074
25232
  return context.endpoint;
25075
25233
  }
25234
+ async function resolveEndpoint(context, io) {
25235
+ const endpoint = requireEndpoint(context);
25236
+ if (context.discovered) return endpoint;
25237
+ const api = await discoverApiUrl(endpoint, {
25238
+ fetch: io.fetch,
25239
+ debug: context.debug,
25240
+ timeoutMs: context.timeoutMs
25241
+ });
25242
+ context.endpoint = api;
25243
+ context.credential = findCredential(io.env, api);
25244
+ context.discovered = true;
25245
+ return api;
25246
+ }
25247
+ function foreignOrgMessage(context) {
25248
+ const slug = context.slug;
25249
+ if (context.mode !== "local" || slug === void 0 || slug === LOCAL_ORG) {
25250
+ return void 0;
25251
+ }
25252
+ return `local mode holds the organization ${LOCAL_ORG} only, not ${slug}: ${context.localFix ?? ""} to file to ${slug} on a server, or pass --org ${LOCAL_ORG}`;
25253
+ }
25254
+ function refuseForeignOrg(context) {
25255
+ const message = foreignOrgMessage(context);
25256
+ if (message !== void 0) throw new ConfigError(message);
25257
+ }
25076
25258
  var NO_ORG_MESSAGE = "no organization: pass --org <slug> or run `quibble init --org <slug>`";
25077
25259
  function requireOrg(context) {
25078
25260
  if (context.org === void 0) throw new ConfigError(NO_ORG_MESSAGE);
@@ -25196,20 +25378,76 @@ function firstValue(...values) {
25196
25378
  }
25197
25379
  return void 0;
25198
25380
  }
25199
- function resolveContext(options) {
25200
- const project = findProjectConfig(options.cwd);
25201
- const user = loadUserConfig(options.env);
25202
- const endpoint = firstValue(
25203
- options.endpoint,
25204
- options.env["QUIBBLE_ENDPOINT"],
25205
- project?.endpoint,
25206
- user.endpoint
25381
+ function localFixOf(candidate) {
25382
+ switch (candidate.source) {
25383
+ case "--endpoint":
25384
+ return "drop --endpoint local";
25385
+ case "QUIBBLE_ENDPOINT":
25386
+ return "unset QUIBBLE_ENDPOINT";
25387
+ default:
25388
+ return `remove "endpoint": "local" from ${candidate.path ?? ""}`;
25389
+ }
25390
+ }
25391
+ function resolveEndpointSource(options, project, user) {
25392
+ if (options.local === true) {
25393
+ return {
25394
+ mode: "local",
25395
+ endpoint: void 0,
25396
+ endpointSource: "--local",
25397
+ localFix: "drop --local"
25398
+ };
25399
+ }
25400
+ const candidates2 = [
25401
+ { source: "--endpoint", value: options.endpoint },
25402
+ { source: "QUIBBLE_ENDPOINT", value: options.env["QUIBBLE_ENDPOINT"] },
25403
+ { source: ".quibblerc", value: project?.endpoint, path: project?.path },
25404
+ {
25405
+ source: "config.json",
25406
+ value: user.endpoint,
25407
+ path: defaultConfigPath(options.env)
25408
+ }
25409
+ ];
25410
+ const chosen = candidates2.find(
25411
+ (candidate) => candidate.value !== void 0 && candidate.value.trim() !== ""
25207
25412
  );
25208
- const name = firstValue(options.project, project?.project);
25209
- const given = firstValue(options.org, project?.org);
25210
- const slug = given === void 0 ? void 0 : parseOrgSlug(given);
25413
+ if (chosen === void 0) {
25414
+ return {
25415
+ mode: "remote",
25416
+ endpoint: DEFAULT_ENDPOINT,
25417
+ endpointSource: "default",
25418
+ localFix: void 0
25419
+ };
25420
+ }
25421
+ const value = parseEndpoint(chosen.value ?? "", chosen.path ?? chosen.source);
25422
+ if (value.kind === "local") {
25423
+ return {
25424
+ mode: "local",
25425
+ endpoint: void 0,
25426
+ endpointSource: chosen.source,
25427
+ localFix: localFixOf(chosen)
25428
+ };
25429
+ }
25430
+ return {
25431
+ mode: "remote",
25432
+ endpoint: value.url,
25433
+ endpointSource: chosen.source,
25434
+ localFix: void 0
25435
+ };
25436
+ }
25437
+ function resolveContext(options) {
25438
+ const project = findProjectConfig(options.cwd);
25439
+ const user = loadUserConfig(options.env);
25440
+ const { mode, endpoint, endpointSource, localFix } = resolveEndpointSource(
25441
+ options,
25442
+ project,
25443
+ user
25444
+ );
25445
+ const given = firstValue(options.project, project?.project);
25446
+ const name = given === void 0 ? void 0 : foldProjectName(given);
25447
+ const org = firstValue(options.org, project?.org);
25448
+ const slug = org === void 0 ? void 0 : parseOrgSlug(org);
25211
25449
  const token = firstValue(options.env[TOKEN_ENV]);
25212
- const mode = options.local === true || endpoint === void 0 ? "local" : "remote";
25450
+ const discovered = endpoint !== void 0 && isDefaultEndpoint(endpoint);
25213
25451
  return {
25214
25452
  project: name,
25215
25453
  org: mode === "local" ? LOCAL_ORG : slug,
@@ -25219,10 +25457,12 @@ function resolveContext(options) {
25219
25457
  cwd: project === void 0 ? void 0 : relativeCwd(project.root, options.cwd),
25220
25458
  mode,
25221
25459
  endpoint,
25460
+ endpointSource,
25461
+ localFix,
25462
+ discovered,
25463
+ timeoutMs: void 0,
25222
25464
  token,
25223
- // `quibble config` diagnoses a missing session, so the entry is read here
25224
- // and no command has to read the file to describe the session.
25225
- credential: endpoint === void 0 ? void 0 : findCredential(options.env, endpoint),
25465
+ credential: discovered && endpoint !== void 0 ? findCredential(options.env, endpoint) : void 0,
25226
25466
  actAs: firstValue(options.env[ACT_AS_ENV]),
25227
25467
  debug: debugEnabled(options.env),
25228
25468
  agent: firstValue(options.agent, user.agent) ?? DEFAULT_AGENT,
@@ -25235,6 +25475,15 @@ function resolveContext(options) {
25235
25475
  projectConfig: project
25236
25476
  };
25237
25477
  }
25478
+ function rememberEndpoint(env, endpoint) {
25479
+ const path = defaultConfigPath(env);
25480
+ const raw = readJsonFile(path) ?? {};
25481
+ if (stringField(raw, "endpoint", path) !== void 0) return void 0;
25482
+ mkdirSync2(dirname3(path), { recursive: true });
25483
+ writeFileSync2(path, `${JSON.stringify({ ...raw, endpoint }, null, 2)}
25484
+ `, "utf8");
25485
+ return path;
25486
+ }
25238
25487
 
25239
25488
  // src/ids.ts
25240
25489
  var FULL_ID_LENGTH = 36;
@@ -25438,7 +25687,7 @@ var ApiClient = class {
25438
25687
  constructor(options) {
25439
25688
  this.endpoint = options.endpoint;
25440
25689
  this.org = options.org;
25441
- this.#base = trimSlash(options.endpoint);
25690
+ this.#base = trimSlash2(options.endpoint);
25442
25691
  this.#auth = options.authorizer;
25443
25692
  this.#beforeFirst = options.beforeFirst;
25444
25693
  this.#http = {
@@ -25516,6 +25765,13 @@ var ApiClient = class {
25516
25765
  const retagged = answer2.body["retagged"];
25517
25766
  return { ...answer2, retagged: typeof retagged === "number" ? retagged : 0 };
25518
25767
  }
25768
+ /**
25769
+ * Soft-deletes a project (section 7.5). The route hides it and its
25770
+ * complaints everywhere and revokes its tokens, and answers `204`.
25771
+ */
25772
+ async deleteProject(name) {
25773
+ await this.request("DELETE", this.projectPath(name));
25774
+ }
25519
25775
  /** Lists the tokens of a project, newest first, as the route sorts them. */
25520
25776
  async tokens(project) {
25521
25777
  const body = await this.request("GET", `${this.projectPath(project)}/tokens`);
@@ -25804,7 +26060,7 @@ import {
25804
26060
  closeSync,
25805
26061
  fstatSync,
25806
26062
  linkSync,
25807
- mkdirSync as mkdirSync2,
26063
+ mkdirSync as mkdirSync3,
25808
26064
  openSync,
25809
26065
  readFileSync as readFileSync3,
25810
26066
  readSync,
@@ -25878,7 +26134,7 @@ function endsInsideLine(path) {
25878
26134
  }
25879
26135
  function appendLine(path, value) {
25880
26136
  try {
25881
- mkdirSync2(dirname4(path), { recursive: true, mode: HOME_MODE });
26137
+ mkdirSync3(dirname4(path), { recursive: true, mode: HOME_MODE });
25882
26138
  const lead = endsInsideLine(path) ? "\n" : "";
25883
26139
  appendFileSync(path, `${lead}${JSON.stringify(value)}
25884
26140
  `, {
@@ -25996,7 +26252,7 @@ function createLock(lock, owner = { pid: process.pid, host: hostname3(), at: (/*
25996
26252
  const text = JSON.stringify(owner);
25997
26253
  let fd;
25998
26254
  try {
25999
- mkdirSync2(dirname4(lock), { recursive: true, mode: HOME_MODE });
26255
+ mkdirSync3(dirname4(lock), { recursive: true, mode: HOME_MODE });
26000
26256
  fd = openSync(lock, "wx", QUEUE_MODE);
26001
26257
  } catch (error62) {
26002
26258
  if (error62.code === "EEXIST") return void 0;
@@ -26046,8 +26302,8 @@ function releaseLock(lock, text) {
26046
26302
  }
26047
26303
  function stopReason(status, text) {
26048
26304
  if (status >= 500) return `the API answered ${status}`;
26049
- const code = serverCode(text);
26050
- const stops = status === 429 || status === 401 || status === 403 || status === 404 && code === ORG_NOT_FOUND;
26305
+ const code2 = serverCode(text);
26306
+ const stops = status === 429 || status === 401 || status === 403 || status === 404 && code2 === ORG_NOT_FOUND;
26051
26307
  if (!stops) return void 0;
26052
26308
  const message = stringOf(asRecord(asRecord(parseQuietly(text))?.["error"]), "message");
26053
26309
  return message ?? `the API answered ${status}`;
@@ -26117,12 +26373,12 @@ async function flushQueue(options) {
26117
26373
  return result;
26118
26374
  }
26119
26375
  const record2 = asRecord(asRecord(parseQuietly(text))?.["error"]);
26120
- const code = stringOf(record2, "code") ?? `http_${status}`;
26376
+ const code2 = stringOf(record2, "code") ?? `http_${status}`;
26121
26377
  const message = stringOf(record2, "message") ?? `the API answered ${status}`;
26122
26378
  appendLine(options.path, {
26123
26379
  op: "dropped",
26124
26380
  key: entry.key,
26125
- code,
26381
+ code: code2,
26126
26382
  message,
26127
26383
  droppedAt: now().toISOString()
26128
26384
  });
@@ -26176,23 +26432,23 @@ function actAsProblem(context) {
26176
26432
  if (context.credential !== void 0) {
26177
26433
  return `you are logged in to ${endpoint}`;
26178
26434
  }
26179
- if (!isLoopback(endpoint)) return `${endpoint} is not a loopback endpoint`;
26435
+ if (!isLoopbackUrl(endpoint)) return `${endpoint} is not a loopback endpoint`;
26180
26436
  return void 0;
26181
26437
  }
26182
26438
  function checkActAs(context) {
26183
26439
  const problem = actAsProblem(context);
26184
26440
  if (problem !== void 0) throw actAsNotApplicable(problem);
26185
26441
  }
26186
- function openClient(context, io, options = {}) {
26442
+ async function openClient(context, io, options = {}) {
26443
+ const endpoint = await resolveEndpoint(context, io);
26187
26444
  checkActAs(context);
26188
- const endpoint = requireEndpoint(context);
26189
- const http = { debug: context.debug, fetch: io.fetch };
26445
+ const http = { debug: context.debug, fetch: io.fetch, timeoutMs: context.timeoutMs };
26190
26446
  const org = {
26191
26447
  ...context.org === void 0 ? {} : { org: context.org },
26192
26448
  ...options.queue === false ? {} : { beforeFirst: queueSender(context, endpoint, io) }
26193
26449
  };
26194
26450
  if (context.token === void 0 && context.credential === void 0) {
26195
- if (!isLoopback(endpoint)) throw notLoggedIn(endpoint);
26451
+ if (!isLoopbackUrl(endpoint)) throw notLoggedIn(endpoint);
26196
26452
  return new ApiClient({
26197
26453
  endpoint,
26198
26454
  authorizer: devAuthorizer(endpoint, context.actAs, http),
@@ -26207,26 +26463,27 @@ function openClient(context, io, options = {}) {
26207
26463
  ...http
26208
26464
  });
26209
26465
  }
26210
- function openOrgClient(context, io) {
26211
- checkActAs(context);
26466
+ async function openOrgClient(context, io, options = {}) {
26467
+ if (context.mode === "local") checkActAs(context);
26212
26468
  requireOrg(context);
26213
- return openClient(context, io);
26469
+ return openClient(context, io, options);
26214
26470
  }
26215
- function openBackend(context, io) {
26471
+ async function openBackend(context, io) {
26216
26472
  if (context.mode === "local") {
26217
26473
  checkActAs(context);
26474
+ refuseForeignOrg(context);
26218
26475
  const local = new FileStore(context.storePath);
26219
26476
  return { store: local, client: void 0, local };
26220
26477
  }
26221
- const client = openOrgClient(context, io);
26478
+ const client = await openOrgClient(context, io);
26222
26479
  return { store: new RemoteStore(client), client, local: void 0 };
26223
26480
  }
26224
- function openStore(context, io) {
26225
- return openBackend(context, io).store;
26481
+ async function openStore(context, io) {
26482
+ return (await openBackend(context, io)).store;
26226
26483
  }
26227
26484
  function needsServer(command, context) {
26228
26485
  return new ConfigError(
26229
- context.endpoint === void 0 ? `${command} needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc` : `${command} needs a server: drop --local`
26486
+ `${command} needs a server: ${context.localFix ?? "drop --local"}`
26230
26487
  );
26231
26488
  }
26232
26489
  function requireServer(command, context) {
@@ -26242,7 +26499,7 @@ async function runComplain(message, options, context, io) {
26242
26499
  throw new UsageError(REMOTE_REPORTER_MESSAGE);
26243
26500
  }
26244
26501
  const body = await readMessage(message, io);
26245
- const store = openStore(context, io);
26502
+ const store = await openStore(context, io);
26246
26503
  const input2 = {
26247
26504
  org,
26248
26505
  project,
@@ -26266,6 +26523,7 @@ async function runComplain(message, options, context, io) {
26266
26523
  }
26267
26524
  if (options.json === true) {
26268
26525
  writeJson(io.stdout, complaint);
26526
+ localNotice(context, io);
26269
26527
  return 0;
26270
26528
  }
26271
26529
  const scope = context.mode === "remote" ? `${complaint.org}/${complaint.project}` : complaint.project;
@@ -26274,8 +26532,16 @@ async function runComplain(message, options, context, io) {
26274
26532
  `Filed complaint ${complaint.id} for ${scope} (${complaint.severity})`
26275
26533
  );
26276
26534
  for (const line of matchLines(complaint)) writeLine(io.stdout, line);
26535
+ localNotice(context, io);
26277
26536
  return 0;
26278
26537
  }
26538
+ function localNotice(context, io) {
26539
+ if (context.mode !== "local") return;
26540
+ writeLine(
26541
+ io.stderr,
26542
+ `quibble: local mode: nothing was sent; the complaint is in ${context.storePath}`
26543
+ );
26544
+ }
26279
26545
  function matchLines(complaint) {
26280
26546
  const lines = [];
26281
26547
  if (complaint.openMatches === 1) {
@@ -26319,7 +26585,7 @@ function describeCredential(context) {
26319
26585
  if (context.credential !== void 0) {
26320
26586
  return `logged in as ${context.credential.email}`;
26321
26587
  }
26322
- if (context.endpoint !== void 0 && isLoopback(context.endpoint)) {
26588
+ if (context.endpoint !== void 0 && isLoopbackUrl(context.endpoint)) {
26323
26589
  return "loopback dev token";
26324
26590
  }
26325
26591
  return "not logged in";
@@ -26331,14 +26597,18 @@ function describeAuth(context) {
26331
26597
  if (problem === void 0) return `${credential}, acting as ${context.actAs}`;
26332
26598
  return `${credential}; ${ACT_AS_ENV} does not apply: ${problem}`;
26333
26599
  }
26334
- function runConfig(options, context, io) {
26600
+ async function runConfig(options, context, io) {
26601
+ if (context.mode === "remote") await resolveEndpoint(context, io);
26602
+ const ignored = foreignOrgMessage(context) === void 0 ? void 0 : context.slug;
26335
26603
  const report = {
26336
26604
  project: context.project ?? "none",
26337
26605
  org: context.org ?? null,
26606
+ orgIgnored: ignored ?? null,
26338
26607
  projectRoot: context.projectRoot ?? null,
26339
26608
  configPath: context.configPath ?? null,
26340
26609
  mode: context.mode,
26341
- endpoint: context.endpoint ?? null,
26610
+ endpoint: context.endpoint ?? "local",
26611
+ endpointSource: context.endpointSource,
26342
26612
  storePath: context.storePath,
26343
26613
  agent: context.agent,
26344
26614
  reporter: context.reporter,
@@ -26351,16 +26621,84 @@ function runConfig(options, context, io) {
26351
26621
  writeJson(io.stdout, report);
26352
26622
  return 0;
26353
26623
  }
26354
- const width = Math.max(...Object.keys(report).map((key) => key.length));
26355
- for (const [key, value] of Object.entries(report)) {
26356
- writeLine(io.stdout, `${key.padEnd(width)} ${value ?? "none"}`);
26624
+ const { orgIgnored, ...lines } = report;
26625
+ const text = { ...lines };
26626
+ if (orgIgnored !== null) {
26627
+ text["org"] = `${report.org ?? ""}; ${orgIgnored} does not apply: ${context.localFix ?? ""}`;
26628
+ }
26629
+ const width = Math.max(...Object.keys(text).map((key) => key.length));
26630
+ for (const [key, value] of Object.entries(text)) {
26631
+ writeLine(io.stdout, `${key.padEnd(width)} ${String(value ?? "none")}`);
26357
26632
  }
26358
26633
  return 0;
26359
26634
  }
26360
26635
 
26361
26636
  // src/commands/init.ts
26362
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "node:fs";
26363
- import { basename as basename2, join as join6, resolve as resolve3 } from "node:path";
26637
+ import { existsSync as existsSync2, writeFileSync as writeFileSync5 } from "node:fs";
26638
+ import { basename as basename2, join as join7, resolve as resolve3 } from "node:path";
26639
+
26640
+ // src/hook.ts
26641
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
26642
+ import { dirname as dirname5, join as join5 } from "node:path";
26643
+ var SETTINGS_PATH = ".claude/settings.json";
26644
+ var HOOK_COMMAND = "command -v quibble >/dev/null 2>&1 && quibble sync --quiet || true";
26645
+ var HOOK_MARK = "quibble sync --quiet";
26646
+ function hookEntry() {
26647
+ return {
26648
+ matcher: "startup",
26649
+ hooks: [{ type: "command", command: HOOK_COMMAND }]
26650
+ };
26651
+ }
26652
+ function isObject2(value) {
26653
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26654
+ }
26655
+ function runsSync(entry) {
26656
+ if (!isObject2(entry) || !Array.isArray(entry["hooks"])) return false;
26657
+ return entry["hooks"].some(
26658
+ (hook) => isObject2(hook) && typeof hook["command"] === "string" && hook["command"].includes(HOOK_MARK)
26659
+ );
26660
+ }
26661
+ function planHook(root) {
26662
+ const path = join5(root, SETTINGS_PATH);
26663
+ let text;
26664
+ try {
26665
+ text = readFileSync4(path, "utf8");
26666
+ } catch (error62) {
26667
+ if (!isNotFound(error62)) throw error62;
26668
+ const created = { hooks: { SessionStart: [hookEntry()] } };
26669
+ return { path, text: `${JSON.stringify(created, null, 2)}
26670
+ ` };
26671
+ }
26672
+ const invalid = new UsageError(
26673
+ `${path} is not valid JSON: fix it, then run the command again`
26674
+ );
26675
+ let settings;
26676
+ try {
26677
+ settings = JSON.parse(text);
26678
+ } catch {
26679
+ throw invalid;
26680
+ }
26681
+ if (!isObject2(settings)) throw invalid;
26682
+ const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
26683
+ if (!isObject2(hooks)) throw new UsageError(`${path}: hooks must be an object`);
26684
+ const start = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
26685
+ if (!Array.isArray(start)) {
26686
+ throw new UsageError(`${path}: hooks.SessionStart must be an array`);
26687
+ }
26688
+ if (start.some(runsSync)) return { path, text: void 0 };
26689
+ const merged = {
26690
+ ...settings,
26691
+ hooks: { ...hooks, SessionStart: [...start, hookEntry()] }
26692
+ };
26693
+ return { path, text: `${JSON.stringify(merged, null, 2)}
26694
+ ` };
26695
+ }
26696
+ function applyHook(plan) {
26697
+ if (plan.text === void 0) return `${plan.path} already runs quibble sync`;
26698
+ mkdirSync4(dirname5(plan.path), { recursive: true });
26699
+ writeFileSync3(plan.path, plan.text, "utf8");
26700
+ return `Added the quibble sync hook to ${plan.path}`;
26701
+ }
26364
26702
 
26365
26703
  // src/snippet.ts
26366
26704
  import { basename } from "node:path";
@@ -26423,57 +26761,131 @@ function agentForFile(path) {
26423
26761
  function hasSnippet(text) {
26424
26762
  return text.split(/\r?\n/).some((line) => line.trimEnd() === SNIPPET_HEADING);
26425
26763
  }
26764
+ function splitLines(text) {
26765
+ const lines = [];
26766
+ const pattern = /([^\r\n]*)(\r\n|\n|\r)?/g;
26767
+ let match;
26768
+ while ((match = pattern.exec(text)) !== null) {
26769
+ const [whole, content = "", eol = ""] = match;
26770
+ if (whole === "") break;
26771
+ lines.push({ text: content, eol });
26772
+ }
26773
+ return lines;
26774
+ }
26775
+ function fenceOf(text) {
26776
+ if (text.startsWith("```")) return "```";
26777
+ if (text.startsWith("~~~")) return "~~~";
26778
+ return void 0;
26779
+ }
26780
+ function endsSection(text) {
26781
+ return text.startsWith("# ") || text.startsWith("## ");
26782
+ }
26783
+ function agentOfText(lines) {
26784
+ const line = lines.find(
26785
+ (text) => text.includes("quibble complain") && text.includes("--agent")
26786
+ );
26787
+ if (line === void 0) return void 0;
26788
+ return /--agent\s+(\S+)/.exec(line)?.[1];
26789
+ }
26790
+ function findSection(text) {
26791
+ const lines = splitLines(text);
26792
+ const start = lines.findIndex((line) => line.text.trimEnd() === SNIPPET_HEADING);
26793
+ if (start < 0) return void 0;
26794
+ let next = lines.length;
26795
+ let fence;
26796
+ for (let index = start + 1; index < lines.length; index += 1) {
26797
+ const content = lines[index]?.text ?? "";
26798
+ if (fence !== void 0) {
26799
+ if (content.startsWith(fence)) fence = void 0;
26800
+ continue;
26801
+ }
26802
+ fence = fenceOf(content);
26803
+ if (fence === void 0 && endsSection(content)) {
26804
+ next = index;
26805
+ break;
26806
+ }
26807
+ }
26808
+ let end = next;
26809
+ while (end > start + 1 && (lines[end - 1]?.text.trim() ?? "") === "") end -= 1;
26810
+ const eol = lines[start]?.eol || "\n";
26811
+ return { lines, start, end, eol: eol === "\r\n" ? "\r\n" : "\n" };
26812
+ }
26813
+ function sectionText(section) {
26814
+ return section.lines.slice(section.start, section.end).map((line) => line.text);
26815
+ }
26816
+ function replaceSection(section, snippet) {
26817
+ const join9 = (lines) => lines.map((line) => `${line.text}${line.eol}`).join("");
26818
+ const body = snippet.split("\n").map((line) => `${line}${section.eol}`).join("");
26819
+ return `${join9(section.lines.slice(0, section.start))}${body}${join9(section.lines.slice(section.end))}`;
26820
+ }
26821
+ function withLineBreak(text, old) {
26822
+ return old.includes("\r\n") ? text.replace(/\r?\n/g, "\r\n") : text;
26823
+ }
26824
+ function orList(items) {
26825
+ if (items.length <= 1) return items.join("");
26826
+ return `${items.slice(0, -1).join(", ")} or ${items.at(-1) ?? ""}`;
26827
+ }
26828
+ function refreshedLine(project, tags) {
26829
+ const lead = `Quibble refreshed its instructions for ${project}:`;
26830
+ if (tags === void 0 || tags.length === 0) return `${lead} any tag is allowed.`;
26831
+ return `${lead} tag each complaint with ${orList(tags)}, or ${OTHER_TAG_PREFIX}<name> when none fits.`;
26832
+ }
26426
26833
 
26427
26834
  // src/commands/snippet.ts
26428
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
26429
- import { dirname as dirname5, join as join5, resolve as resolve2 } from "node:path";
26835
+ import { existsSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
26836
+ import { dirname as dirname6, join as join6, resolve as resolve2 } from "node:path";
26430
26837
  function existsError(path) {
26431
26838
  return new UsageError(`${path} already exists: pass --force to overwrite it`);
26432
26839
  }
26433
26840
  function readOptional2(path) {
26434
26841
  try {
26435
- return readFileSync4(path, "utf8");
26842
+ return readFileSync5(path, "utf8");
26436
26843
  } catch (error62) {
26437
26844
  if (isNotFound(error62)) return "";
26438
26845
  throw error62;
26439
26846
  }
26440
26847
  }
26441
26848
  function writeSkill(path, snippet) {
26442
- mkdirSync3(dirname5(path), { recursive: true });
26443
- writeFileSync2(path, skillFile(snippet), "utf8");
26849
+ mkdirSync5(dirname6(path), { recursive: true });
26850
+ writeFileSync4(path, skillFile(snippet), "utf8");
26851
+ }
26852
+ async function snippetSource(project, context, io, options = {}) {
26853
+ let tags;
26854
+ if (context.mode === "remote") {
26855
+ tags = (await (await openOrgClient(context, io, options)).project(project)).tags;
26856
+ } else {
26857
+ checkActAs(context);
26858
+ tags = context.allowedTags;
26859
+ }
26860
+ const scope = {
26861
+ ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26862
+ ...context.slug === void 0 ? {} : { org: context.slug }
26863
+ };
26864
+ return { tags, scope };
26444
26865
  }
26445
26866
  async function runSnippet(options, context, io) {
26446
26867
  const project = requireProject(context);
26447
26868
  const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
26448
26869
  const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
26449
- const skillPath = options.skill === true ? join5(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
26870
+ const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
26450
26871
  const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
26451
26872
  if (options.append !== void 0 && hasSnippet(existing)) {
26452
26873
  throw new UsageError(
26453
- `${options.append} already has a "${SNIPPET_HEADING}" section: edit it by hand`
26874
+ `${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
26454
26875
  );
26455
26876
  }
26456
26877
  if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
26457
26878
  throw existsError(skillPath);
26458
26879
  }
26459
- let tags;
26460
- if (context.mode === "remote") {
26461
- tags = (await openOrgClient(context, io).project(project)).tags;
26462
- } else {
26463
- checkActAs(context);
26464
- tags = context.allowedTags;
26465
- }
26466
- const scope = {
26467
- ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26468
- ...context.slug === void 0 ? {} : { org: context.slug }
26469
- };
26880
+ const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
26881
+ const { tags, scope } = await snippetSource(project, context, io);
26470
26882
  let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
26471
26883
  shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
26472
26884
  if (appendPath !== void 0) {
26473
26885
  const agent = override ?? agentForFile(appendPath);
26474
26886
  const snippet = agentSnippet(tags, { ...scope, agent });
26475
26887
  const kept = existing.replace(/\s+$/, "");
26476
- writeFileSync2(appendPath, kept === "" ? `${snippet}
26888
+ writeFileSync4(appendPath, kept === "" ? `${snippet}
26477
26889
  ` : `${kept}
26478
26890
 
26479
26891
  ${snippet}
@@ -26486,23 +26898,25 @@ ${snippet}
26486
26898
  writeSkill(skillPath, snippet);
26487
26899
  if (appendPath === void 0) shown = { agent, snippet };
26488
26900
  }
26901
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26489
26902
  if (options.json === true) {
26490
26903
  writeJson(io.stdout, {
26491
26904
  snippet: shown.snippet,
26492
26905
  agent: shown.agent,
26493
26906
  appended: appendPath ?? null,
26494
- skill: skillPath ?? null
26907
+ skill: skillPath ?? null,
26908
+ hook: hook?.path ?? null
26495
26909
  });
26496
26910
  return 0;
26497
26911
  }
26498
26912
  if (appendPath === void 0 && skillPath === void 0) {
26499
26913
  writeLine(io.stdout, shown.snippet);
26500
- return 0;
26501
26914
  }
26502
26915
  if (appendPath !== void 0) {
26503
26916
  writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
26504
26917
  }
26505
26918
  if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26919
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26506
26920
  return 0;
26507
26921
  }
26508
26922
 
@@ -26513,34 +26927,47 @@ function agentLead(agent) {
26513
26927
  }
26514
26928
  async function runInit(options, io) {
26515
26929
  const directory = resolve3(io.cwd);
26516
- const path = join6(directory, PROJECT_CONFIG_FILENAME);
26930
+ const path = join7(directory, PROJECT_CONFIG_FILENAME);
26517
26931
  if (existsSync2(path) && options.force !== true) {
26518
26932
  throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
26519
26933
  }
26520
- const skillPath = options.skill === true ? join6(directory, SKILL_PATH) : void 0;
26934
+ const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
26521
26935
  if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
26522
26936
  throw existsError(skillPath);
26523
26937
  }
26524
26938
  const project = resolveProjectName(options.project, directory);
26525
26939
  const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
26526
- const endpoint = options.endpoint?.trim();
26527
26940
  const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
26528
26941
  const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
26942
+ const hook = options.hook === true ? planHook(directory) : void 0;
26943
+ const endpoint = await endpointToWrite(options.endpoint, io);
26529
26944
  const config2 = {
26530
26945
  project,
26531
26946
  ...org === void 0 ? {} : { org },
26532
- ...endpoint === void 0 || endpoint === "" ? {} : { endpoint },
26947
+ ...endpoint === void 0 ? {} : { endpoint },
26533
26948
  ...tags === void 0 ? {} : { tags }
26534
26949
  };
26535
- writeFileSync3(path, `${JSON.stringify(config2, null, 2)}
26950
+ writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
26536
26951
  `, "utf8");
26952
+ const context = {
26953
+ ...resolveContext({
26954
+ cwd: io.cwd,
26955
+ env: io.env,
26956
+ endpoint: options.endpoint,
26957
+ org: options.org,
26958
+ local: options.local
26959
+ }),
26960
+ // `init` acts as nobody, so it ignores `QUIBBLE_ACT_AS`.
26961
+ actAs: void 0
26962
+ };
26537
26963
  const snippet = agentSnippet(tags, {
26538
26964
  agent,
26539
- ...endpoint === void 0 ? {} : { endpoint },
26965
+ ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26540
26966
  ...org === void 0 ? {} : { org }
26541
26967
  });
26542
26968
  if (skillPath !== void 0) writeSkill(skillPath, snippet);
26543
- const check2 = await checkServer(options, project, io);
26969
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26970
+ const check2 = await checkServer(context, project, io);
26544
26971
  if (options.json === true) {
26545
26972
  writeJson(io.stdout, {
26546
26973
  path,
@@ -26548,12 +26975,14 @@ async function runInit(options, io) {
26548
26975
  org: org ?? null,
26549
26976
  tags: tags ?? null,
26550
26977
  skill: skillPath ?? null,
26978
+ hook: hook?.path ?? null,
26551
26979
  server: check2.server
26552
26980
  });
26553
26981
  return 0;
26554
26982
  }
26555
26983
  writeLine(io.stdout, `Wrote ${path}`);
26556
26984
  if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26985
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26557
26986
  writeLine(io.stdout, `Project: ${project}`);
26558
26987
  if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
26559
26988
  if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
@@ -26571,26 +27000,28 @@ async function runInit(options, io) {
26571
27000
  }
26572
27001
  return 0;
26573
27002
  }
26574
- async function checkServer(options, project, io) {
26575
- const context = {
26576
- ...resolveContext({
26577
- cwd: io.cwd,
26578
- env: io.env,
26579
- endpoint: options.endpoint,
26580
- org: options.org,
26581
- local: options.local
26582
- }),
26583
- actAs: void 0
26584
- };
26585
- const { endpoint, org } = context;
26586
- if (context.mode !== "remote" || endpoint === void 0 || org === void 0 || context.token === void 0 && context.credential === void 0 && !isLoopback(endpoint)) {
27003
+ async function endpointToWrite(given, io) {
27004
+ if (given === void 0 || given.trim() === "") return void 0;
27005
+ const value = parseEndpoint(given, "--endpoint");
27006
+ if (value.kind === "local") return LOCAL_ENDPOINT;
27007
+ const api = await discoverApiUrl(value.url, {
27008
+ fetch: io.fetch,
27009
+ debug: debugEnabled(io.env)
27010
+ });
27011
+ return isDefaultEndpoint(api) ? void 0 : api;
27012
+ }
27013
+ async function checkServer(context, project, io) {
27014
+ const org = context.org;
27015
+ if (context.mode !== "remote" || org === void 0) return { server: "unchecked" };
27016
+ const endpoint = await resolveEndpoint(context, io);
27017
+ if (context.token === void 0 && context.credential === void 0 && !isLoopbackUrl(endpoint)) {
26587
27018
  return { server: "unchecked" };
26588
27019
  }
26589
27020
  const warn = (text) => {
26590
27021
  writeLine(io.stderr, `quibble: warning: ${text}`);
26591
27022
  };
26592
27023
  try {
26593
- await openClient(context, io, { queue: false }).project(project);
27024
+ await (await openClient(context, io, { queue: false })).project(project);
26594
27025
  return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
26595
27026
  } catch (error62) {
26596
27027
  if (error62 instanceof RemoteError && error62.status === 404) {
@@ -26742,12 +27173,12 @@ var REPORTER_WIDTH = 20;
26742
27173
  var TAGS_WIDTH = 24;
26743
27174
  var SUMMARY_MAX = 60;
26744
27175
  var RECURRED_MARK = "\u21BB";
26745
- function cut2(text, width) {
27176
+ function cut3(text, width) {
26746
27177
  if (text.length <= width) return text;
26747
27178
  return `${text.slice(0, width - 3)}...`;
26748
27179
  }
26749
27180
  function summaryOf(complaint) {
26750
- return cut2(printable(summarize(complaint.message)), SUMMARY_MAX);
27181
+ return cut3(printable(summarize(complaint.message)), SUMMARY_MAX);
26751
27182
  }
26752
27183
  function limitNote(count, limit, more) {
26753
27184
  if (limit >= MAX_LIMIT) {
@@ -26760,7 +27191,7 @@ async function runList(options, context, io) {
26760
27191
  if (options.ids === true && options.json === true) {
26761
27192
  throw new UsageError("--ids and --json cannot be used together");
26762
27193
  }
26763
- const { store, client } = openBackend(context, io);
27194
+ const { store, client } = await openBackend(context, io);
26764
27195
  const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
26765
27196
  const filter = parseListFilter({
26766
27197
  ...resolveScope(options, context),
@@ -26801,7 +27232,7 @@ async function runList(options, context, io) {
26801
27232
  return 0;
26802
27233
  }
26803
27234
  function reporterOf(complaint) {
26804
- return cut2(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
27235
+ return cut3(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
26805
27236
  }
26806
27237
  function groupCell(group) {
26807
27238
  const count = group.count > 1 ? `\xD7${group.count}` : "";
@@ -26818,7 +27249,7 @@ function formatTable(rows2) {
26818
27249
  status: complaint.status,
26819
27250
  project: complaint.project,
26820
27251
  reporter: reporterOf(complaint),
26821
- tags: cut2(printable(complaint.tags.join(",")), TAGS_WIDTH),
27252
+ tags: cut3(printable(complaint.tags.join(",")), TAGS_WIDTH),
26822
27253
  group: group === void 0 ? "" : groupCell(group),
26823
27254
  summary: summaryOf(complaint)
26824
27255
  };
@@ -26835,13 +27266,13 @@ function formatTable(rows2) {
26835
27266
  }
26836
27267
 
26837
27268
  // src/commands/orgs.ts
26838
- var ORGS_NEEDS_SERVER = "orgs needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
26839
27269
  var ORGS_PAGE = "/orgs";
26840
27270
  var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
26841
27271
  async function runOrgs(options, context, io) {
26842
- if (context.mode !== "remote") throw new ConfigError(ORGS_NEEDS_SERVER);
27272
+ requireServer("orgs", context);
27273
+ const client = await openClient(context, io);
26843
27274
  const endpoint = requireEndpoint(context);
26844
- const answer2 = await openClient(context, io).orgs();
27275
+ const answer2 = await client.orgs();
26845
27276
  if (options.json === true) {
26846
27277
  writeJson(io.stdout, answer2.body);
26847
27278
  return 0;
@@ -26849,9 +27280,10 @@ async function runOrgs(options, context, io) {
26849
27280
  if (answer2.orgs.length === 0) {
26850
27281
  const config2 = await fetchAuthConfig(endpoint, {
26851
27282
  debug: context.debug,
26852
- fetch: io.fetch
27283
+ fetch: io.fetch,
27284
+ timeoutMs: context.timeoutMs
26853
27285
  });
26854
- const webUrl = config2.mode === "cognito" ? trimSlash(config2.webUrl) : endpoint;
27286
+ const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
26855
27287
  writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE}.`);
26856
27288
  return 0;
26857
27289
  }
@@ -26874,6 +27306,81 @@ function formatOrgs(orgs) {
26874
27306
  return [format(COLUMNS2), ...rows2.map(format)];
26875
27307
  }
26876
27308
 
27309
+ // src/commands/login.ts
27310
+ import { hostname as hostname4 } from "node:os";
27311
+
27312
+ // src/opener.ts
27313
+ import { spawn as nodeSpawn } from "node:child_process";
27314
+ var OPEN_TIMEOUT_MS = 5e3;
27315
+ var OPENABLE_URL_RE = /^https?:\/\/[A-Za-z0-9.:[\]-]+(?:\/[A-Za-z0-9/?=._~-]*)?$/;
27316
+ function isWsl(platform, env) {
27317
+ if (platform !== "linux") return false;
27318
+ const set2 = (name) => (env[name] ?? "") !== "";
27319
+ return set2("WSL_DISTRO_NAME") || set2("WSL_INTEROP");
27320
+ }
27321
+ function openCommands(url2, platform, env) {
27322
+ if (platform === "darwin") return [{ command: "open", args: [url2] }];
27323
+ if (platform === "win32") {
27324
+ return [{ command: "cmd", args: ["/c", "start", "", url2] }];
27325
+ }
27326
+ if (isWsl(platform, env)) {
27327
+ return [
27328
+ { command: "wslview", args: [url2] },
27329
+ { command: "cmd.exe", args: ["/c", "start", "", url2] }
27330
+ ];
27331
+ }
27332
+ return [{ command: "xdg-open", args: [url2] }];
27333
+ }
27334
+ function attempt(spawn, entry, timeoutMs) {
27335
+ return new Promise((resolve4) => {
27336
+ let settled = false;
27337
+ const settle2 = (opened) => {
27338
+ if (settled) return;
27339
+ settled = true;
27340
+ clearTimeout(timer);
27341
+ resolve4(opened);
27342
+ };
27343
+ const timer = setTimeout(() => {
27344
+ settle2(false);
27345
+ }, timeoutMs);
27346
+ timer.unref();
27347
+ let child;
27348
+ try {
27349
+ child = spawn(entry.command, entry.args, {
27350
+ shell: false,
27351
+ detached: true,
27352
+ stdio: "ignore",
27353
+ windowsHide: true
27354
+ });
27355
+ } catch {
27356
+ settle2(false);
27357
+ return;
27358
+ }
27359
+ child.on("error", () => {
27360
+ settle2(false);
27361
+ });
27362
+ child.on("exit", (code2) => {
27363
+ settle2(code2 === 0);
27364
+ });
27365
+ child.unref();
27366
+ });
27367
+ }
27368
+ function platformOpener(options = {}) {
27369
+ const platform = options.platform ?? process.platform;
27370
+ const env = options.env ?? process.env;
27371
+ const spawn = options.spawn ?? nodeSpawn;
27372
+ const timeoutMs = options.timeoutMs ?? OPEN_TIMEOUT_MS;
27373
+ return {
27374
+ async open(url2) {
27375
+ if (!OPENABLE_URL_RE.test(url2)) return false;
27376
+ for (const entry of openCommands(url2, platform, env)) {
27377
+ if (await attempt(spawn, entry, timeoutMs)) return true;
27378
+ }
27379
+ return false;
27380
+ }
27381
+ };
27382
+ }
27383
+
26877
27384
  // src/terminal.ts
26878
27385
  import { createInterface } from "node:readline";
26879
27386
  import { Writable } from "node:stream";
@@ -26911,19 +27418,19 @@ var NodeTerminal = class {
26911
27418
  #interface() {
26912
27419
  if (this.#rl !== void 0) return this.#rl;
26913
27420
  const output2 = this.#output;
26914
- const sink = new Writable({
27421
+ const sink2 = new Writable({
26915
27422
  write: (chunk, _encoding, done) => {
26916
27423
  if (!this.#muted) output2.write(chunk);
26917
27424
  done();
26918
27425
  }
26919
27426
  });
26920
- Object.defineProperties(sink, {
27427
+ Object.defineProperties(sink2, {
26921
27428
  columns: { get: () => output2.columns },
26922
27429
  rows: { get: () => output2.rows }
26923
27430
  });
26924
27431
  const rl = createInterface({
26925
27432
  input: this.#input,
26926
- output: sink,
27433
+ output: sink2,
26927
27434
  terminal: true,
26928
27435
  // No answer enters the history, so an arrow key recalls no password.
26929
27436
  historySize: 0
@@ -26995,15 +27502,40 @@ function nodeTerminal(input2, output2) {
26995
27502
 
26996
27503
  // src/commands/login.ts
26997
27504
  var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
26998
- var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal: in CI, set QUIBBLE_TOKEN instead";
27505
+ var NEEDS_TERMINAL_MESSAGE = "quibble login --password needs a terminal: in CI, set QUIBBLE_TOKEN instead";
27506
+ var CI_MESSAGE = "quibble login needs a person to approve it in a browser: in CI, set QUIBBLE_TOKEN instead";
27507
+ var NO_BROWSER_MESSAGE = "--no-browser applies to the browser login only";
27508
+ var DENIED_MESSAGE = "login was denied in the browser";
27509
+ var EXPIRED_MESSAGE = "the login request expired: run quibble login again";
27510
+ var MALFORMED_MESSAGE = "the API answered a malformed login request";
27511
+ var UNKNOWN_STATUS_MESSAGE = "the API answered an unknown login status";
27512
+ var MFA_NEEDS_TERMINAL_MESSAGE = "this account needs a two-factor code: run quibble login in a terminal";
26999
27513
  var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
27514
+ var CUSTOM_CHALLENGE = "CUSTOM_CHALLENGE";
27000
27515
  var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
27516
+ var CLI_LOGINS_PATH = "/v1/cli-logins";
27517
+ var CLI_LOGIN_POLL_PATH = "/v1/cli-logins/poll";
27518
+ var MIN_INTERVAL_SECONDS = 1;
27519
+ var MAX_INTERVAL_SECONDS = 30;
27520
+ var PLAIN_HOST_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::[0-9]+)?$/;
27001
27521
  var LOGIN_PAGE = "/login";
27002
27522
  var ORGS_PAGE2 = "/orgs";
27003
27523
  var NO_PROFILE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
27004
27524
  function terminalOf(io) {
27005
27525
  return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
27006
27526
  }
27527
+ function clockOf2(io) {
27528
+ return io.now ?? (() => /* @__PURE__ */ new Date());
27529
+ }
27530
+ function sleepOf(io) {
27531
+ return io.sleep ?? ((ms) => new Promise((resolve4) => {
27532
+ setTimeout(resolve4, ms);
27533
+ }));
27534
+ }
27535
+ function ciIsSet(env) {
27536
+ const value = (env["CI"] ?? "").trim().toLowerCase();
27537
+ return value !== "" && value !== "0" && value !== "false";
27538
+ }
27007
27539
  async function answer(terminal, label, hidden = false) {
27008
27540
  const text = hidden ? await terminal.promptHidden(label) : await terminal.prompt(label);
27009
27541
  const value = hidden ? text : text.trim();
@@ -27011,6 +27543,23 @@ async function answer(terminal, label, hidden = false) {
27011
27543
  throw new UsageError(`${label.replace(/:$/, "").toLowerCase()} must not be empty`);
27012
27544
  return value;
27013
27545
  }
27546
+ function unsupportedStep(name) {
27547
+ return new RemoteError(
27548
+ `this account needs a sign-in step that quibble doesn't support (${name})`
27549
+ );
27550
+ }
27551
+ function pageError(error62, webUrl) {
27552
+ if (error62 instanceof CognitoError) {
27553
+ const page = `${trimSlash2(webUrl)}${LOGIN_PAGE}`;
27554
+ if (error62.type === "PasswordResetRequiredException") {
27555
+ return new ConfigError(`reset your password at ${page}`);
27556
+ }
27557
+ if (error62.type === "UserNotConfirmedException") {
27558
+ return new ConfigError(`finish your sign-up at ${page}`);
27559
+ }
27560
+ }
27561
+ return error62;
27562
+ }
27014
27563
  async function signIn(terminal, identity, config2) {
27015
27564
  const email3 = await answer(terminal, "Email:");
27016
27565
  const password = await answer(terminal, "Password:", true);
@@ -27023,43 +27572,40 @@ async function signIn(terminal, identity, config2) {
27023
27572
  password
27024
27573
  });
27025
27574
  if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27026
- const code = await answer(terminal, "Code:");
27575
+ const code2 = await answer(terminal, "Code:");
27027
27576
  result = await respondToAuthChallenge(identity, {
27028
27577
  clientId,
27029
27578
  challenge: result.challenge,
27030
27579
  email: email3,
27031
- code
27580
+ code: code2
27032
27581
  });
27033
27582
  }
27034
27583
  if (result.kind === "challenge") {
27035
27584
  const name = result.challenge.challengeName;
27036
27585
  if (name === NEW_PASSWORD_CHALLENGE) {
27037
27586
  throw new ConfigError(
27038
- `finish your first sign-in at ${trimSlash(config2.webUrl)}${LOGIN_PAGE}`
27587
+ `finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
27039
27588
  );
27040
27589
  }
27041
- throw new RemoteError(
27042
- `this account needs a sign-in step that quibble doesn't support (${name})`
27043
- );
27590
+ throw unsupportedStep(name);
27044
27591
  }
27045
27592
  return result.tokens;
27046
27593
  } catch (error62) {
27047
- if (error62 instanceof CognitoError) {
27048
- const page = `${trimSlash(config2.webUrl)}${LOGIN_PAGE}`;
27049
- if (error62.type === "PasswordResetRequiredException") {
27050
- throw new ConfigError(`reset your password at ${page}`);
27051
- }
27052
- if (error62.type === "UserNotConfirmedException") {
27053
- throw new ConfigError(`finish your sign-up at ${page}`);
27054
- }
27055
- }
27056
- throw error62;
27594
+ throw pageError(error62, config2.webUrl);
27057
27595
  }
27058
27596
  }
27059
27597
  async function runLogin(options, context, io) {
27060
- const endpoint = requireEndpoint(context);
27598
+ requireServer("login", context);
27599
+ if (options.password === true) {
27600
+ if (options.browser === false) throw new UsageError(NO_BROWSER_MESSAGE);
27601
+ return passwordLogin(options, context, io);
27602
+ }
27603
+ return browserLogin(options, context, io);
27604
+ }
27605
+ async function passwordLogin(options, context, io) {
27061
27606
  const terminal = terminalOf(io);
27062
27607
  if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
27608
+ const endpoint = await resolveEndpoint(context, io);
27063
27609
  const http = { debug: context.debug, fetch: io.fetch };
27064
27610
  const config2 = await fetchAuthConfig(endpoint, http);
27065
27611
  if (config2.mode === "local") {
@@ -27072,15 +27618,203 @@ async function runLogin(options, context, io) {
27072
27618
  } finally {
27073
27619
  terminal.close();
27074
27620
  }
27621
+ return finishLogin(tokens, endpoint, config2, options, context, io);
27622
+ }
27623
+ async function browserLogin(options, context, io) {
27624
+ if (ciIsSet(io.env)) throw new UsageError(CI_MESSAGE);
27625
+ const endpoint = await resolveEndpoint(context, io);
27626
+ const http = { debug: context.debug, fetch: io.fetch };
27627
+ const config2 = await fetchAuthConfig(endpoint, http);
27628
+ if (config2.mode === "local") {
27629
+ writeLine(io.stdout, `${endpoint} is in local mode: no login needed.`);
27630
+ return 0;
27631
+ }
27632
+ const clock = clockOf2(io);
27633
+ const started = await startLogin(endpoint, config2.webUrl, http, {
27634
+ host: cliLoginHost((io.hostname ?? hostname4)()),
27635
+ os: process.platform,
27636
+ version: options.version
27637
+ });
27638
+ const deadline = clock().getTime() + (CLI_LOGIN_TTL_SECONDS + started.interval) * 1e3;
27639
+ writeLine(
27640
+ io.stderr,
27641
+ `Approve this login in your browser: ${started.url} (code ${started.code}). Waiting...`
27642
+ );
27643
+ if (options.browser !== false) {
27644
+ const opener = io.opener ?? platformOpener({ env: io.env });
27645
+ void opener.open(started.url).catch(() => false);
27646
+ }
27647
+ const approval = await waitForApproval(endpoint, started, deadline, http, io);
27648
+ const tokens = await browserSignIn(
27649
+ { region: config2.region, ...http },
27650
+ config2,
27651
+ approval,
27652
+ io
27653
+ );
27654
+ return finishLogin(tokens, endpoint, config2, options, context, io);
27655
+ }
27656
+ function postJson(url2, body, http) {
27657
+ return send(url2, {
27658
+ method: "POST",
27659
+ headers: { "content-type": "application/json" },
27660
+ body: JSON.stringify(body),
27661
+ ...http
27662
+ });
27663
+ }
27664
+ async function startLogin(endpoint, webUrl, http, body) {
27665
+ const url2 = `${trimSlash2(endpoint)}${CLI_LOGINS_PATH}`;
27666
+ const site = { method: "POST", url: url2, debug: http.debug };
27667
+ const response = await postJson(url2, body, http);
27668
+ const text = await readText(response, site);
27669
+ if (response.status === 404) {
27670
+ throw new RemoteError(
27671
+ `${endpoint} does not support the browser login: run quibble login --password${detail(site, 404)}`,
27672
+ { status: 404 }
27673
+ );
27674
+ }
27675
+ if (!response.ok) throw remoteFailure(site, response, text);
27676
+ const started = checkStart(asRecord(parseQuietly(text)), webUrl);
27677
+ if (started === void 0) {
27678
+ throw new RemoteError(`${MALFORMED_MESSAGE}${detail(site, response.status)}`);
27679
+ }
27680
+ return started;
27681
+ }
27682
+ function checkStart(body, webUrl) {
27683
+ const loginKey = body?.["loginKey"];
27684
+ const code2 = body?.["code"];
27685
+ const url2 = body?.["url"];
27686
+ if (typeof loginKey !== "string" || !CLI_LOGIN_KEY_RE.test(loginKey)) return void 0;
27687
+ if (typeof code2 !== "string" || typeof url2 !== "string") return void 0;
27688
+ try {
27689
+ if (parseCliLoginCode(code2) !== code2) return void 0;
27690
+ } catch {
27691
+ return void 0;
27692
+ }
27693
+ let origin;
27694
+ try {
27695
+ const web = new URL(webUrl);
27696
+ const secure = web.protocol === "https:" || web.protocol === "http:" && isLoopbackUrl(web.origin);
27697
+ if (!secure || !PLAIN_HOST_RE.test(web.host)) return void 0;
27698
+ origin = web.origin;
27699
+ } catch {
27700
+ return void 0;
27701
+ }
27702
+ if (url2 !== cliLoginUrl(origin, code2)) return void 0;
27703
+ const raw = body?.["interval"];
27704
+ const interval = typeof raw === "number" && Number.isInteger(raw) ? Math.min(MAX_INTERVAL_SECONDS, Math.max(MIN_INTERVAL_SECONDS, raw)) : CLI_LOGIN_POLL_SECONDS;
27705
+ return { loginKey, code: code2, url: url2, interval };
27706
+ }
27707
+ async function waitForApproval(endpoint, started, deadline, http, io) {
27708
+ const clock = clockOf2(io);
27709
+ const sleep = sleepOf(io);
27710
+ for (; ; ) {
27711
+ await sleep(started.interval * 1e3);
27712
+ if (clock().getTime() > deadline) throw new ConfigError(EXPIRED_MESSAGE);
27713
+ const answer2 = await poll(endpoint, started.loginKey, http);
27714
+ if (answer2 === void 0) continue;
27715
+ const status = answer2["status"];
27716
+ switch (status) {
27717
+ case "pending":
27718
+ continue;
27719
+ case "denied":
27720
+ throw new ConfigError(DENIED_MESSAGE);
27721
+ case "expired":
27722
+ throw new ConfigError(EXPIRED_MESSAGE);
27723
+ case "approved": {
27724
+ const username = answer2["username"];
27725
+ const secret = answer2["secret"];
27726
+ if (typeof username !== "string" || username === "") {
27727
+ throw new RemoteError(MALFORMED_MESSAGE);
27728
+ }
27729
+ if (typeof secret !== "string" || secret === "") {
27730
+ throw new RemoteError(MALFORMED_MESSAGE);
27731
+ }
27732
+ return { username, secret };
27733
+ }
27734
+ default:
27735
+ throw new RemoteError(UNKNOWN_STATUS_MESSAGE);
27736
+ }
27737
+ }
27738
+ }
27739
+ async function poll(endpoint, loginKey, http) {
27740
+ const url2 = `${trimSlash2(endpoint)}${CLI_LOGIN_POLL_PATH}`;
27741
+ const site = { method: "POST", url: url2, debug: http.debug };
27742
+ let response;
27743
+ let text;
27744
+ try {
27745
+ response = await postJson(url2, { loginKey }, http);
27746
+ text = await readText(response, site);
27747
+ } catch (error62) {
27748
+ if (error62 instanceof RemoteError && error62.unanswered) return void 0;
27749
+ throw error62;
27750
+ }
27751
+ if (!response.ok) {
27752
+ if (response.status >= 500 || response.status === 429) return void 0;
27753
+ throw remoteFailure(site, response, text);
27754
+ }
27755
+ return asRecord(parseJson(text, site, response.status)) ?? {};
27756
+ }
27757
+ async function browserSignIn(identity, config2, approval, io) {
27758
+ const clientId = config2.cliClientId;
27759
+ try {
27760
+ let result = await initiateAuth(identity, {
27761
+ clientId,
27762
+ flow: "CUSTOM_AUTH",
27763
+ username: approval.username
27764
+ });
27765
+ if (result.kind === "challenge") {
27766
+ const name = result.challenge.challengeName;
27767
+ if (name !== CUSTOM_CHALLENGE) throw unsupportedStep(name);
27768
+ result = await respondToCustomChallenge(identity, {
27769
+ clientId,
27770
+ challenge: result.challenge,
27771
+ username: approval.username,
27772
+ answer: approval.secret
27773
+ });
27774
+ }
27775
+ if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27776
+ result = await answerCode(
27777
+ identity,
27778
+ clientId,
27779
+ result.challenge,
27780
+ approval.username,
27781
+ io
27782
+ );
27783
+ }
27784
+ if (result.kind === "challenge") {
27785
+ throw unsupportedStep(result.challenge.challengeName);
27786
+ }
27787
+ return result.tokens;
27788
+ } catch (error62) {
27789
+ throw pageError(error62, config2.webUrl);
27790
+ }
27791
+ }
27792
+ async function answerCode(identity, clientId, challenge, username, io) {
27793
+ const terminal = terminalOf(io);
27794
+ try {
27795
+ if (!terminal.isTTY) throw new ConfigError(MFA_NEEDS_TERMINAL_MESSAGE);
27796
+ const code2 = await answer(terminal, "Code:");
27797
+ return await respondToAuthChallenge(identity, {
27798
+ clientId,
27799
+ challenge,
27800
+ email: username,
27801
+ code: code2
27802
+ });
27803
+ } finally {
27804
+ terminal.close();
27805
+ }
27806
+ }
27807
+ async function finishLogin(tokens, endpoint, config2, options, context, io) {
27075
27808
  if (tokens.refreshToken === void 0) {
27076
27809
  throw new RemoteError("sign-in didn't return a refresh token");
27077
27810
  }
27078
- const now = /* @__PURE__ */ new Date();
27811
+ const now = clockOf2(io)();
27079
27812
  const me = await readProfile(
27080
27813
  new ApiClient({
27081
27814
  endpoint,
27082
27815
  authorizer: bearerAuthorizer(tokens.accessToken),
27083
- ...http
27816
+ debug: context.debug,
27817
+ fetch: io.fetch
27084
27818
  }),
27085
27819
  config2.webUrl
27086
27820
  );
@@ -27097,25 +27831,36 @@ async function runLogin(options, context, io) {
27097
27831
  sub: me.sub,
27098
27832
  email: me.email
27099
27833
  });
27834
+ const saved = saveEndpoint(context, endpoint, io.env);
27100
27835
  if (options.json === true) {
27101
- writeJson(io.stdout, { ...me.body, endpoint });
27836
+ writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
27102
27837
  return 0;
27103
27838
  }
27104
27839
  writeLine(io.stdout, `Logged in to ${endpoint} as ${me.email}.`);
27840
+ if (saved !== void 0) {
27841
+ writeLine(io.stdout, `Saved ${endpoint} as your endpoint in ${saved}.`);
27842
+ }
27105
27843
  return 0;
27106
27844
  }
27845
+ function saveEndpoint(context, endpoint, env) {
27846
+ if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
27847
+ return void 0;
27848
+ }
27849
+ return rememberEndpoint(env, endpoint);
27850
+ }
27107
27851
  async function readProfile(client, webUrl) {
27108
27852
  try {
27109
27853
  return await client.me();
27110
27854
  } catch (error62) {
27111
27855
  if (error62 instanceof RemoteError && NO_PROFILE_STATUSES.has(error62.status ?? 0)) {
27112
- throw new ConfigError(`finish your account at ${trimSlash(webUrl)}${LOGIN_PAGE}`);
27856
+ throw new ConfigError(`finish your account at ${trimSlash2(webUrl)}${LOGIN_PAGE}`);
27113
27857
  }
27114
27858
  throw error62;
27115
27859
  }
27116
27860
  }
27117
27861
  async function runLogout(context, io) {
27118
- const endpoint = requireEndpoint(context);
27862
+ requireServer("logout", context);
27863
+ const endpoint = await resolveEndpoint(context, io);
27119
27864
  const credential = findCredential(io.env, endpoint);
27120
27865
  if (credential === void 0) {
27121
27866
  writeLine(io.stdout, `Not logged in to ${endpoint}.`);
@@ -27151,8 +27896,8 @@ async function runWhoami(options, context, io) {
27151
27896
  writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
27152
27897
  return 0;
27153
27898
  }
27899
+ const client = await openClient(context, io);
27154
27900
  const endpoint = requireEndpoint(context);
27155
- const client = openClient(context, io);
27156
27901
  const me = await client.me();
27157
27902
  const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
27158
27903
  if (options.json === true) {
@@ -27169,7 +27914,7 @@ async function runWhoami(options, context, io) {
27169
27914
  debug: context.debug,
27170
27915
  fetch: io.fetch
27171
27916
  });
27172
- const webUrl = config2.mode === "cognito" ? trimSlash(config2.webUrl) : endpoint;
27917
+ const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
27173
27918
  lines.push("", `Create one at ${webUrl}${ORGS_PAGE2}.`);
27174
27919
  }
27175
27920
  for (const line of lines) writeLine(io.stdout, line);
@@ -27209,18 +27954,40 @@ function whoamiLines(endpoint, me, org) {
27209
27954
  return lines;
27210
27955
  }
27211
27956
 
27957
+ // src/commands/project.ts
27958
+ var DELETE_NEEDS_YES = "project delete needs --yes when stdin is not a terminal";
27959
+ var DELETE_MISMATCH = "the name did not match: nothing was deleted";
27960
+ async function confirm(org, project, io) {
27961
+ const terminal = io.terminal ?? nodeTerminal(io.stdin, io.stderr);
27962
+ if (!terminal.isTTY) throw new UsageError(DELETE_NEEDS_YES);
27963
+ let answer2;
27964
+ try {
27965
+ answer2 = await terminal.prompt(`Type the project name to delete ${org}/${project}:`);
27966
+ } finally {
27967
+ terminal.close();
27968
+ }
27969
+ if (foldProjectName(answer2.trim()) !== project) throw new UsageError(DELETE_MISMATCH);
27970
+ }
27971
+ async function runProjectDelete(name, options, context, io) {
27972
+ requireServer("project delete", context);
27973
+ const project = parseProjectName(name);
27974
+ const org = requireOrg(context);
27975
+ if (options.yes !== true) await confirm(org, project, io);
27976
+ await (await openOrgClient(context, io)).deleteProject(project);
27977
+ if (options.json === true) {
27978
+ writeJson(io.stdout, { org, project, deleted: true });
27979
+ return 0;
27980
+ }
27981
+ writeLine(io.stdout, `Deleted project ${org}/${project}.`);
27982
+ return 0;
27983
+ }
27984
+
27212
27985
  // src/commands/promote.ts
27213
- var PROMOTE_NEEDS_SERVER = "promote needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
27214
- var PROMOTE_NEEDS_SERVER_LOCAL = "promote needs a server: drop --local";
27215
27986
  async function runPromote(tag, options, context, io) {
27216
- if (context.mode !== "remote") {
27217
- throw new ConfigError(
27218
- context.endpoint === void 0 ? PROMOTE_NEEDS_SERVER : PROMOTE_NEEDS_SERVER_LOCAL
27219
- );
27220
- }
27987
+ requireServer("promote", context);
27221
27988
  const project = requireProject(context);
27222
27989
  const promotion = parseTagPromotion({ tag });
27223
- const result = await openOrgClient(context, io).promoteTag(project, promotion.tag);
27990
+ const result = await (await openOrgClient(context, io)).promoteTag(project, promotion.tag);
27224
27991
  if (options.json === true) {
27225
27992
  writeJson(io.stdout, {
27226
27993
  project,
@@ -27245,7 +28012,7 @@ var NO_TAG_MESSAGE = "retag needs at least one --tag";
27245
28012
  async function runRetag(value, options, context, io) {
27246
28013
  const tags = options.tag ?? [];
27247
28014
  if (tags.length === 0) throw new UsageError(NO_TAG_MESSAGE);
27248
- const { store, client, local } = openBackend(context, io);
28015
+ const { store, client, local } = await openBackend(context, io);
27249
28016
  const [id] = await resolveIds([value], { store, local, context });
27250
28017
  if (id === void 0) throw new NotFoundError(`no complaint with id ${value}`);
27251
28018
  const allowedTags = client === void 0 ? await localAllowedTags(store, id, context) : void 0;
@@ -27274,9 +28041,9 @@ async function localAllowedTags(store, id, context) {
27274
28041
  // src/commands/retry.ts
27275
28042
  async function runRetry(options, context, io) {
27276
28043
  requireServer("retry", context);
28044
+ const client = await openClient(context, io, { queue: false });
27277
28045
  const endpoint = requireEndpoint(context);
27278
28046
  const path = queuePath(io.env);
27279
- const client = openClient(context, io, { queue: false });
27280
28047
  const result = await flushQueue({
27281
28048
  path,
27282
28049
  endpoint,
@@ -27348,7 +28115,7 @@ function showLines(complaint, groupCount, remote) {
27348
28115
  return [...lines, "", printableLines(complaint.message)];
27349
28116
  }
27350
28117
  async function runShow(value, options, context, io) {
27351
- const { store, local } = openBackend(context, io);
28118
+ const { store, local } = await openBackend(context, io);
27352
28119
  const [id] = await resolveIds([value], { store, local, context });
27353
28120
  const complaint = id === void 0 ? void 0 : await getComplaint(store, id);
27354
28121
  if (id === void 0 || complaint === void 0) {
@@ -27403,7 +28170,7 @@ async function runStatusChange(status, values, options, context, io) {
27403
28170
  };
27404
28171
  parseStatusChange(draft);
27405
28172
  const given = await readIdArguments(values, io);
27406
- const backend = openBackend(context, io);
28173
+ const backend = await openBackend(context, io);
27407
28174
  const extra = options.duplicateOf === void 0 ? [] : [options.duplicateOf];
27408
28175
  const resolved = await resolveIds([...given, ...extra], {
27409
28176
  store: backend.store,
@@ -27442,10 +28209,10 @@ async function runStatusChange(status, values, options, context, io) {
27442
28209
  writeLine(io.stdout, successLine(status, result.complaint));
27443
28210
  continue;
27444
28211
  }
27445
- const code = result.error?.code ?? NOT_FOUND;
27446
- const message = code === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
28212
+ const code2 = result.error?.code ?? NOT_FOUND;
28213
+ const message = code2 === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
27447
28214
  writeLine(io.stderr, `quibble: ${result.id}: ${message ?? "the change failed"}`);
27448
- failures.push({ remote: backend.store instanceof RemoteStore && code !== NOT_FOUND });
28215
+ failures.push({ remote: backend.store instanceof RemoteStore && code2 !== NOT_FOUND });
27449
28216
  }
27450
28217
  if (options.json === true) writeJson(io.stdout, results);
27451
28218
  if (error62 !== void 0) throw error62;
@@ -27499,7 +28266,7 @@ async function runSummary(options, context, io) {
27499
28266
  throw new UsageError("--json and --format cannot be used together");
27500
28267
  }
27501
28268
  const format = readFormat(options.format);
27502
- const { store, client } = openBackend(context, io);
28269
+ const { store, client } = await openBackend(context, io);
27503
28270
  const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
27504
28271
  const filter = parseListFilter({
27505
28272
  ...resolveScope(options, context),
@@ -27609,11 +28376,127 @@ function markdownLines(filter, summary, note) {
27609
28376
  return lines;
27610
28377
  }
27611
28378
 
28379
+ // src/commands/sync.ts
28380
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
28381
+ import { join as join8 } from "node:path";
28382
+ import { Writable as Writable2 } from "node:stream";
28383
+ var SYNC_TIMEOUT_MS = 3e3;
28384
+ var SECTION_FILES = ["CLAUDE.md", "AGENTS.md"];
28385
+ function readOptional3(path) {
28386
+ try {
28387
+ return readFileSync6(path, "utf8");
28388
+ } catch (error62) {
28389
+ if (isNotFound(error62)) return void 0;
28390
+ throw error62;
28391
+ }
28392
+ }
28393
+ function holders(root) {
28394
+ const found = [];
28395
+ for (const name of SECTION_FILES) {
28396
+ const path = join8(root, name);
28397
+ const text = readOptional3(path);
28398
+ if (text !== void 0 && findSection(text) !== void 0) {
28399
+ found.push({ path, text, skill: false });
28400
+ }
28401
+ }
28402
+ const skill = join8(root, SKILL_PATH);
28403
+ if (existsSync3(skill)) {
28404
+ found.push({ path: skill, text: readOptional3(skill) ?? "", skill: true });
28405
+ }
28406
+ return found;
28407
+ }
28408
+ function agentOf(lines, path) {
28409
+ const named = agentOfText(lines);
28410
+ if (named !== void 0) {
28411
+ try {
28412
+ return parseAgentName(named);
28413
+ } catch {
28414
+ }
28415
+ }
28416
+ return agentForFile(path);
28417
+ }
28418
+ function rewrite(holder, tags, scope) {
28419
+ if (holder.skill) {
28420
+ const agent2 = agentOf(holder.text.split(/\r?\n/), holder.path);
28421
+ return withLineBreak(skillFile(agentSnippet(tags, { ...scope, agent: agent2 })), holder.text);
28422
+ }
28423
+ const section = findSection(holder.text);
28424
+ if (section === void 0) return holder.text;
28425
+ const agent = agentOf(sectionText(section), holder.path);
28426
+ return replaceSection(section, agentSnippet(tags, { ...scope, agent }));
28427
+ }
28428
+ function sink() {
28429
+ return new Writable2({
28430
+ write(_chunk, _encoding, done) {
28431
+ done();
28432
+ }
28433
+ });
28434
+ }
28435
+ async function runSync(options, contextOf, io) {
28436
+ if (options.quiet !== true) return sync(options, contextOf(), io);
28437
+ const silent = { ...io, stdout: sink(), stderr: sink() };
28438
+ try {
28439
+ const context = contextOf();
28440
+ context.timeoutMs = SYNC_TIMEOUT_MS;
28441
+ const result = await syncFiles({ hook: options.hook }, context, silent);
28442
+ if (result.files.some((file2) => file2.updated)) {
28443
+ writeLine(io.stdout, refreshedLine(result.project, result.tags));
28444
+ }
28445
+ } catch {
28446
+ }
28447
+ return 0;
28448
+ }
28449
+ async function sync(options, context, io) {
28450
+ const result = await syncFiles(options, context, io);
28451
+ if (options.json === true) {
28452
+ writeJson(io.stdout, {
28453
+ project: result.project,
28454
+ tags: result.tags ?? null,
28455
+ files: result.files,
28456
+ hook: result.hook ?? null
28457
+ });
28458
+ return 0;
28459
+ }
28460
+ for (const file2 of result.files) {
28461
+ writeLine(
28462
+ io.stdout,
28463
+ file2.updated ? `Updated ${file2.path}` : `${file2.path} is up to date`
28464
+ );
28465
+ }
28466
+ return 0;
28467
+ }
28468
+ async function syncFiles(options, context, io) {
28469
+ const project = requireProject(context);
28470
+ const root = context.projectRoot ?? io.cwd;
28471
+ let hook;
28472
+ if (options.hook === true) {
28473
+ const plan = planHook(root);
28474
+ const line = applyHook(plan);
28475
+ hook = plan.path;
28476
+ if (options.json !== true) writeLine(io.stdout, line);
28477
+ }
28478
+ const found = holders(root);
28479
+ if (found.length === 0) {
28480
+ throw new UsageError(
28481
+ `no Quibble snippet under ${root}: run quibble snippet --append CLAUDE.md`
28482
+ );
28483
+ }
28484
+ const { tags, scope } = await snippetSource(project, context, io, { queue: false });
28485
+ const files = [];
28486
+ for (const holder of found) {
28487
+ const next = rewrite(holder, tags, scope);
28488
+ const updated = next !== holder.text;
28489
+ if (updated) writeFileSync6(holder.path, next, "utf8");
28490
+ files.push({ path: holder.path, updated });
28491
+ }
28492
+ return { project, tags, files, hook };
28493
+ }
28494
+
27612
28495
  // src/commands/tags.ts
27613
28496
  async function runTags(options, context, io) {
27614
28497
  const project = requireProject(context);
27615
28498
  if (context.mode !== "remote") checkActAs(context);
27616
- const tags = context.mode === "remote" ? (await openOrgClient(context, io).project(project)).tags : context.allowedTags;
28499
+ const tags = context.mode === "remote" ? (await (await openOrgClient(context, io)).project(project)).tags : context.allowedTags;
27617
28500
  if (options.json === true) {
27618
28501
  writeJson(io.stdout, { project, tags: tags ?? null });
27619
28502
  return 0;
@@ -27634,7 +28517,7 @@ async function runTagsEdit(edit, given, options, context, io) {
27634
28517
  const project = requireProject(context);
27635
28518
  if (given.length === 0) throw new UsageError(`tags ${edit} needs at least one tag`);
27636
28519
  const named = parseAllowedTags(given);
27637
- const client = openOrgClient(context, io);
28520
+ const client = await openOrgClient(context, io);
27638
28521
  let next;
27639
28522
  if (edit === "set") {
27640
28523
  next = named;
@@ -27695,7 +28578,7 @@ async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */
27695
28578
  { name, ...expiresAt === void 0 ? {} : { expiresAt } },
27696
28579
  now
27697
28580
  );
27698
- const client = openOrgClient(context, io);
28581
+ const client = await openOrgClient(context, io);
27699
28582
  const minted = await client.createToken(project, body);
27700
28583
  if (options.json === true) {
27701
28584
  writeJson(io.stdout, minted.body);
@@ -27705,13 +28588,13 @@ async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */
27705
28588
  io.stdout,
27706
28589
  `Created token ${minted.token.name} for ${org}/${project}. It is shown once:`
27707
28590
  );
27708
- writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash(client.endpoint)}`);
28591
+ writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash2(client.endpoint)}`);
27709
28592
  writeLine(io.stdout, `QUIBBLE_TOKEN=${minted.plaintext}`);
27710
28593
  return 0;
27711
28594
  }
27712
28595
  async function runTokenList(options, context, io, now = /* @__PURE__ */ new Date()) {
27713
28596
  const { project } = scopeOf(context);
27714
- const answer2 = await openOrgClient(context, io).tokens(project);
28597
+ const answer2 = await (await openOrgClient(context, io)).tokens(project);
27715
28598
  if (options.json === true) {
27716
28599
  writeJson(io.stdout, answer2.body);
27717
28600
  return 0;
@@ -27750,7 +28633,7 @@ function findToken(tokens, value) {
27750
28633
  }
27751
28634
  async function runTokenRevoke(value, options, context, io) {
27752
28635
  const { project, org } = scopeOf(context);
27753
- const client = openOrgClient(context, io);
28636
+ const client = await openOrgClient(context, io);
27754
28637
  const token = findToken((await client.tokens(project)).tokens, value);
27755
28638
  await client.revokeToken(project, token.id);
27756
28639
  if (options.json === true) {
@@ -27785,6 +28668,7 @@ var TAGS_EDITS = [
27785
28668
  var ID_HELP = `complaint id, or a unique prefix of at least ${SHORT_ID_MIN} characters`;
27786
28669
  var REASON_HELP = `${REASONS.slice(0, -1).join(", ")} or ${REASONS.at(-1) ?? ""}`;
27787
28670
  function packageVersion() {
28671
+ if (typeof QUIBBLE_BUILD_VERSION === "string") return QUIBBLE_BUILD_VERSION;
27788
28672
  const require2 = createRequire(import.meta.url);
27789
28673
  const manifest = require2("../package.json");
27790
28674
  return typeof manifest.version === "string" ? manifest.version : "0.0.0";
@@ -27798,7 +28682,7 @@ function rejectEmptyOptions(options) {
27798
28682
  }
27799
28683
  }
27800
28684
  function withGlobalOptions(command) {
27801
- return command.option("--local", "use the local store even if an endpoint is set").option("--endpoint <url>", "base URL of the Quibble API").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
28685
+ return command.option("--local", "use the local store, whatever the endpoint says").option("--endpoint <url>", "API URL, web URL, host, or local").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
27802
28686
  }
27803
28687
  function globals(program2, command) {
27804
28688
  const outer = program2.opts();
@@ -27836,19 +28720,34 @@ function buildProgram(io, setCode) {
27836
28720
  }).exitOverride();
27837
28721
  withGlobalOptions(program2);
27838
28722
  withGlobalOptions(
27839
- program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name (default: the directory name)").option("-t, --tag <tag>", "allowed tag (repeatable)", collect).option("--agent <name>", "agent named in the snippet (default: claude)").option("--skill", "also write .claude/skills/quibble/SKILL.md").option("--force", "overwrite an existing .quibblerc or skill file")
28723
+ program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name (default: the directory name)").option("-t, --tag <tag>", "allowed tag (repeatable)", collect).option("--agent <name>", "agent named in the snippet (default: claude)").option("--skill", "also write .claude/skills/quibble/SKILL.md").option("--hook", "also add the quibble sync hook to .claude/settings.json").option("--force", "overwrite an existing .quibblerc or skill file")
27840
28724
  ).action(async function() {
27841
28725
  const options = { ...this.opts(), ...globals(program2, this) };
27842
28726
  rejectEmptyOptions(options);
27843
28727
  setCode(await runInit(options, io));
27844
28728
  });
27845
28729
  withGlobalOptions(
27846
- program2.command("snippet").description("print or write the agent snippet of this project").option("--append <file>", "append the snippet to a file such as CLAUDE.md").option("--skill", "write .claude/skills/quibble/SKILL.md").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
28730
+ program2.command("snippet").description("print or write the agent snippet of this project").option("--append <file>", "append the snippet to a file such as CLAUDE.md").option("--skill", "write .claude/skills/quibble/SKILL.md").option("--hook", "add the quibble sync hook to .claude/settings.json").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
27847
28731
  ).action(async function() {
27848
28732
  const options = { ...this.opts(), ...globals(program2, this) };
27849
28733
  rejectEmptyOptions(options);
27850
28734
  setCode(await runSnippet(options, contextFor(options, io), io));
27851
28735
  });
28736
+ withGlobalOptions(
28737
+ program2.command("sync").description("rewrite the agent snippet in place with the current tags").option("--hook", "also add the quibble sync hook to .claude/settings.json").option("--quiet", "print nothing but a changed tag list, and always exit 0").option("--project <name>", "project name (overrides .quibblerc)")
28738
+ ).action(async function() {
28739
+ const options = { ...this.opts(), ...globals(program2, this) };
28740
+ setCode(
28741
+ await runSync(
28742
+ options,
28743
+ () => {
28744
+ rejectEmptyOptions(options);
28745
+ return contextFor(options, io);
28746
+ },
28747
+ io
28748
+ )
28749
+ );
28750
+ });
27852
28751
  withGlobalOptions(
27853
28752
  program2.command("complain").description("file a complaint").argument("[message]", "what went wrong; omit it or pass `-` to read stdin").option("-s, --severity <level>", "low, medium or high").option("-t, --tag <tag>", "tag (repeatable)", collect).option("--agent <name>", "name of the agent filing it").option("--reporter <name>", "person you are filing for (local mode only)").option("--project <name>", "project name (overrides .quibblerc)").option("--cmd <command>", "the command that failed")
27854
28753
  ).action(async function(message) {
@@ -27944,6 +28843,14 @@ function buildProgram(io, setCode) {
27944
28843
  rejectEmptyOptions(options);
27945
28844
  setCode(await runTokenRevoke(value, options, contextFor(options, io), io));
27946
28845
  });
28846
+ const projectCommand = program2.command("project").description("manage the projects of the organization (remote only)");
28847
+ withGlobalOptions(
28848
+ projectCommand.command("delete").description("delete a project and hide its complaints").argument("<name>", "project name").option("--yes", "skip the question")
28849
+ ).action(async function(name) {
28850
+ const options = { ...this.opts(), ...globals(program2, this) };
28851
+ rejectEmptyOptions(options);
28852
+ setCode(await runProjectDelete(name, options, contextFor(options, io), io));
28853
+ });
27947
28854
  withGlobalOptions(
27948
28855
  program2.command("retry").description("send the complaints queued while offline")
27949
28856
  ).action(async function() {
@@ -27966,11 +28873,17 @@ function buildProgram(io, setCode) {
27966
28873
  setCode(await runOrgs(options, contextFor(options, io), io));
27967
28874
  });
27968
28875
  withGlobalOptions(
27969
- program2.command("login").description("log in to the endpoint")
28876
+ program2.command("login").description("log in to the endpoint, approved in a browser").option("--no-browser", "print the approval URL, and open no browser").option("--password", "log in with an email and a password on the terminal")
27970
28877
  ).action(async function() {
27971
28878
  const options = { ...this.opts(), ...globals(program2, this) };
27972
28879
  rejectEmptyOptions(options);
27973
- setCode(await runLogin(options, contextFor(options, io), io));
28880
+ setCode(
28881
+ await runLogin(
28882
+ { ...options, version: packageVersion() },
28883
+ contextFor(options, io),
28884
+ io
28885
+ )
28886
+ );
27974
28887
  });
27975
28888
  withGlobalOptions(
27976
28889
  program2.command("logout").description("log out of the endpoint")
@@ -27988,10 +28901,10 @@ function buildProgram(io, setCode) {
27988
28901
  });
27989
28902
  withGlobalOptions(
27990
28903
  program2.command("config").description("print the resolved configuration")
27991
- ).action(function() {
28904
+ ).action(async function() {
27992
28905
  const options = { ...this.opts(), ...globals(program2, this) };
27993
28906
  rejectEmptyOptions(options);
27994
- setCode(runConfig(options, contextFor(options, io), io));
28907
+ setCode(await runConfig(options, contextFor(options, io), io));
27995
28908
  });
27996
28909
  return program2;
27997
28910
  }
@@ -27999,9 +28912,10 @@ function stripErrorPrefix(message) {
27999
28912
  return message.replace(/^error:\s*/i, "");
28000
28913
  }
28001
28914
  async function main(argv, io) {
28002
- let code = 0;
28915
+ resetDiscovery();
28916
+ let code2 = 0;
28003
28917
  const program2 = buildProgram(io, (value) => {
28004
- code = value;
28918
+ code2 = value;
28005
28919
  });
28006
28920
  if (argv.length === 0) {
28007
28921
  io.stdout.write(program2.helpInformation());
@@ -28009,7 +28923,7 @@ async function main(argv, io) {
28009
28923
  }
28010
28924
  try {
28011
28925
  await program2.parseAsync(argv, { from: "user" });
28012
- return code;
28926
+ return code2;
28013
28927
  } catch (error62) {
28014
28928
  if (error62 instanceof CommanderError) {
28015
28929
  if (error62.code === "commander.help" || error62.code === "commander.helpDisplayed") {
@@ -28031,10 +28945,18 @@ for (const stream of [process2.stdout, process2.stderr]) {
28031
28945
  throw error62;
28032
28946
  });
28033
28947
  }
28034
- process2.exitCode = await main(process2.argv.slice(2), {
28948
+ function flushed(stream) {
28949
+ return new Promise((resolve4) => {
28950
+ stream.write("", () => resolve4());
28951
+ });
28952
+ }
28953
+ var code = await main(process2.argv.slice(2), {
28035
28954
  stdin: process2.stdin,
28036
28955
  stdout: process2.stdout,
28037
28956
  stderr: process2.stderr,
28038
28957
  env: process2.env,
28039
28958
  cwd: process2.cwd()
28040
28959
  });
28960
+ process2.exitCode = code;
28961
+ await Promise.all([flushed(process2.stdout), flushed(process2.stderr)]);
28962
+ process2.exit(code);