letthemquibble 1.2.0 → 2.1.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 +24 -6
  2. package/dist/quibble.js +1404 -728
  3. package/package.json +3 -7
package/dist/quibble.js CHANGED
@@ -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;
@@ -23233,6 +23233,14 @@ 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 {
@@ -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) {
@@ -23461,6 +23498,43 @@ function freeText(max, typeMessage) {
23461
23498
  const message = lengthMessage(1, max);
23462
23499
  return external_exports.string({ message: typeMessage ?? message }).overwrite((value) => value.replace(/\r\n?/g, "\n")).trim().min(1, { message }).max(max, { message }).refine((value) => !CONTROL_RE.test(value), { message: CONTROL_MESSAGE });
23463
23500
  }
23501
+ var SECRET_PATTERNS = [
23502
+ {
23503
+ kind: "an AWS access key",
23504
+ pattern: /(?<![A-Za-z0-9])(?:AKIA|ASIA)[0-9A-Z]{16}(?![A-Za-z0-9])/
23505
+ },
23506
+ {
23507
+ kind: "a GitHub token",
23508
+ pattern: /(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])|(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{22,}/
23509
+ },
23510
+ {
23511
+ kind: "an API key",
23512
+ pattern: /(?<![A-Za-z0-9-])sk-(?:ant-|proj-)?(?=[A-Za-z_-]*[0-9])[A-Za-z0-9_-]{32,}/
23513
+ },
23514
+ { kind: "a Stripe live key", pattern: /(?<![A-Za-z0-9])[rs]k_live_[A-Za-z0-9]{20,}/ },
23515
+ { kind: "a Slack token", pattern: /(?<![A-Za-z0-9])xox[abposr]-[A-Za-z0-9-]{10,}/ },
23516
+ {
23517
+ kind: "a Quibble project token",
23518
+ pattern: /(?<![A-Za-z0-9])qbp_[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/
23519
+ },
23520
+ { kind: "a private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ }
23521
+ ];
23522
+ function findSecret(text) {
23523
+ return SECRET_PATTERNS.find(({ pattern }) => pattern.test(text))?.kind;
23524
+ }
23525
+ function secretFree(schema, max) {
23526
+ return schema.superRefine((value, ctx) => {
23527
+ if (value.length > max || CONTROL_RE.test(value))
23528
+ return;
23529
+ const kind = findSecret(value);
23530
+ if (kind !== void 0) {
23531
+ ctx.addIssue({
23532
+ code: "custom",
23533
+ message: `holds what looks like ${kind}: remove the secret and file again`
23534
+ });
23535
+ }
23536
+ });
23537
+ }
23464
23538
  var SEVERITY_MESSAGE = `must be one of ${SEVERITIES.join(", ")}`;
23465
23539
  var STATUS_MESSAGE = `must be one of ${STATUSES.join(", ")}`;
23466
23540
  var LIMIT_MESSAGE = `must be an integer between 1 and ${MAX_LIMIT}`;
@@ -23613,7 +23687,7 @@ var agentSchema = freeText(AGENT_MAX);
23613
23687
  var newComplaintShape = {
23614
23688
  org: orgSlugSchema,
23615
23689
  project: projectNameSchema,
23616
- message: freeText(MESSAGE_MAX),
23690
+ message: secretFree(freeText(MESSAGE_MAX), MESSAGE_MAX),
23617
23691
  severity: severitySchema.optional(),
23618
23692
  tags: tagsSchema.optional(),
23619
23693
  agent: agentSchema.optional(),
@@ -23621,7 +23695,7 @@ var newComplaintShape = {
23621
23695
  reporterName: freeText(REPORTER_MAX).optional(),
23622
23696
  cwd: cwdSchema.optional(),
23623
23697
  // Free text: it is trimmed and may hold newlines, and nothing parses it.
23624
- cmd: freeText(CMD_MAX).optional()
23698
+ cmd: secretFree(freeText(CMD_MAX), CMD_MAX).optional()
23625
23699
  };
23626
23700
  var newComplaintObject = external_exports.strictObject(newComplaintShape);
23627
23701
  function withDefaults(input2) {
@@ -23906,6 +23980,23 @@ function newTokenSchema(now) {
23906
23980
  ...input2.expiresAt === void 0 ? {} : { expiresAt: input2.expiresAt }
23907
23981
  }));
23908
23982
  }
23983
+ function reportedField(max) {
23984
+ const message = lengthMessage(1, max);
23985
+ return external_exports.string({ message }).trim().min(1, { message }).max(max, { message }).refine((value) => !CLI_LOGIN_CONTROL_RE.test(value), { message: CONTROL_MESSAGE });
23986
+ }
23987
+ var newCliLoginSchema = external_exports.strictObject({
23988
+ host: reportedField(CLI_LOGIN_HOST_MAX),
23989
+ os: reportedField(CLI_LOGIN_OS_MAX),
23990
+ version: reportedField(CLI_LOGIN_VERSION_MAX)
23991
+ }).transform((input2) => ({
23992
+ host: input2.host,
23993
+ os: input2.os,
23994
+ version: input2.version
23995
+ }));
23996
+ var LOGIN_KEY_MESSAGE = "must be 43 base64url characters";
23997
+ var cliLoginPollSchema = external_exports.strictObject({
23998
+ loginKey: external_exports.string({ message: LOGIN_KEY_MESSAGE }).regex(CLI_LOGIN_KEY_RE, { message: LOGIN_KEY_MESSAGE })
23999
+ }).transform((input2) => ({ loginKey: input2.loginKey }));
23909
24000
  var UNKNOWN_KEY_MESSAGE = "unknown key";
23910
24001
  var emptyBodySchema = external_exports.strictObject({});
23911
24002
  function formatIssues(error62) {
@@ -24756,6 +24847,7 @@ var TOO_MANY_LINE = "Too many attempts. Try again later.";
24756
24847
  var WRONG_CODE_LINE = "Wrong code.";
24757
24848
  var EXPIRED_CODE_LINE = "That code has expired. Request a new one.";
24758
24849
  var PASSWORD_POLICY_LINE = "Use at least 12 characters, with an uppercase letter, a lowercase letter, a number and a symbol.";
24850
+ var BROWSER_REFUSED_LINE = "the browser approval could not be used: run quibble login again";
24759
24851
  var UNREACHABLE_TYPE = "Unreachable";
24760
24852
  var INCORRECT_CREDENTIALS_MESSAGE = "Incorrect username or password.";
24761
24853
  var CognitoError = class extends RemoteError {
@@ -24772,14 +24864,15 @@ var CognitoError = class extends RemoteError {
24772
24864
  function endpointUrl(region) {
24773
24865
  return `https://cognito-idp.${region}.amazonaws.com/`;
24774
24866
  }
24775
- function errorLineOf(type, detail2, signIn2) {
24867
+ function errorLineOf(type, detail2, step) {
24776
24868
  switch (type) {
24777
24869
  case UNREACHABLE_TYPE:
24778
24870
  return UNREACHABLE_LINE;
24779
24871
  case "UserNotFoundException":
24780
- return signIn2 ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24872
+ return step === "sign-in" ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24781
24873
  case "NotAuthorizedException":
24782
- return signIn2 && detail2 === INCORRECT_CREDENTIALS_MESSAGE ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24874
+ if (step === "browser") return BROWSER_REFUSED_LINE;
24875
+ return step === "sign-in" && detail2 === INCORRECT_CREDENTIALS_MESSAGE ? WRONG_CREDENTIALS_LINE : detail2 ?? type;
24783
24876
  case "LimitExceededException":
24784
24877
  case "TooManyRequestsException":
24785
24878
  case "TooManyFailedAttemptsException":
@@ -24800,7 +24893,7 @@ function typeOf(value) {
24800
24893
  const hash2 = value.indexOf("#");
24801
24894
  return hash2 === -1 ? value : value.slice(hash2 + 1);
24802
24895
  }
24803
- async function call(endpoint, operation, body, signIn2 = false) {
24896
+ async function call(endpoint, operation, body, step = "other") {
24804
24897
  const url2 = endpointUrl(endpoint.region);
24805
24898
  const send2 = endpoint.fetch ?? globalThis.fetch;
24806
24899
  const site = {
@@ -24812,7 +24905,7 @@ async function call(endpoint, operation, body, signIn2 = false) {
24812
24905
  throw new CognitoError(
24813
24906
  type,
24814
24907
  detail2,
24815
- errorLineOf(type, detail2, signIn2),
24908
+ errorLineOf(type, detail2, step),
24816
24909
  detail(site, status)
24817
24910
  );
24818
24911
  };
@@ -24895,14 +24988,27 @@ function authAnswerOf(body) {
24895
24988
  }
24896
24989
  };
24897
24990
  }
24991
+ function flowOf(input2) {
24992
+ switch (input2.flow) {
24993
+ case "USER_PASSWORD_AUTH":
24994
+ return {
24995
+ parameters: { USERNAME: input2.username, PASSWORD: input2.password },
24996
+ step: "sign-in"
24997
+ };
24998
+ case "REFRESH_TOKEN_AUTH":
24999
+ return { parameters: { REFRESH_TOKEN: input2.refreshToken }, step: "other" };
25000
+ case "CUSTOM_AUTH":
25001
+ return { parameters: { USERNAME: input2.username }, step: "browser" };
25002
+ }
25003
+ }
24898
25004
  async function initiateAuth(endpoint, input2) {
24899
- const parameters = input2.flow === "USER_PASSWORD_AUTH" ? { USERNAME: input2.username, PASSWORD: input2.password } : { REFRESH_TOKEN: input2.refreshToken };
25005
+ const { parameters, step } = flowOf(input2);
24900
25006
  return authAnswerOf(
24901
25007
  await call(
24902
25008
  endpoint,
24903
25009
  "InitiateAuth",
24904
25010
  { ClientId: input2.clientId, AuthFlow: input2.flow, AuthParameters: parameters },
24905
- input2.flow === "USER_PASSWORD_AUTH"
25011
+ step
24906
25012
  )
24907
25013
  );
24908
25014
  }
@@ -24917,6 +25023,22 @@ async function respondToAuthChallenge(endpoint, input2) {
24917
25023
  })
24918
25024
  );
24919
25025
  }
25026
+ async function respondToCustomChallenge(endpoint, input2) {
25027
+ const username = input2.challenge.challengeParameters["USERNAME"] ?? input2.username;
25028
+ return authAnswerOf(
25029
+ await call(
25030
+ endpoint,
25031
+ "RespondToAuthChallenge",
25032
+ {
25033
+ ClientId: input2.clientId,
25034
+ ChallengeName: input2.challenge.challengeName,
25035
+ Session: input2.challenge.session,
25036
+ ChallengeResponses: { USERNAME: username, ANSWER: input2.answer }
25037
+ },
25038
+ "browser"
25039
+ )
25040
+ );
25041
+ }
24920
25042
  async function revokeToken(endpoint, input2) {
24921
25043
  await call(endpoint, "RevokeToken", { ClientId: input2.clientId, Token: input2.token });
24922
25044
  }
@@ -25350,7 +25472,7 @@ function resolveEndpointSource(options, project, user) {
25350
25472
  };
25351
25473
  }
25352
25474
  function resolveContext(options) {
25353
- const project = findProjectConfig(options.cwd);
25475
+ const project = options.readProjectConfig === false ? void 0 : findProjectConfig(options.cwd);
25354
25476
  const user = loadUserConfig(options.env);
25355
25477
  const { mode, endpoint, endpointSource, localFix } = resolveEndpointSource(
25356
25478
  options,
@@ -25591,7 +25713,7 @@ function readComplaint(body) {
25591
25713
  throw new RemoteError(NOT_A_COMPLAINT, { cause: error62 });
25592
25714
  }
25593
25715
  }
25594
- var ApiClient = class {
25716
+ var ApiClient = class _ApiClient {
25595
25717
  endpoint;
25596
25718
  /** The organization of every org-scoped path, when the command resolved one. */
25597
25719
  org;
@@ -25639,6 +25761,19 @@ var ApiClient = class {
25639
25761
  if (this.org === void 0) throw new ConfigError(NO_ORG_MESSAGE);
25640
25762
  return `/v1/orgs/${encodeURIComponent(this.org)}`;
25641
25763
  }
25764
+ /**
25765
+ * The same client for one organization, with the same authorizer, so a
25766
+ * command that learns the organization from `GET /v1/me` reads the auth
25767
+ * config and refreshes a session once. It runs no `beforeFirst`.
25768
+ */
25769
+ forOrg(org) {
25770
+ return new _ApiClient({
25771
+ endpoint: this.endpoint,
25772
+ authorizer: this.#auth,
25773
+ org,
25774
+ ...this.#http
25775
+ });
25776
+ }
25642
25777
  /** Reads the caller's own identity. */
25643
25778
  async me() {
25644
25779
  return parseMe(await this.request("GET", "/v1/me"));
@@ -25666,6 +25801,18 @@ var ApiClient = class {
25666
25801
  async project(name) {
25667
25802
  return readProject(await this.request("GET", this.projectPath(name)), name);
25668
25803
  }
25804
+ /**
25805
+ * Creates a project, or restores a deleted one of that name (section 7.5).
25806
+ * The route needs `admin`, and answers `201` with the record.
25807
+ */
25808
+ async createProject(name, tags) {
25809
+ const body = await this.request(
25810
+ "POST",
25811
+ `${this.orgPath()}/projects`,
25812
+ JSON.stringify({ project: name, ...tags === void 0 ? {} : { tags } })
25813
+ );
25814
+ return readProject(body, name);
25815
+ }
25669
25816
  /**
25670
25817
  * Replaces the allowed tag list of a project, or clears it with `null`. The
25671
25818
  * route retags the stored complaints and answers how many it rewrote.
@@ -26229,7 +26376,9 @@ function projectOf(entry) {
26229
26376
  }
26230
26377
  function headlineOfEntry(entry) {
26231
26378
  const message = entry.body["message"];
26232
- return typeof message === "string" ? printable(summarize(message)) : "?";
26379
+ if (typeof message !== "string") return '"?"';
26380
+ const headline = summarize(message);
26381
+ return findSecret(headline) === void 0 ? `"${printable(headline)}"` : `key ${printable(entry.key)}`;
26233
26382
  }
26234
26383
  function queuedCount(n) {
26235
26384
  return `${n} queued ${n === 1 ? "complaint" : "complaints"}`;
@@ -26300,7 +26449,7 @@ async function flushQueue(options) {
26300
26449
  result.dropped += 1;
26301
26450
  writeLine(
26302
26451
  options.stderr,
26303
- `quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)}, "${headlineOfEntry(entry)}": ${printable(message)}`
26452
+ `quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)}, ${headlineOfEntry(entry)}: ${printable(message)}`
26304
26453
  );
26305
26454
  }
26306
26455
  return result;
@@ -26552,6 +26701,75 @@ async function runConfig(options, context, io) {
26552
26701
  import { existsSync as existsSync2, writeFileSync as writeFileSync5 } from "node:fs";
26553
26702
  import { basename as basename2, join as join7, resolve as resolve3 } from "node:path";
26554
26703
 
26704
+ // src/git.ts
26705
+ import { spawn as nodeSpawn } from "node:child_process";
26706
+ var GIT_TIMEOUT_MS = 5e3;
26707
+ var ORIGIN_ARGS = ["remote", "get-url", "origin"];
26708
+ function nodeGit(options = {}) {
26709
+ const spawn = options.spawn ?? nodeSpawn;
26710
+ const timeoutMs = options.timeoutMs ?? GIT_TIMEOUT_MS;
26711
+ return {
26712
+ originUrl(cwd) {
26713
+ return new Promise((resolve4) => {
26714
+ let settled = false;
26715
+ let child;
26716
+ let output2 = "";
26717
+ const settle2 = (url2) => {
26718
+ if (settled) return;
26719
+ settled = true;
26720
+ clearTimeout(timer);
26721
+ resolve4(url2);
26722
+ };
26723
+ const timer = setTimeout(() => {
26724
+ settle2(void 0);
26725
+ child?.kill();
26726
+ }, timeoutMs);
26727
+ timer.unref();
26728
+ try {
26729
+ child = spawn("git", [...ORIGIN_ARGS], {
26730
+ cwd,
26731
+ shell: false,
26732
+ stdio: ["ignore", "pipe", "ignore"],
26733
+ windowsHide: true
26734
+ });
26735
+ } catch {
26736
+ settle2(void 0);
26737
+ return;
26738
+ }
26739
+ child.stdout?.setEncoding("utf8");
26740
+ child.stdout?.on("data", (chunk) => {
26741
+ output2 += chunk;
26742
+ });
26743
+ child.on("error", () => {
26744
+ settle2(void 0);
26745
+ });
26746
+ child.on("close", (code2) => {
26747
+ const url2 = output2.trim();
26748
+ settle2(code2 === 0 && url2 !== "" ? url2 : void 0);
26749
+ });
26750
+ });
26751
+ }
26752
+ };
26753
+ }
26754
+ function remoteSegment(url2) {
26755
+ let rest = url2.trim();
26756
+ if (rest.endsWith("/")) rest = rest.slice(0, -1);
26757
+ if (rest.endsWith(".git")) rest = rest.slice(0, -".git".length);
26758
+ const slash = rest.lastIndexOf("/");
26759
+ const colon = rest.lastIndexOf(":");
26760
+ const scpLike = !rest.includes("://") && colon > slash;
26761
+ return rest.slice((scpLike ? colon : slash) + 1);
26762
+ }
26763
+ function projectOfRemote(url2) {
26764
+ const segment = remoteSegment(url2);
26765
+ if (segment === "") return void 0;
26766
+ try {
26767
+ return parseProjectName(segment);
26768
+ } catch {
26769
+ return void 0;
26770
+ }
26771
+ }
26772
+
26555
26773
  // src/hook.ts
26556
26774
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
26557
26775
  import { dirname as dirname5, join as join5 } from "node:path";
@@ -26624,11 +26842,13 @@ var DEFAULT_SNIPPET_AGENT = "claude";
26624
26842
  var AGENTS_FILE_AGENT = "codex";
26625
26843
  var SNIPPET_INTRO = `This project uses Quibble. When the build, tests, docs or tooling waste your
26626
26844
  time, file a complaint instead of quietly working around the problem.`;
26845
+ var SNIPPET_SECRETS = "- Never paste secrets, tokens or .env lines into a complaint; Quibble refuses them.";
26627
26846
  var SNIPPET_USAGE = `- Set severity with \`-s low|medium|high\`. Add tags with \`-t <tag>\` (repeatable).
26628
26847
  - Put a one-line summary first; it is the headline everywhere. Add details below.
26629
26848
  - Name the command that failed with \`--cmd "<command>"\`.
26630
26849
  - Pipe a long message on stdin with \`quibble complain -\`.
26631
- - File one complaint per problem, as soon as you hit it, not at the end of the task.`;
26850
+ - File one complaint per problem, as soon as you hit it, not at the end of the task.
26851
+ ${SNIPPET_SECRETS}`;
26632
26852
  var SKILL_FRONT_MATTER = `---
26633
26853
  name: quibble
26634
26854
  description: File a Quibble complaint when the build, tests, docs or tooling of this project waste your time. Use it as soon as you hit the problem, not at the end of the task.
@@ -26746,826 +26966,1273 @@ function refreshedLine(project, tags) {
26746
26966
  return `${lead} tag each complaint with ${orList(tags)}, or ${OTHER_TAG_PREFIX}<name> when none fits.`;
26747
26967
  }
26748
26968
 
26749
- // src/commands/snippet.ts
26750
- import { existsSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
26751
- import { dirname as dirname6, join as join6, resolve as resolve2 } from "node:path";
26752
- function existsError(path) {
26753
- return new UsageError(`${path} already exists: pass --force to overwrite it`);
26754
- }
26755
- function readOptional2(path) {
26756
- try {
26757
- return readFileSync5(path, "utf8");
26758
- } catch (error62) {
26759
- if (isNotFound(error62)) return "";
26760
- throw error62;
26761
- }
26762
- }
26763
- function writeSkill(path, snippet) {
26764
- mkdirSync5(dirname6(path), { recursive: true });
26765
- writeFileSync4(path, skillFile(snippet), "utf8");
26969
+ // src/commands/login.ts
26970
+ import { hostname as hostname4 } from "node:os";
26971
+
26972
+ // src/opener.ts
26973
+ import { spawn as nodeSpawn2 } from "node:child_process";
26974
+ var OPEN_TIMEOUT_MS = 5e3;
26975
+ var OPENABLE_URL_RE = /^https?:\/\/[A-Za-z0-9.:[\]-]+(?:\/[A-Za-z0-9/?=._~-]*)?$/;
26976
+ function isWsl(platform, env) {
26977
+ if (platform !== "linux") return false;
26978
+ const set2 = (name) => (env[name] ?? "") !== "";
26979
+ return set2("WSL_DISTRO_NAME") || set2("WSL_INTEROP");
26980
+ }
26981
+ function openCommands(url2, platform, env) {
26982
+ if (platform === "darwin") return [{ command: "open", args: [url2] }];
26983
+ if (platform === "win32") {
26984
+ return [{ command: "cmd", args: ["/c", "start", "", url2] }];
26985
+ }
26986
+ if (isWsl(platform, env)) {
26987
+ return [
26988
+ { command: "wslview", args: [url2] },
26989
+ { command: "cmd.exe", args: ["/c", "start", "", url2] }
26990
+ ];
26991
+ }
26992
+ return [{ command: "xdg-open", args: [url2] }];
26993
+ }
26994
+ function attempt(spawn, entry, timeoutMs) {
26995
+ return new Promise((resolve4) => {
26996
+ let settled = false;
26997
+ const settle2 = (opened) => {
26998
+ if (settled) return;
26999
+ settled = true;
27000
+ clearTimeout(timer);
27001
+ resolve4(opened);
27002
+ };
27003
+ const timer = setTimeout(() => {
27004
+ settle2(false);
27005
+ }, timeoutMs);
27006
+ timer.unref();
27007
+ let child;
27008
+ try {
27009
+ child = spawn(entry.command, entry.args, {
27010
+ shell: false,
27011
+ detached: true,
27012
+ stdio: "ignore",
27013
+ windowsHide: true
27014
+ });
27015
+ } catch {
27016
+ settle2(false);
27017
+ return;
27018
+ }
27019
+ child.on("error", () => {
27020
+ settle2(false);
27021
+ });
27022
+ child.on("exit", (code2) => {
27023
+ settle2(code2 === 0);
27024
+ });
27025
+ child.unref();
27026
+ });
26766
27027
  }
26767
- async function snippetSource(project, context, io, options = {}) {
26768
- let tags;
26769
- if (context.mode === "remote") {
26770
- tags = (await (await openOrgClient(context, io, options)).project(project)).tags;
26771
- } else {
26772
- checkActAs(context);
26773
- tags = context.allowedTags;
26774
- }
26775
- const scope = {
26776
- ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26777
- ...context.slug === void 0 ? {} : { org: context.slug }
27028
+ function platformOpener(options = {}) {
27029
+ const platform = options.platform ?? process.platform;
27030
+ const env = options.env ?? process.env;
27031
+ const spawn = options.spawn ?? nodeSpawn2;
27032
+ const timeoutMs = options.timeoutMs ?? OPEN_TIMEOUT_MS;
27033
+ return {
27034
+ async open(url2) {
27035
+ if (!OPENABLE_URL_RE.test(url2)) return false;
27036
+ for (const entry of openCommands(url2, platform, env)) {
27037
+ if (await attempt(spawn, entry, timeoutMs)) return true;
27038
+ }
27039
+ return false;
27040
+ }
26778
27041
  };
26779
- return { tags, scope };
26780
27042
  }
26781
- async function runSnippet(options, context, io) {
26782
- const project = requireProject(context);
26783
- const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
26784
- const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
26785
- const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
26786
- const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
26787
- if (options.append !== void 0 && hasSnippet(existing)) {
26788
- throw new UsageError(
26789
- `${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
26790
- );
26791
- }
26792
- if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
26793
- throw existsError(skillPath);
26794
- }
26795
- const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
26796
- const { tags, scope } = await snippetSource(project, context, io);
26797
- let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
26798
- shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
26799
- if (appendPath !== void 0) {
26800
- const agent = override ?? agentForFile(appendPath);
26801
- const snippet = agentSnippet(tags, { ...scope, agent });
26802
- const kept = existing.replace(/\s+$/, "");
26803
- writeFileSync4(appendPath, kept === "" ? `${snippet}
26804
- ` : `${kept}
26805
27043
 
26806
- ${snippet}
26807
- `);
26808
- shown = { agent, snippet };
26809
- }
26810
- if (skillPath !== void 0) {
26811
- const agent = override ?? DEFAULT_SNIPPET_AGENT;
26812
- const snippet = agentSnippet(tags, { ...scope, agent });
26813
- writeSkill(skillPath, snippet);
26814
- if (appendPath === void 0) shown = { agent, snippet };
27044
+ // src/terminal.ts
27045
+ import { createInterface } from "node:readline";
27046
+ import { Writable } from "node:stream";
27047
+ var NodeTerminal = class {
27048
+ isTTY;
27049
+ #input;
27050
+ #output;
27051
+ #rl;
27052
+ /**
27053
+ * True unless a visible prompt is waiting, so a line that arrives early,
27054
+ * a pasted password after the email for one, echoes nowhere.
27055
+ */
27056
+ #muted = true;
27057
+ /** Lines that arrived before a prompt asked for them. */
27058
+ #lines = [];
27059
+ #waiting;
27060
+ #closed = false;
27061
+ constructor(input2, output2) {
27062
+ this.#input = input2;
27063
+ this.#output = output2;
27064
+ this.isTTY = input2.isTTY === true;
26815
27065
  }
26816
- const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26817
- if (options.json === true) {
26818
- writeJson(io.stdout, {
26819
- snippet: shown.snippet,
26820
- agent: shown.agent,
26821
- appended: appendPath ?? null,
26822
- skill: skillPath ?? null,
26823
- hook: hook?.path ?? null
26824
- });
26825
- return 0;
27066
+ prompt(label) {
27067
+ return this.#read(label, false);
26826
27068
  }
26827
- if (appendPath === void 0 && skillPath === void 0) {
26828
- writeLine(io.stdout, shown.snippet);
27069
+ promptHidden(label) {
27070
+ return this.#read(label, true);
26829
27071
  }
26830
- if (appendPath !== void 0) {
26831
- writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
27072
+ close() {
27073
+ this.#closed = true;
27074
+ this.#rl?.close();
27075
+ this.#rl = void 0;
26832
27076
  }
26833
- if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26834
- if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26835
- return 0;
26836
- }
26837
-
26838
- // src/commands/init.ts
26839
- var CODEX_LINE = "For Codex, run `quibble snippet --append AGENTS.md`, which names the agent codex.";
26840
- function agentLead(agent) {
26841
- return agent === DEFAULT_SNIPPET_AGENT ? "Add this to your CLAUDE.md:" : "Add this to your agent's instructions file:";
26842
- }
26843
- async function runInit(options, io) {
26844
- const directory = resolve3(io.cwd);
26845
- const path = join7(directory, PROJECT_CONFIG_FILENAME);
26846
- if (existsSync2(path) && options.force !== true) {
26847
- throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
27077
+ /** The one interface, created on the first prompt. */
27078
+ #interface() {
27079
+ if (this.#rl !== void 0) return this.#rl;
27080
+ const output2 = this.#output;
27081
+ const sink2 = new Writable({
27082
+ write: (chunk, _encoding, done) => {
27083
+ if (!this.#muted) output2.write(chunk);
27084
+ done();
27085
+ }
27086
+ });
27087
+ Object.defineProperties(sink2, {
27088
+ columns: { get: () => output2.columns },
27089
+ rows: { get: () => output2.rows }
27090
+ });
27091
+ const rl = createInterface({
27092
+ input: this.#input,
27093
+ output: sink2,
27094
+ terminal: true,
27095
+ // No answer enters the history, so an arrow key recalls no password.
27096
+ historySize: 0
27097
+ });
27098
+ rl.on("line", (line) => {
27099
+ this.#muted = true;
27100
+ if (this.#waiting !== void 0) {
27101
+ const settle2 = this.#waiting;
27102
+ this.#waiting = void 0;
27103
+ settle2(line);
27104
+ return;
27105
+ }
27106
+ this.#lines.push(line);
27107
+ });
27108
+ rl.on("close", () => {
27109
+ this.#closed = true;
27110
+ this.#waiting?.("");
27111
+ this.#waiting = void 0;
27112
+ });
27113
+ rl.on("SIGINT", () => {
27114
+ this.close();
27115
+ this.#output.write("\n");
27116
+ process.kill(process.pid, "SIGINT");
27117
+ });
27118
+ this.#rl = rl;
27119
+ return rl;
26848
27120
  }
26849
- const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
26850
- if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
26851
- throw existsError(skillPath);
27121
+ /** Waits for the next line, which a paste may have delivered already. */
27122
+ #next() {
27123
+ const queued = this.#lines.shift();
27124
+ if (queued !== void 0) return Promise.resolve(queued);
27125
+ if (this.#closed) return Promise.resolve("");
27126
+ return new Promise((resolve4) => {
27127
+ this.#waiting = resolve4;
27128
+ });
26852
27129
  }
26853
- const project = resolveProjectName(options.project, directory);
26854
- const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
26855
- const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
26856
- const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
26857
- const hook = options.hook === true ? planHook(directory) : void 0;
26858
- const endpoint = await endpointToWrite(options.endpoint, io);
26859
- const config2 = {
26860
- project,
26861
- ...org === void 0 ? {} : { org },
26862
- ...endpoint === void 0 ? {} : { endpoint },
26863
- ...tags === void 0 ? {} : { tags }
26864
- };
26865
- writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
26866
- `, "utf8");
26867
- const context = {
26868
- ...resolveContext({
26869
- cwd: io.cwd,
26870
- env: io.env,
26871
- endpoint: options.endpoint,
26872
- org: options.org,
26873
- local: options.local
26874
- }),
26875
- // `init` acts as nobody, so it ignores `QUIBBLE_ACT_AS`.
26876
- actAs: void 0
26877
- };
26878
- const snippet = agentSnippet(tags, {
26879
- agent,
26880
- ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26881
- ...org === void 0 ? {} : { org }
26882
- });
26883
- if (skillPath !== void 0) writeSkill(skillPath, snippet);
26884
- const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26885
- const check2 = await checkServer(context, project, io);
26886
- if (options.json === true) {
26887
- writeJson(io.stdout, {
26888
- path,
26889
- project,
26890
- org: org ?? null,
26891
- tags: tags ?? null,
26892
- skill: skillPath ?? null,
26893
- hook: hook?.path ?? null,
26894
- server: check2.server
27130
+ async #read(label, hidden) {
27131
+ if (this.#closed) return "";
27132
+ const rl = this.#interface();
27133
+ if (hidden) {
27134
+ this.#output.write(`${label} `);
27135
+ rl.setPrompt("");
27136
+ rl.prompt();
27137
+ try {
27138
+ return await this.#next();
27139
+ } finally {
27140
+ this.#output.write("\n");
27141
+ }
27142
+ }
27143
+ const queued = this.#lines.shift();
27144
+ if (queued !== void 0) {
27145
+ this.#output.write(`${label} ${queued}
27146
+ `);
27147
+ return queued;
27148
+ }
27149
+ rl.setPrompt(`${label} `);
27150
+ this.#muted = false;
27151
+ rl.prompt();
27152
+ try {
27153
+ return await this.#next();
27154
+ } finally {
27155
+ this.#muted = true;
27156
+ }
27157
+ }
27158
+ };
27159
+ function nodeTerminal(input2, output2) {
27160
+ return new NodeTerminal(input2, output2);
27161
+ }
27162
+
27163
+ // src/commands/login.ts
27164
+ var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
27165
+ var NEEDS_TERMINAL_MESSAGE = "quibble login --password needs a terminal: in CI, set QUIBBLE_TOKEN instead";
27166
+ var CI_MESSAGE = "quibble login needs a person to approve it in a browser: in CI, set QUIBBLE_TOKEN instead";
27167
+ var NO_BROWSER_MESSAGE = "--no-browser applies to the browser login only";
27168
+ var DENIED_MESSAGE = "login was denied in the browser";
27169
+ var EXPIRED_MESSAGE = "the login request expired: run quibble login again";
27170
+ var MALFORMED_MESSAGE = "the API answered a malformed login request";
27171
+ var UNKNOWN_STATUS_MESSAGE = "the API answered an unknown login status";
27172
+ var MFA_NEEDS_TERMINAL_MESSAGE = "this account needs a two-factor code: run quibble login in a terminal";
27173
+ var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
27174
+ var CUSTOM_CHALLENGE = "CUSTOM_CHALLENGE";
27175
+ var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
27176
+ var CLI_LOGINS_PATH = "/v1/cli-logins";
27177
+ var CLI_LOGIN_POLL_PATH = "/v1/cli-logins/poll";
27178
+ var MIN_INTERVAL_SECONDS = 1;
27179
+ var MAX_INTERVAL_SECONDS = 30;
27180
+ var PLAIN_HOST_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::[0-9]+)?$/;
27181
+ var LOGIN_PAGE = "/login";
27182
+ var ORGS_PAGE = "/orgs";
27183
+ var NO_PROFILE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
27184
+ function localModeLine(endpoint) {
27185
+ return `${endpoint} is in local mode: no login needed.`;
27186
+ }
27187
+ function terminalOf(io) {
27188
+ return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
27189
+ }
27190
+ function clockOf2(io) {
27191
+ return io.now ?? (() => /* @__PURE__ */ new Date());
27192
+ }
27193
+ function sleepOf(io) {
27194
+ return io.sleep ?? ((ms) => new Promise((resolve4) => {
27195
+ setTimeout(resolve4, ms);
27196
+ }));
27197
+ }
27198
+ function ciIsSet(env) {
27199
+ const value = (env["CI"] ?? "").trim().toLowerCase();
27200
+ return value !== "" && value !== "0" && value !== "false";
27201
+ }
27202
+ async function answer(terminal, label, hidden = false) {
27203
+ const text = hidden ? await terminal.promptHidden(label) : await terminal.prompt(label);
27204
+ const value = hidden ? text : text.trim();
27205
+ if (value === "")
27206
+ throw new UsageError(`${label.replace(/:$/, "").toLowerCase()} must not be empty`);
27207
+ return value;
27208
+ }
27209
+ function unsupportedStep(name) {
27210
+ return new RemoteError(
27211
+ `this account needs a sign-in step that quibble doesn't support (${name})`
27212
+ );
27213
+ }
27214
+ function pageError(error62, webUrl) {
27215
+ if (error62 instanceof CognitoError) {
27216
+ const page = `${trimSlash2(webUrl)}${LOGIN_PAGE}`;
27217
+ if (error62.type === "PasswordResetRequiredException") {
27218
+ return new ConfigError(`reset your password at ${page}`);
27219
+ }
27220
+ if (error62.type === "UserNotConfirmedException") {
27221
+ return new ConfigError(`finish your sign-up at ${page}`);
27222
+ }
27223
+ }
27224
+ return error62;
27225
+ }
27226
+ async function signIn(terminal, identity, config2) {
27227
+ const email3 = await answer(terminal, "Email:");
27228
+ const password = await answer(terminal, "Password:", true);
27229
+ const clientId = config2.cliClientId;
27230
+ try {
27231
+ let result = await initiateAuth(identity, {
27232
+ clientId,
27233
+ flow: "USER_PASSWORD_AUTH",
27234
+ username: email3,
27235
+ password
26895
27236
  });
27237
+ if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27238
+ const code2 = await answer(terminal, "Code:");
27239
+ result = await respondToAuthChallenge(identity, {
27240
+ clientId,
27241
+ challenge: result.challenge,
27242
+ email: email3,
27243
+ code: code2
27244
+ });
27245
+ }
27246
+ if (result.kind === "challenge") {
27247
+ const name = result.challenge.challengeName;
27248
+ if (name === NEW_PASSWORD_CHALLENGE) {
27249
+ throw new ConfigError(
27250
+ `finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
27251
+ );
27252
+ }
27253
+ throw unsupportedStep(name);
27254
+ }
27255
+ return result.tokens;
27256
+ } catch (error62) {
27257
+ throw pageError(error62, config2.webUrl);
27258
+ }
27259
+ }
27260
+ async function runLogin(options, context, io) {
27261
+ requireServer("login", context);
27262
+ if (options.password === true) {
27263
+ if (options.browser === false) throw new UsageError(NO_BROWSER_MESSAGE);
27264
+ return passwordLogin(options, context, io);
27265
+ }
27266
+ return browserLogin(options, context, io);
27267
+ }
27268
+ async function passwordLogin(options, context, io) {
27269
+ const terminal = terminalOf(io);
27270
+ if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
27271
+ const endpoint = await resolveEndpoint(context, io);
27272
+ const http = { debug: context.debug, fetch: io.fetch };
27273
+ const config2 = await fetchAuthConfig(endpoint, http);
27274
+ if (config2.mode === "local") {
27275
+ writeLine(io.stdout, localModeLine(endpoint));
26896
27276
  return 0;
26897
27277
  }
26898
- writeLine(io.stdout, `Wrote ${path}`);
26899
- if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26900
- if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26901
- writeLine(io.stdout, `Project: ${project}`);
26902
- if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
26903
- if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
26904
- writeLine(io.stdout);
26905
- writeLine(io.stdout, agentLead(agent));
26906
- writeLine(io.stdout);
26907
- writeLine(io.stdout, snippet);
26908
- if (agent === DEFAULT_SNIPPET_AGENT) {
26909
- writeLine(io.stdout);
26910
- writeLine(io.stdout, CODEX_LINE);
27278
+ let tokens;
27279
+ try {
27280
+ tokens = await signIn(terminal, { region: config2.region, ...http }, config2);
27281
+ } finally {
27282
+ terminal.close();
26911
27283
  }
26912
- if (check2.line !== void 0) {
26913
- writeLine(io.stdout);
26914
- writeLine(io.stdout, check2.line);
27284
+ const me = await storeSession(tokens, endpoint, config2, http, io);
27285
+ return reportLogin(me, endpoint, options, context, io);
27286
+ }
27287
+ async function browserLogin(options, context, io) {
27288
+ if (ciIsSet(io.env)) throw new UsageError(CI_MESSAGE);
27289
+ const endpoint = await resolveEndpoint(context, io);
27290
+ const session = await browserSession(endpoint, options, context.debug, io);
27291
+ if (session === void 0) {
27292
+ writeLine(io.stdout, localModeLine(endpoint));
27293
+ return 0;
26915
27294
  }
26916
- return 0;
27295
+ return reportLogin(session.me, endpoint, options, context, io);
26917
27296
  }
26918
- async function endpointToWrite(given, io) {
26919
- if (given === void 0 || given.trim() === "") return void 0;
26920
- const value = parseEndpoint(given, "--endpoint");
26921
- if (value.kind === "local") return LOCAL_ENDPOINT;
26922
- const api = await discoverApiUrl(value.url, {
26923
- fetch: io.fetch,
26924
- debug: debugEnabled(io.env)
27297
+ async function browserSession(endpoint, options, debug, io) {
27298
+ const http = { debug, fetch: io.fetch };
27299
+ const config2 = await fetchAuthConfig(endpoint, http);
27300
+ if (config2.mode === "local") return void 0;
27301
+ const clock = clockOf2(io);
27302
+ const started = await startLogin(endpoint, config2.webUrl, http, {
27303
+ host: cliLoginHost((io.hostname ?? hostname4)()),
27304
+ os: process.platform,
27305
+ version: options.version
26925
27306
  });
26926
- return isDefaultEndpoint(api) ? void 0 : api;
27307
+ const deadline = clock().getTime() + (CLI_LOGIN_TTL_SECONDS + started.interval) * 1e3;
27308
+ writeLine(
27309
+ io.stderr,
27310
+ `Approve this login in your browser: ${started.url} (code ${started.code}). Waiting...`
27311
+ );
27312
+ if (options.browser !== false) {
27313
+ const opener = io.opener ?? platformOpener({ env: io.env });
27314
+ void opener.open(started.url).catch(() => false);
27315
+ }
27316
+ const approval = await waitForApproval(endpoint, started, deadline, http, io);
27317
+ const tokens = await browserSignIn(
27318
+ { region: config2.region, ...http },
27319
+ config2,
27320
+ approval,
27321
+ io
27322
+ );
27323
+ const me = await storeSession(tokens, endpoint, config2, http, io);
27324
+ return { me, webUrl: config2.webUrl };
26927
27325
  }
26928
- async function checkServer(context, project, io) {
26929
- const org = context.org;
26930
- if (context.mode !== "remote" || org === void 0) return { server: "unchecked" };
26931
- const endpoint = await resolveEndpoint(context, io);
26932
- if (context.token === void 0 && context.credential === void 0 && !isLoopbackUrl(endpoint)) {
26933
- return { server: "unchecked" };
27326
+ function postJson(url2, body, http) {
27327
+ return send(url2, {
27328
+ method: "POST",
27329
+ headers: { "content-type": "application/json" },
27330
+ body: JSON.stringify(body),
27331
+ ...http
27332
+ });
27333
+ }
27334
+ async function startLogin(endpoint, webUrl, http, body) {
27335
+ const url2 = `${trimSlash2(endpoint)}${CLI_LOGINS_PATH}`;
27336
+ const site = { method: "POST", url: url2, debug: http.debug };
27337
+ const response = await postJson(url2, body, http);
27338
+ const text = await readText(response, site);
27339
+ if (response.status === 404) {
27340
+ throw new RemoteError(
27341
+ `${endpoint} does not support the browser login: run quibble login --password${detail(site, 404)}`,
27342
+ { status: 404 }
27343
+ );
26934
27344
  }
26935
- const warn = (text) => {
26936
- writeLine(io.stderr, `quibble: warning: ${text}`);
26937
- };
27345
+ if (!response.ok) throw remoteFailure(site, response, text);
27346
+ const started = checkStart(asRecord(parseQuietly(text)), webUrl);
27347
+ if (started === void 0) {
27348
+ throw new RemoteError(`${MALFORMED_MESSAGE}${detail(site, response.status)}`);
27349
+ }
27350
+ return started;
27351
+ }
27352
+ function checkStart(body, webUrl) {
27353
+ const loginKey = body?.["loginKey"];
27354
+ const code2 = body?.["code"];
27355
+ const url2 = body?.["url"];
27356
+ if (typeof loginKey !== "string" || !CLI_LOGIN_KEY_RE.test(loginKey)) return void 0;
27357
+ if (typeof code2 !== "string" || typeof url2 !== "string") return void 0;
26938
27358
  try {
26939
- await (await openClient(context, io, { queue: false })).project(project);
26940
- return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
26941
- } catch (error62) {
26942
- if (error62 instanceof RemoteError && error62.status === 404) {
26943
- if (error62.serverCode === ORG_NOT_FOUND) {
26944
- warn(`no organization ${org} on ${endpoint} for this account`);
26945
- return { server: "no_org" };
27359
+ if (parseCliLoginCode(code2) !== code2) return void 0;
27360
+ } catch {
27361
+ return void 0;
27362
+ }
27363
+ let origin;
27364
+ try {
27365
+ const web = new URL(webUrl);
27366
+ const secure = web.protocol === "https:" || web.protocol === "http:" && isLoopbackUrl(web.origin);
27367
+ if (!secure || !PLAIN_HOST_RE.test(web.host)) return void 0;
27368
+ origin = web.origin;
27369
+ } catch {
27370
+ return void 0;
27371
+ }
27372
+ if (url2 !== cliLoginUrl(origin, code2)) return void 0;
27373
+ const raw = body?.["interval"];
27374
+ const interval = typeof raw === "number" && Number.isInteger(raw) ? Math.min(MAX_INTERVAL_SECONDS, Math.max(MIN_INTERVAL_SECONDS, raw)) : CLI_LOGIN_POLL_SECONDS;
27375
+ return { loginKey, code: code2, url: url2, interval };
27376
+ }
27377
+ async function waitForApproval(endpoint, started, deadline, http, io) {
27378
+ const clock = clockOf2(io);
27379
+ const sleep = sleepOf(io);
27380
+ for (; ; ) {
27381
+ await sleep(started.interval * 1e3);
27382
+ if (clock().getTime() > deadline) throw new ConfigError(EXPIRED_MESSAGE);
27383
+ const answer2 = await poll(endpoint, started.loginKey, http);
27384
+ if (answer2 === void 0) continue;
27385
+ const status = answer2["status"];
27386
+ switch (status) {
27387
+ case "pending":
27388
+ continue;
27389
+ case "denied":
27390
+ throw new ConfigError(DENIED_MESSAGE);
27391
+ case "expired":
27392
+ throw new ConfigError(EXPIRED_MESSAGE);
27393
+ case "approved": {
27394
+ const username = answer2["username"];
27395
+ const secret = answer2["secret"];
27396
+ if (typeof username !== "string" || username === "") {
27397
+ throw new RemoteError(MALFORMED_MESSAGE);
27398
+ }
27399
+ if (typeof secret !== "string" || secret === "") {
27400
+ throw new RemoteError(MALFORMED_MESSAGE);
27401
+ }
27402
+ return { username, secret };
26946
27403
  }
26947
- warn(
26948
- `${org} has no project ${project} on ${endpoint}: an admin creates it in the organization settings`
26949
- );
26950
- return { server: "no_project" };
27404
+ default:
27405
+ throw new RemoteError(UNKNOWN_STATUS_MESSAGE);
26951
27406
  }
26952
- warn(`could not check the project on ${endpoint}: ${describeError(error62)}`);
26953
- return { server: "unreachable" };
26954
27407
  }
26955
27408
  }
26956
- function resolveProjectName(given, directory) {
26957
- const candidate = given?.trim();
26958
- if (candidate !== void 0 && candidate !== "") return parseProjectName(candidate);
26959
- const name = basename2(directory);
27409
+ async function poll(endpoint, loginKey, http) {
27410
+ const url2 = `${trimSlash2(endpoint)}${CLI_LOGIN_POLL_PATH}`;
27411
+ const site = { method: "POST", url: url2, debug: http.debug };
27412
+ let response;
27413
+ let text;
26960
27414
  try {
26961
- return parseProjectName(name);
27415
+ response = await postJson(url2, { loginKey }, http);
27416
+ text = await readText(response, site);
26962
27417
  } catch (error62) {
26963
- throw new ConfigError(
26964
- `cannot use the directory name "${name}" as a project name: pass --project <name>`,
26965
- void 0,
26966
- { cause: error62 }
26967
- );
27418
+ if (error62 instanceof RemoteError && error62.unanswered) return void 0;
27419
+ throw error62;
27420
+ }
27421
+ if (!response.ok) {
27422
+ if (response.status >= 500 || response.status === 429) return void 0;
27423
+ throw remoteFailure(site, response, text);
26968
27424
  }
27425
+ return asRecord(parseJson(text, site, response.status)) ?? {};
26969
27426
  }
26970
-
26971
- // src/mine.ts
26972
- function mineMessage(env) {
26973
- return `--mine needs a reporter: set "reporter" in ${defaultConfigPath(env)}`;
27427
+ async function browserSignIn(identity, config2, approval, io) {
27428
+ const clientId = config2.cliClientId;
27429
+ try {
27430
+ let result = await initiateAuth(identity, {
27431
+ clientId,
27432
+ flow: "CUSTOM_AUTH",
27433
+ username: approval.username
27434
+ });
27435
+ if (result.kind === "challenge") {
27436
+ const name = result.challenge.challengeName;
27437
+ if (name !== CUSTOM_CHALLENGE) throw unsupportedStep(name);
27438
+ result = await respondToCustomChallenge(identity, {
27439
+ clientId,
27440
+ challenge: result.challenge,
27441
+ username: approval.username,
27442
+ answer: approval.secret
27443
+ });
27444
+ }
27445
+ if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27446
+ result = await answerCode(
27447
+ identity,
27448
+ clientId,
27449
+ result.challenge,
27450
+ approval.username,
27451
+ io
27452
+ );
27453
+ }
27454
+ if (result.kind === "challenge") {
27455
+ throw unsupportedStep(result.challenge.challengeName);
27456
+ }
27457
+ return result.tokens;
27458
+ } catch (error62) {
27459
+ throw pageError(error62, config2.webUrl);
27460
+ }
26974
27461
  }
26975
- async function resolveMine(context, client, env) {
26976
- if (client === void 0) {
26977
- if (context.reporter === DEFAULT_REPORTER) throw new UsageError(mineMessage(env));
26978
- return context.reporter;
27462
+ async function answerCode(identity, clientId, challenge, username, io) {
27463
+ const terminal = terminalOf(io);
27464
+ try {
27465
+ if (!terminal.isTTY) throw new ConfigError(MFA_NEEDS_TERMINAL_MESSAGE);
27466
+ const code2 = await answer(terminal, "Code:");
27467
+ return await respondToAuthChallenge(identity, {
27468
+ clientId,
27469
+ challenge,
27470
+ email: username,
27471
+ code: code2
27472
+ });
27473
+ } finally {
27474
+ terminal.close();
26979
27475
  }
26980
- return (await client.me()).reporter;
26981
27476
  }
26982
-
26983
- // src/since.ts
26984
- var SINCE_MESSAGE = "--since must be a duration like 7d, a date like 2026-09-01, or an ISO 8601 UTC timestamp";
26985
- var DURATION_RE = /^(\d+)(m|h|d|w)$/;
26986
- var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
26987
- var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
26988
- var MINUTE_MS = 6e4;
26989
- var HOUR_MS = 60 * MINUTE_MS;
26990
- var DAY_MS3 = 24 * HOUR_MS;
26991
- var UNIT_MS = {
26992
- m: MINUTE_MS,
26993
- h: HOUR_MS,
26994
- d: DAY_MS3,
26995
- w: 7 * DAY_MS3
26996
- };
26997
- var EXPIRES_MESSAGE = "--expires must be a duration like 30d, a date like 2026-12-01, or an ISO 8601 UTC timestamp";
26998
- function reject(message = SINCE_MESSAGE) {
26999
- throw new UsageError(message);
27000
- }
27001
- function iso(at) {
27002
- if (Number.isNaN(at.getTime())) reject();
27003
- return at.toISOString();
27004
- }
27005
- function durationMs(digits, unit) {
27006
- if (digits.startsWith("0")) return void 0;
27007
- const step = UNIT_MS[unit];
27008
- return step === void 0 ? void 0 : Number(digits) * step;
27477
+ async function storeSession(tokens, endpoint, config2, http, io) {
27478
+ if (tokens.refreshToken === void 0) {
27479
+ throw new RemoteError("sign-in didn't return a refresh token");
27480
+ }
27481
+ const now = clockOf2(io)();
27482
+ const me = await readProfile(
27483
+ new ApiClient({
27484
+ endpoint,
27485
+ authorizer: bearerAuthorizer(tokens.accessToken),
27486
+ ...http
27487
+ }),
27488
+ config2.webUrl
27489
+ );
27490
+ if (me.sub === void 0 || me.email === void 0) {
27491
+ throw new RemoteError(
27492
+ "unexpected response from the API: identity has no sub or email"
27493
+ );
27494
+ }
27495
+ saveCredential(io.env, {
27496
+ endpoint,
27497
+ refreshToken: tokens.refreshToken,
27498
+ accessToken: tokens.accessToken,
27499
+ expiresAt: new Date(now.getTime() + tokens.expiresIn * 1e3).toISOString(),
27500
+ sub: me.sub,
27501
+ email: me.email
27502
+ });
27503
+ return me;
27009
27504
  }
27010
- function fromDuration(digits, unit, now) {
27011
- const span = durationMs(digits, unit);
27012
- if (span === void 0) reject();
27013
- return iso(new Date(now.getTime() - span));
27505
+ function reportLogin(me, endpoint, options, context, io) {
27506
+ const saved = saveEndpoint(context, endpoint, io.env);
27507
+ if (options.json === true) {
27508
+ writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
27509
+ return 0;
27510
+ }
27511
+ writeLine(io.stdout, loggedInLine(endpoint, me));
27512
+ if (saved !== void 0) {
27513
+ writeLine(io.stdout, `Saved ${endpoint} as your endpoint in ${saved}.`);
27514
+ }
27515
+ return 0;
27014
27516
  }
27015
- function fromDate(value) {
27016
- const at = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
27017
- const stamp2 = iso(at);
27018
- if (!stamp2.startsWith(value)) reject();
27019
- return stamp2;
27517
+ function loggedInLine(endpoint, me) {
27518
+ return `Logged in to ${endpoint} as ${me.email ?? me.reporter}.`;
27020
27519
  }
27021
- function parseSince(text, now) {
27022
- const value = text.trim();
27023
- const duration3 = DURATION_RE.exec(value);
27024
- if (duration3 !== null) {
27025
- return fromDuration(duration3[1] ?? "", duration3[2] ?? "", now);
27520
+ function saveEndpoint(context, endpoint, env) {
27521
+ if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
27522
+ return void 0;
27026
27523
  }
27027
- if (DATE_RE.test(value)) return fromDate(value);
27028
- if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
27029
- return reject();
27524
+ return rememberEndpoint(env, endpoint);
27030
27525
  }
27031
- function parseExpires(text, now) {
27032
- const value = text.trim();
27033
- const duration3 = DURATION_RE.exec(value);
27034
- let stamp2;
27526
+ async function readProfile(client, webUrl) {
27035
27527
  try {
27036
- if (duration3 !== null) {
27037
- const span = durationMs(duration3[1] ?? "", duration3[2] ?? "");
27038
- if (span === void 0) reject(EXPIRES_MESSAGE);
27039
- stamp2 = iso(new Date(now.getTime() + span));
27040
- } else if (DATE_RE.test(value)) {
27041
- stamp2 = fromDate(value);
27042
- } else if (TIMESTAMP_RE.test(value)) {
27043
- stamp2 = iso(new Date(value));
27044
- } else {
27045
- return reject(EXPIRES_MESSAGE);
27528
+ return await client.me();
27529
+ } catch (error62) {
27530
+ if (error62 instanceof RemoteError && NO_PROFILE_STATUSES.has(error62.status ?? 0)) {
27531
+ throw new ConfigError(`finish your account at ${trimSlash2(webUrl)}${LOGIN_PAGE}`);
27046
27532
  }
27047
- } catch {
27048
- return reject(EXPIRES_MESSAGE);
27533
+ throw error62;
27049
27534
  }
27050
- return stamp2;
27051
27535
  }
27052
-
27053
- // src/scope.ts
27054
- function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
27055
- if (options.project !== void 0 && options.all === true) {
27056
- throw new UsageError("--project and --all cannot be used together");
27536
+ async function runLogout(context, io) {
27537
+ requireServer("logout", context);
27538
+ const endpoint = await resolveEndpoint(context, io);
27539
+ const credential = findCredential(io.env, endpoint);
27540
+ if (credential === void 0) {
27541
+ writeLine(io.stdout, `Not logged in to ${endpoint}.`);
27542
+ return 0;
27057
27543
  }
27058
- if (options.open === true && options.status !== void 0) {
27059
- throw new UsageError("--open and --status cannot be used together");
27544
+ const http = { debug: context.debug, fetch: io.fetch };
27545
+ if (!await revoke(endpoint, credential.refreshToken, http)) {
27546
+ writeLine(
27547
+ io.stderr,
27548
+ `quibble: warning: could not revoke the refresh token at ${endpoint}`
27549
+ );
27060
27550
  }
27061
- const project = options.project ?? (options.all === true ? void 0 : context.project);
27062
- const status = options.open === true ? "open" : options.status;
27063
- return {
27064
- // No list reads two organizations, because the path names one.
27065
- org: requireOrg(context),
27066
- ...project === void 0 ? {} : { project },
27067
- ...status === void 0 ? {} : { status },
27068
- ...options.since === void 0 ? {} : { since: parseSince(options.since, now) }
27069
- };
27070
- }
27071
- function tagFilter(tags) {
27072
- return tags === void 0 || tags.length === 0 ? {} : { tags: [...tags] };
27073
- }
27074
-
27075
- // src/commands/list.ts
27076
- var COLUMNS = [
27077
- "id",
27078
- "created",
27079
- "severity",
27080
- "status",
27081
- "project",
27082
- "reporter",
27083
- "tags",
27084
- "group",
27085
- "summary"
27086
- ];
27087
- var REPORTER_WIDTH = 20;
27088
- var TAGS_WIDTH = 24;
27089
- var SUMMARY_MAX = 60;
27090
- var RECURRED_MARK = "\u21BB";
27091
- function cut2(text, width) {
27092
- if (text.length <= width) return text;
27093
- return `${text.slice(0, width - 3)}...`;
27094
- }
27095
- function summaryOf(complaint) {
27096
- return cut2(printable(summarize(complaint.message)), SUMMARY_MAX);
27551
+ removeCredential(io.env, endpoint);
27552
+ writeLine(io.stdout, `Logged out of ${endpoint}.`);
27553
+ return 0;
27097
27554
  }
27098
- function limitNote(count, limit, more) {
27099
- if (limit >= MAX_LIMIT) {
27100
- return count >= MAX_LIMIT ? `Showing the newest ${MAX_LIMIT} complaints: older ones may exist, narrow the filters to see them.` : void 0;
27555
+ async function revoke(endpoint, refreshToken, http) {
27556
+ try {
27557
+ const config2 = await fetchAuthConfig(endpoint, http);
27558
+ if (config2.mode === "local") return true;
27559
+ await revokeToken(
27560
+ { region: config2.region, ...http },
27561
+ { clientId: config2.cliClientId, token: refreshToken }
27562
+ );
27563
+ return true;
27564
+ } catch {
27565
+ return false;
27101
27566
  }
27102
- return more ? `Showing the newest ${limit} complaints: pass --limit to see more (up to ${MAX_LIMIT}).` : void 0;
27103
27567
  }
27104
- var BUDGET_NOTE = "The server stopped searching early, so older matches may exist: narrow the filters or pass --since.";
27105
- async function runList(options, context, io) {
27106
- if (options.ids === true && options.json === true) {
27107
- throw new UsageError("--ids and --json cannot be used together");
27108
- }
27109
- const { store, client } = await openBackend(context, io);
27110
- const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
27111
- const filter = parseListFilter({
27112
- ...resolveScope(options, context),
27113
- ...options.severity === void 0 ? {} : { severity: options.severity },
27114
- ...tagFilter(options.tag),
27115
- ...options.search === void 0 ? {} : { q: options.search },
27116
- ...reporter === void 0 ? {} : { reporter },
27117
- ...options.limit === void 0 ? {} : { limit: options.limit }
27118
- });
27119
- const limit = filter.limit ?? DEFAULT_LIMIT;
27120
- const page = await listComplaintPage(store, {
27121
- ...filter,
27122
- limit: Math.min(limit + 1, MAX_LIMIT)
27123
- });
27124
- const complaints = page.complaints.slice(0, limit);
27125
- if (options.json === true) {
27126
- writeJson(io.stdout, complaints);
27568
+ async function runWhoami(options, context, io) {
27569
+ if (context.mode === "local") {
27570
+ checkActAs(context);
27571
+ writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
27127
27572
  return 0;
27128
27573
  }
27129
- const note = limitNote(complaints.length, limit, page.complaints.length > limit);
27130
- if (note !== void 0) writeLine(io.stderr, note);
27131
- if (page.truncated) writeLine(io.stderr, BUDGET_NOTE);
27132
- if (options.ids === true) {
27133
- for (const complaint of complaints) writeLine(io.stdout, complaint.id);
27574
+ const client = await openClient(context, io);
27575
+ const endpoint = requireEndpoint(context);
27576
+ const me = await client.me();
27577
+ const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
27578
+ if (options.json === true) {
27579
+ writeJson(io.stdout, {
27580
+ ...me.body,
27581
+ endpoint,
27582
+ ...org === void 0 ? {} : { org: org.body }
27583
+ });
27134
27584
  return 0;
27135
27585
  }
27136
- if (complaints.length === 0) {
27137
- writeLine(io.stdout, "No complaints found.");
27138
- return 0;
27586
+ const lines = whoamiLines(endpoint, me, org);
27587
+ if (me.kind !== "token" && me.orgs.length === 0) {
27588
+ const config2 = await fetchAuthConfig(endpoint, {
27589
+ debug: context.debug,
27590
+ fetch: io.fetch
27591
+ });
27592
+ const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
27593
+ lines.push("", `Create one at ${webUrl}${ORGS_PAGE}.`);
27139
27594
  }
27140
- const lines = options.ungroup === true ? formatTable(complaints.map((complaint) => ({ complaint }))) : formatTable(
27141
- groupComplaints(complaints).map((group) => ({
27142
- complaint: group.head,
27143
- group
27144
- }))
27145
- );
27146
27595
  for (const line of lines) writeLine(io.stdout, line);
27147
27596
  return 0;
27148
27597
  }
27149
- function reporterOf(complaint) {
27150
- return cut2(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
27598
+ function rows(pairs) {
27599
+ const width = Math.max(...pairs.map(([key]) => key.length));
27600
+ return pairs.map(([key, value]) => `${key.padEnd(width)} ${value}`);
27151
27601
  }
27152
- function groupCell(group) {
27153
- const count = group.count > 1 ? `\xD7${group.count}` : "";
27154
- return `${count}${group.recurred ? RECURRED_MARK : ""}`;
27602
+ function block(entries) {
27603
+ if (entries.length === 0) return [" none"];
27604
+ const width = Math.max(...entries.map(([key]) => key.length));
27605
+ return entries.map(([key, value]) => ` ${key.padEnd(width)} ${value}`);
27155
27606
  }
27156
- function formatTable(rows2) {
27157
- const grouped = rows2.some((row) => row.group !== void 0);
27158
- const columns = COLUMNS.filter((name) => grouped || name !== "group");
27159
- const cells = rows2.map(({ complaint, group }) => {
27160
- const values = {
27161
- id: complaint.id.slice(0, 8),
27162
- created: complaint.createdAt.slice(0, 16),
27163
- severity: complaint.severity,
27164
- status: complaint.status,
27165
- project: complaint.project,
27166
- reporter: reporterOf(complaint),
27167
- tags: cut2(printable(complaint.tags.join(",")), TAGS_WIDTH),
27168
- group: group === void 0 ? "" : groupCell(group),
27169
- summary: summaryOf(complaint)
27170
- };
27171
- return columns.map((name) => values[name]);
27172
- });
27173
- const widths = columns.map(
27174
- (name, column) => cells.reduce(
27175
- (width, row) => Math.max(width, (row[column] ?? "").length),
27176
- name.length
27177
- )
27178
- );
27179
- const format = (line) => line.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
27180
- return [format(columns), ...cells.map(format)];
27607
+ function whoamiLines(endpoint, me, org) {
27608
+ if (me.kind === "token") {
27609
+ return rows([
27610
+ ["endpoint", endpoint],
27611
+ ["token", me.name ?? me.reporter],
27612
+ ["org", me.org ?? ""],
27613
+ ["project", me.project ?? ""],
27614
+ ["role", me.role ?? "filer"]
27615
+ ]);
27616
+ }
27617
+ const lines = rows([
27618
+ ["endpoint", endpoint],
27619
+ ["email", me.email ?? ""],
27620
+ ["name", me.name ?? ""],
27621
+ ["sub", me.sub ?? ""],
27622
+ ["platform", me.platform ? "yes" : "no"]
27623
+ ]);
27624
+ lines.push("", "Organizations");
27625
+ lines.push(...block(me.orgs.map((entry) => [entry.org, entry.role])));
27626
+ if (org === void 0) return lines;
27627
+ lines.push("", `Roles in ${org.org}`);
27628
+ lines.push(...block(org.roles.map((entry) => [entry.project, entry.role])));
27629
+ return lines;
27181
27630
  }
27182
27631
 
27183
- // src/commands/orgs.ts
27184
- var ORGS_PAGE = "/orgs";
27185
- var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
27186
- async function runOrgs(options, context, io) {
27187
- requireServer("orgs", context);
27188
- const client = await openClient(context, io);
27189
- const endpoint = requireEndpoint(context);
27190
- const answer2 = await client.orgs();
27191
- if (options.json === true) {
27192
- writeJson(io.stdout, answer2.body);
27193
- return 0;
27632
+ // src/commands/snippet.ts
27633
+ import { existsSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
27634
+ import { dirname as dirname6, join as join6, resolve as resolve2 } from "node:path";
27635
+ function existsError(path) {
27636
+ return new UsageError(`${path} already exists: pass --force to overwrite it`);
27637
+ }
27638
+ function readOptional2(path) {
27639
+ try {
27640
+ return readFileSync5(path, "utf8");
27641
+ } catch (error62) {
27642
+ if (isNotFound(error62)) return "";
27643
+ throw error62;
27194
27644
  }
27195
- if (answer2.orgs.length === 0) {
27196
- const config2 = await fetchAuthConfig(endpoint, {
27197
- debug: context.debug,
27198
- fetch: io.fetch,
27199
- timeoutMs: context.timeoutMs
27645
+ }
27646
+ function writeSkill(path, snippet) {
27647
+ mkdirSync5(dirname6(path), { recursive: true });
27648
+ writeFileSync4(path, skillFile(snippet), "utf8");
27649
+ }
27650
+ async function snippetSource(project, context, io, options = {}) {
27651
+ let tags;
27652
+ if (context.mode === "remote") {
27653
+ tags = (await (await openOrgClient(context, io, options)).project(project)).tags;
27654
+ } else {
27655
+ checkActAs(context);
27656
+ tags = context.allowedTags;
27657
+ }
27658
+ const scope = {
27659
+ ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
27660
+ ...context.slug === void 0 ? {} : { org: context.slug }
27661
+ };
27662
+ return { tags, scope };
27663
+ }
27664
+ async function runSnippet(options, context, io) {
27665
+ const project = requireProject(context);
27666
+ const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
27667
+ const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
27668
+ const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
27669
+ const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
27670
+ if (options.append !== void 0 && hasSnippet(existing)) {
27671
+ throw new UsageError(
27672
+ `${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
27673
+ );
27674
+ }
27675
+ if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
27676
+ throw existsError(skillPath);
27677
+ }
27678
+ const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
27679
+ const { tags, scope } = await snippetSource(project, context, io);
27680
+ let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
27681
+ shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
27682
+ if (appendPath !== void 0) {
27683
+ const agent = override ?? agentForFile(appendPath);
27684
+ const snippet = agentSnippet(tags, { ...scope, agent });
27685
+ const kept = existing.replace(/\s+$/, "");
27686
+ writeFileSync4(appendPath, kept === "" ? `${snippet}
27687
+ ` : `${kept}
27688
+
27689
+ ${snippet}
27690
+ `);
27691
+ shown = { agent, snippet };
27692
+ }
27693
+ if (skillPath !== void 0) {
27694
+ const agent = override ?? DEFAULT_SNIPPET_AGENT;
27695
+ const snippet = agentSnippet(tags, { ...scope, agent });
27696
+ writeSkill(skillPath, snippet);
27697
+ if (appendPath === void 0) shown = { agent, snippet };
27698
+ }
27699
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
27700
+ if (options.json === true) {
27701
+ writeJson(io.stdout, {
27702
+ snippet: shown.snippet,
27703
+ agent: shown.agent,
27704
+ appended: appendPath ?? null,
27705
+ skill: skillPath ?? null,
27706
+ hook: hook?.path ?? null
27200
27707
  });
27201
- const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
27202
- writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE}.`);
27203
27708
  return 0;
27204
27709
  }
27205
- for (const line of formatOrgs(answer2.orgs)) writeLine(io.stdout, line);
27710
+ if (appendPath === void 0 && skillPath === void 0) {
27711
+ writeLine(io.stdout, shown.snippet);
27712
+ }
27713
+ if (appendPath !== void 0) {
27714
+ writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
27715
+ }
27716
+ if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
27717
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
27206
27718
  return 0;
27207
27719
  }
27208
- function formatOrgs(orgs) {
27209
- const rows2 = [...orgs].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((entry) => [
27210
- entry.org,
27211
- entry.name,
27212
- entry.role,
27213
- entry.openCount === void 0 ? "" : String(entry.openCount)
27214
- ]);
27215
- const widths = COLUMNS2.map(
27216
- (name, column) => rows2.reduce((width, row) => Math.max(width, (row[column] ?? "").length), name.length)
27217
- );
27218
- const format = (cells) => cells.map(
27219
- (cell2, column) => column === COLUMNS2.length - 1 ? cell2.padStart(widths[column] ?? 0) : cell2.padEnd(widths[column] ?? 0)
27220
- ).join(" ").trimEnd();
27221
- return [format(COLUMNS2), ...rows2.map(format)];
27222
- }
27223
27720
 
27224
- // src/terminal.ts
27225
- import { createInterface } from "node:readline";
27226
- import { Writable } from "node:stream";
27227
- var NodeTerminal = class {
27228
- isTTY;
27229
- #input;
27230
- #output;
27231
- #rl;
27232
- /**
27233
- * True unless a visible prompt is waiting, so a line that arrives early,
27234
- * a pasted password after the email for one, echoes nowhere.
27235
- */
27236
- #muted = true;
27237
- /** Lines that arrived before a prompt asked for them. */
27238
- #lines = [];
27239
- #waiting;
27240
- #closed = false;
27241
- constructor(input2, output2) {
27242
- this.#input = input2;
27243
- this.#output = output2;
27244
- this.isTTY = input2.isTTY === true;
27721
+ // src/commands/init.ts
27722
+ var CODEX_LINE = "For Codex, run `quibble snippet --append AGENTS.md`, which names the agent codex.";
27723
+ var LOGIN_PAGE2 = "/login";
27724
+ var ORGS_PAGE2 = "/orgs";
27725
+ var CONFLICT = "conflict";
27726
+ function agentLead(agent) {
27727
+ return agent === DEFAULT_SNIPPET_AGENT ? "Add this to your CLAUDE.md:" : "Add this to your agent's instructions file:";
27728
+ }
27729
+ var SOURCE_NOTES = {
27730
+ flag: "",
27731
+ git: " (from the git remote origin)",
27732
+ directory: " (from the directory name)",
27733
+ token: " (from the project token)"
27734
+ };
27735
+ async function runInit(options, io) {
27736
+ const directory = resolve3(io.cwd);
27737
+ const path = join7(directory, PROJECT_CONFIG_FILENAME);
27738
+ if (existsSync2(path) && options.force !== true) {
27739
+ throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
27245
27740
  }
27246
- prompt(label) {
27247
- return this.#read(label, false);
27741
+ const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
27742
+ if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
27743
+ throw existsError(skillPath);
27248
27744
  }
27249
- promptHidden(label) {
27250
- return this.#read(label, true);
27745
+ const given = options.project?.trim();
27746
+ const flagProject = given === void 0 || given === "" ? void 0 : parseProjectName(given);
27747
+ let org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
27748
+ const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
27749
+ const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
27750
+ const hook = options.hook === true ? planHook(directory) : void 0;
27751
+ const endpoint = await endpointToWrite(options.endpoint, io);
27752
+ const context = {
27753
+ ...resolveContext({
27754
+ cwd: io.cwd,
27755
+ env: io.env,
27756
+ endpoint: options.endpoint,
27757
+ local: options.local,
27758
+ readProjectConfig: false
27759
+ }),
27760
+ // `init` acts as nobody, so it ignores `QUIBBLE_ACT_AS`.
27761
+ actAs: void 0
27762
+ };
27763
+ let named;
27764
+ let nameRefusal;
27765
+ if (flagProject !== void 0) {
27766
+ named = { project: flagProject, source: "flag" };
27767
+ } else {
27768
+ try {
27769
+ named = await nameFromRepository(directory, io);
27770
+ } catch (error62) {
27771
+ if (!(error62 instanceof ConfigError) || context.token === void 0) throw error62;
27772
+ nameRefusal = error62;
27773
+ }
27251
27774
  }
27252
- close() {
27253
- this.#closed = true;
27254
- this.#rl?.close();
27255
- this.#rl = void 0;
27775
+ let remote;
27776
+ if (context.mode === "remote") {
27777
+ const login = await logIn(context, options, io);
27778
+ if (login === "local") return 0;
27779
+ remote = login;
27256
27780
  }
27257
- /** The one interface, created on the first prompt. */
27258
- #interface() {
27259
- if (this.#rl !== void 0) return this.#rl;
27260
- const output2 = this.#output;
27261
- const sink2 = new Writable({
27262
- write: (chunk, _encoding, done) => {
27263
- if (!this.#muted) output2.write(chunk);
27264
- done();
27265
- }
27266
- });
27267
- Object.defineProperties(sink2, {
27268
- columns: { get: () => output2.columns },
27269
- rows: { get: () => output2.rows }
27270
- });
27271
- const rl = createInterface({
27272
- input: this.#input,
27273
- output: sink2,
27274
- terminal: true,
27275
- // No answer enters the history, so an arrow key recalls no password.
27276
- historySize: 0
27277
- });
27278
- rl.on("line", (line) => {
27279
- this.#muted = true;
27280
- if (this.#waiting !== void 0) {
27281
- const settle2 = this.#waiting;
27282
- this.#waiting = void 0;
27283
- settle2(line);
27284
- return;
27285
- }
27286
- this.#lines.push(line);
27287
- });
27288
- rl.on("close", () => {
27289
- this.#closed = true;
27290
- this.#waiting?.("");
27291
- this.#waiting = void 0;
27292
- });
27293
- rl.on("SIGINT", () => {
27294
- this.close();
27295
- this.#output.write("\n");
27296
- process.kill(process.pid, "SIGINT");
27781
+ if (org === void 0 && remote?.client !== void 0) {
27782
+ const account = await findOrganization(remote.client, remote, context, io);
27783
+ org = account.org;
27784
+ if (account.project !== void 0 && flagProject === void 0) {
27785
+ named = { project: account.project, source: "token" };
27786
+ }
27787
+ }
27788
+ if (named === void 0) throw nameRefusal;
27789
+ const { project, source } = named;
27790
+ const config2 = {
27791
+ project,
27792
+ ...org === void 0 ? {} : { org },
27793
+ ...endpoint === void 0 ? {} : { endpoint },
27794
+ ...tags === void 0 ? {} : { tags }
27795
+ };
27796
+ writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
27797
+ `, "utf8");
27798
+ const snippet = agentSnippet(tags, {
27799
+ agent,
27800
+ ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
27801
+ ...org === void 0 ? {} : { org }
27802
+ });
27803
+ if (skillPath !== void 0) writeSkill(skillPath, snippet);
27804
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
27805
+ const check2 = remote?.client === void 0 || org === void 0 ? { server: "unchecked" } : await checkServer(remote.client.forOrg(org), remote.endpoint, context, {
27806
+ org,
27807
+ project,
27808
+ tags,
27809
+ io
27810
+ });
27811
+ if (options.json === true) {
27812
+ writeJson(io.stdout, {
27813
+ path,
27814
+ project,
27815
+ projectSource: source,
27816
+ org: org ?? null,
27817
+ tags: tags ?? null,
27818
+ skill: skillPath ?? null,
27819
+ hook: hook?.path ?? null,
27820
+ server: check2.server
27297
27821
  });
27298
- this.#rl = rl;
27299
- return rl;
27822
+ return 0;
27300
27823
  }
27301
- /** Waits for the next line, which a paste may have delivered already. */
27302
- #next() {
27303
- const queued = this.#lines.shift();
27304
- if (queued !== void 0) return Promise.resolve(queued);
27305
- if (this.#closed) return Promise.resolve("");
27306
- return new Promise((resolve4) => {
27307
- this.#waiting = resolve4;
27308
- });
27824
+ writeLine(io.stdout, `Wrote ${path}`);
27825
+ if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
27826
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
27827
+ writeLine(io.stdout, `Project: ${project}${SOURCE_NOTES[source]}`);
27828
+ if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
27829
+ if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
27830
+ writeLine(io.stdout);
27831
+ writeLine(io.stdout, agentLead(agent));
27832
+ writeLine(io.stdout);
27833
+ writeLine(io.stdout, snippet);
27834
+ if (agent === DEFAULT_SNIPPET_AGENT) {
27835
+ writeLine(io.stdout);
27836
+ writeLine(io.stdout, CODEX_LINE);
27309
27837
  }
27310
- async #read(label, hidden) {
27311
- if (this.#closed) return "";
27312
- const rl = this.#interface();
27313
- if (hidden) {
27314
- this.#output.write(`${label} `);
27315
- rl.setPrompt("");
27316
- rl.prompt();
27317
- try {
27318
- return await this.#next();
27319
- } finally {
27320
- this.#output.write("\n");
27321
- }
27322
- }
27323
- const queued = this.#lines.shift();
27324
- if (queued !== void 0) {
27325
- this.#output.write(`${label} ${queued}
27326
- `);
27327
- return queued;
27328
- }
27329
- rl.setPrompt(`${label} `);
27330
- this.#muted = false;
27331
- rl.prompt();
27332
- try {
27333
- return await this.#next();
27334
- } finally {
27335
- this.#muted = true;
27336
- }
27838
+ if (check2.line !== void 0) {
27839
+ writeLine(io.stdout);
27840
+ writeLine(io.stdout, check2.line);
27337
27841
  }
27338
- };
27339
- function nodeTerminal(input2, output2) {
27340
- return new NodeTerminal(input2, output2);
27341
- }
27342
-
27343
- // src/commands/login.ts
27344
- var LOCAL_IDENTITY_MESSAGE = "local mode: no identity.";
27345
- var NEEDS_TERMINAL_MESSAGE = "quibble login needs a terminal: in CI, set QUIBBLE_TOKEN instead";
27346
- var MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA";
27347
- var NEW_PASSWORD_CHALLENGE = "NEW_PASSWORD_REQUIRED";
27348
- var LOGIN_PAGE = "/login";
27349
- var ORGS_PAGE2 = "/orgs";
27350
- var NO_PROFILE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
27351
- function terminalOf(io) {
27352
- return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
27842
+ return 0;
27353
27843
  }
27354
- async function answer(terminal, label, hidden = false) {
27355
- const text = hidden ? await terminal.promptHidden(label) : await terminal.prompt(label);
27356
- const value = hidden ? text : text.trim();
27357
- if (value === "")
27358
- throw new UsageError(`${label.replace(/:$/, "").toLowerCase()} must not be empty`);
27359
- return value;
27844
+ async function endpointToWrite(given, io) {
27845
+ if (given === void 0 || given.trim() === "") return void 0;
27846
+ const value = parseEndpoint(given, "--endpoint");
27847
+ if (value.kind === "local") return LOCAL_ENDPOINT;
27848
+ const api = await discoverApiUrl(value.url, {
27849
+ fetch: io.fetch,
27850
+ debug: debugEnabled(io.env)
27851
+ });
27852
+ return isDefaultEndpoint(api) ? void 0 : api;
27360
27853
  }
27361
- async function signIn(terminal, identity, config2) {
27362
- const email3 = await answer(terminal, "Email:");
27363
- const password = await answer(terminal, "Password:", true);
27364
- const clientId = config2.cliClientId;
27854
+ async function nameFromRepository(directory, io) {
27855
+ const url2 = await (io.git ?? nodeGit()).originUrl(directory);
27856
+ const fromGit = url2 === void 0 ? void 0 : projectOfRemote(url2);
27857
+ if (fromGit !== void 0) return { project: fromGit, source: "git" };
27858
+ const name = basename2(directory);
27365
27859
  try {
27366
- let result = await initiateAuth(identity, {
27367
- clientId,
27368
- flow: "USER_PASSWORD_AUTH",
27369
- username: email3,
27370
- password
27371
- });
27372
- if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27373
- const code2 = await answer(terminal, "Code:");
27374
- result = await respondToAuthChallenge(identity, {
27375
- clientId,
27376
- challenge: result.challenge,
27377
- email: email3,
27378
- code: code2
27379
- });
27380
- }
27381
- if (result.kind === "challenge") {
27382
- const name = result.challenge.challengeName;
27383
- if (name === NEW_PASSWORD_CHALLENGE) {
27384
- throw new ConfigError(
27385
- `finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
27386
- );
27387
- }
27388
- throw new RemoteError(
27389
- `this account needs a sign-in step that quibble doesn't support (${name})`
27390
- );
27391
- }
27392
- return result.tokens;
27860
+ return { project: parseProjectName(name), source: "directory" };
27393
27861
  } catch (error62) {
27394
- if (error62 instanceof CognitoError) {
27395
- const page = `${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`;
27396
- if (error62.type === "PasswordResetRequiredException") {
27397
- throw new ConfigError(`reset your password at ${page}`);
27398
- }
27399
- if (error62.type === "UserNotConfirmedException") {
27400
- throw new ConfigError(`finish your sign-up at ${page}`);
27401
- }
27402
- }
27403
- throw error62;
27862
+ throw new ConfigError(
27863
+ `cannot use the directory name "${name}" as a project name: pass --project <name>`,
27864
+ void 0,
27865
+ { cause: error62 }
27866
+ );
27404
27867
  }
27405
27868
  }
27406
- async function runLogin(options, context, io) {
27407
- requireServer("login", context);
27408
- const terminal = terminalOf(io);
27409
- if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
27869
+ async function logIn(context, options, io) {
27410
27870
  const endpoint = await resolveEndpoint(context, io);
27411
- const http = { debug: context.debug, fetch: io.fetch };
27412
- const config2 = await fetchAuthConfig(endpoint, http);
27413
- if (config2.mode === "local") {
27414
- writeLine(io.stdout, `${endpoint} is in local mode: no login needed.`);
27415
- return 0;
27871
+ let webUrl;
27872
+ const loopback = isLoopbackUrl(endpoint);
27873
+ if (context.token === void 0 && context.credential === void 0 && !loopback) {
27874
+ if (options.login === false || ciIsSet(io.env)) {
27875
+ return { endpoint, client: void 0, webUrl };
27876
+ }
27877
+ const session = await browserSession(
27878
+ endpoint,
27879
+ { version: options.version },
27880
+ context.debug,
27881
+ io
27882
+ );
27883
+ if (session === void 0) {
27884
+ writeLine(io.stderr, localModeLine(endpoint));
27885
+ return "local";
27886
+ }
27887
+ writeLine(io.stderr, loggedInLine(endpoint, session.me));
27888
+ webUrl = session.webUrl;
27889
+ context.credential = findCredential(io.env, endpoint);
27416
27890
  }
27417
- let tokens;
27891
+ const client = await openClient(context, io, { queue: false });
27892
+ return { endpoint, client, webUrl };
27893
+ }
27894
+ async function findOrganization(client, remote, context, io) {
27895
+ const endpoint = remote.endpoint;
27896
+ let me;
27418
27897
  try {
27419
- tokens = await signIn(terminal, { region: config2.region, ...http }, config2);
27420
- } finally {
27421
- terminal.close();
27898
+ me = await client.me();
27899
+ } catch (error62) {
27900
+ if (error62 instanceof RemoteError && error62.status === 404 && error62.serverCode !== ORG_NOT_FOUND) {
27901
+ const webUrl = await webUrlOf(remote, context, io);
27902
+ throw new ConfigError(`finish your account at ${webUrl}${LOGIN_PAGE2}`);
27903
+ }
27904
+ writeLine(
27905
+ io.stderr,
27906
+ `quibble: warning: could not read the organizations of ${endpoint}: ${describeError(error62)}`
27907
+ );
27908
+ return { org: void 0, project: void 0 };
27422
27909
  }
27423
- if (tokens.refreshToken === void 0) {
27424
- throw new RemoteError("sign-in didn't return a refresh token");
27910
+ if (me.kind === "token") {
27911
+ return {
27912
+ org: me.org,
27913
+ project: me.project === void 0 ? void 0 : parseProjectName(me.project)
27914
+ };
27425
27915
  }
27426
- const now = /* @__PURE__ */ new Date();
27427
- const me = await readProfile(
27428
- new ApiClient({
27429
- endpoint,
27430
- authorizer: bearerAuthorizer(tokens.accessToken),
27431
- ...http
27432
- }),
27433
- config2.webUrl
27434
- );
27435
- if (me.sub === void 0 || me.email === void 0) {
27436
- throw new RemoteError(
27437
- "unexpected response from the API: identity has no sub or email"
27916
+ const who = me.email ?? me.reporter;
27917
+ const slugs = me.orgs.map((entry) => entry.org).sort();
27918
+ const [only] = slugs;
27919
+ if (slugs.length === 1 && only !== void 0) return { org: only, project: void 0 };
27920
+ if (slugs.length === 0) {
27921
+ const webUrl = await webUrlOf(remote, context, io);
27922
+ throw new ConfigError(
27923
+ `${who} belongs to no organization on ${endpoint}: create one at ${webUrl}${ORGS_PAGE2}, then run quibble init again`
27438
27924
  );
27439
27925
  }
27440
- saveCredential(io.env, {
27441
- endpoint,
27442
- refreshToken: tokens.refreshToken,
27443
- accessToken: tokens.accessToken,
27444
- expiresAt: new Date(now.getTime() + tokens.expiresIn * 1e3).toISOString(),
27445
- sub: me.sub,
27446
- email: me.email
27926
+ throw new UsageError(
27927
+ `${who} belongs to several organizations on ${endpoint}: ${slugs.join(", ")}. Pass --org <slug>`
27928
+ );
27929
+ }
27930
+ async function webUrlOf(remote, context, io) {
27931
+ if (remote.webUrl !== void 0) return trimSlash2(remote.webUrl);
27932
+ const config2 = await fetchAuthConfig(remote.endpoint, {
27933
+ debug: context.debug,
27934
+ fetch: io.fetch
27447
27935
  });
27448
- const saved = saveEndpoint(context, endpoint, io.env);
27449
- if (options.json === true) {
27450
- writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
27451
- return 0;
27936
+ return config2.mode === "cognito" ? trimSlash2(config2.webUrl) : remote.endpoint;
27937
+ }
27938
+ async function checkServer(client, endpoint, context, input2) {
27939
+ const { org, project, io } = input2;
27940
+ const warn = (text) => {
27941
+ writeLine(io.stderr, `quibble: warning: ${text}`);
27942
+ };
27943
+ const found = {
27944
+ server: "found",
27945
+ line: `Project ${org}/${project} exists on ${endpoint}.`
27946
+ };
27947
+ try {
27948
+ await client.project(project);
27949
+ return found;
27950
+ } catch (error62) {
27951
+ if (!(error62 instanceof RemoteError && error62.status === 404)) {
27952
+ warn(`could not check the project on ${endpoint}: ${describeError(error62)}`);
27953
+ return { server: "unreachable" };
27954
+ }
27955
+ if (error62.serverCode === ORG_NOT_FOUND) {
27956
+ warn(`no organization ${org} on ${endpoint} for this account`);
27957
+ return { server: "no_org" };
27958
+ }
27452
27959
  }
27453
- writeLine(io.stdout, `Logged in to ${endpoint} as ${me.email}.`);
27454
- if (saved !== void 0) {
27455
- writeLine(io.stdout, `Saved ${endpoint} as your endpoint in ${saved}.`);
27960
+ const noAdmin = () => {
27961
+ warn(
27962
+ `${org} has no project ${project} on ${endpoint}: ask an admin of ${org} to create it`
27963
+ );
27964
+ return { server: "no_project" };
27965
+ };
27966
+ if (context.token !== void 0) return noAdmin();
27967
+ try {
27968
+ await client.createProject(project, input2.tags);
27969
+ return {
27970
+ server: "created",
27971
+ line: `Created the project ${org}/${project} on ${endpoint}.`
27972
+ };
27973
+ } catch (error62) {
27974
+ if (error62 instanceof RemoteError) {
27975
+ if (error62.status === 409 && error62.serverCode === CONFLICT) return found;
27976
+ if (error62.status === 403) return noAdmin();
27977
+ }
27978
+ warn(
27979
+ `could not create the project ${org}/${project} on ${endpoint}: ${describeError(error62)}`
27980
+ );
27981
+ return { server: "no_project" };
27456
27982
  }
27457
- return 0;
27458
27983
  }
27459
- function saveEndpoint(context, endpoint, env) {
27460
- if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
27461
- return void 0;
27984
+
27985
+ // src/mine.ts
27986
+ function mineMessage(env) {
27987
+ return `--mine needs a reporter: set "reporter" in ${defaultConfigPath(env)}`;
27988
+ }
27989
+ async function resolveMine(context, client, env) {
27990
+ if (client === void 0) {
27991
+ if (context.reporter === DEFAULT_REPORTER) throw new UsageError(mineMessage(env));
27992
+ return context.reporter;
27993
+ }
27994
+ return (await client.me()).reporter;
27995
+ }
27996
+
27997
+ // src/since.ts
27998
+ var SINCE_MESSAGE = "--since must be a duration like 7d, a date like 2026-09-01, or an ISO 8601 UTC timestamp";
27999
+ var DURATION_RE = /^(\d+)(m|h|d|w)$/;
28000
+ var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
28001
+ var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
28002
+ var MINUTE_MS = 6e4;
28003
+ var HOUR_MS = 60 * MINUTE_MS;
28004
+ var DAY_MS3 = 24 * HOUR_MS;
28005
+ var UNIT_MS = {
28006
+ m: MINUTE_MS,
28007
+ h: HOUR_MS,
28008
+ d: DAY_MS3,
28009
+ w: 7 * DAY_MS3
28010
+ };
28011
+ var EXPIRES_MESSAGE = "--expires must be a duration like 30d, a date like 2026-12-01, or an ISO 8601 UTC timestamp";
28012
+ function reject(message = SINCE_MESSAGE) {
28013
+ throw new UsageError(message);
28014
+ }
28015
+ function iso(at) {
28016
+ if (Number.isNaN(at.getTime())) reject();
28017
+ return at.toISOString();
28018
+ }
28019
+ function durationMs(digits, unit) {
28020
+ if (digits.startsWith("0")) return void 0;
28021
+ const step = UNIT_MS[unit];
28022
+ return step === void 0 ? void 0 : Number(digits) * step;
28023
+ }
28024
+ function fromDuration(digits, unit, now) {
28025
+ const span = durationMs(digits, unit);
28026
+ if (span === void 0) reject();
28027
+ return iso(new Date(now.getTime() - span));
28028
+ }
28029
+ function fromDate(value) {
28030
+ const at = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
28031
+ const stamp2 = iso(at);
28032
+ if (!stamp2.startsWith(value)) reject();
28033
+ return stamp2;
28034
+ }
28035
+ function parseSince(text, now) {
28036
+ const value = text.trim();
28037
+ const duration3 = DURATION_RE.exec(value);
28038
+ if (duration3 !== null) {
28039
+ return fromDuration(duration3[1] ?? "", duration3[2] ?? "", now);
27462
28040
  }
27463
- return rememberEndpoint(env, endpoint);
28041
+ if (DATE_RE.test(value)) return fromDate(value);
28042
+ if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
28043
+ return reject();
27464
28044
  }
27465
- async function readProfile(client, webUrl) {
28045
+ function parseExpires(text, now) {
28046
+ const value = text.trim();
28047
+ const duration3 = DURATION_RE.exec(value);
28048
+ let stamp2;
27466
28049
  try {
27467
- return await client.me();
27468
- } catch (error62) {
27469
- if (error62 instanceof RemoteError && NO_PROFILE_STATUSES.has(error62.status ?? 0)) {
27470
- throw new ConfigError(`finish your account at ${trimSlash2(webUrl)}${LOGIN_PAGE}`);
28050
+ if (duration3 !== null) {
28051
+ const span = durationMs(duration3[1] ?? "", duration3[2] ?? "");
28052
+ if (span === void 0) reject(EXPIRES_MESSAGE);
28053
+ stamp2 = iso(new Date(now.getTime() + span));
28054
+ } else if (DATE_RE.test(value)) {
28055
+ stamp2 = fromDate(value);
28056
+ } else if (TIMESTAMP_RE.test(value)) {
28057
+ stamp2 = iso(new Date(value));
28058
+ } else {
28059
+ return reject(EXPIRES_MESSAGE);
27471
28060
  }
27472
- throw error62;
28061
+ } catch {
28062
+ return reject(EXPIRES_MESSAGE);
27473
28063
  }
28064
+ return stamp2;
27474
28065
  }
27475
- async function runLogout(context, io) {
27476
- requireServer("logout", context);
27477
- const endpoint = await resolveEndpoint(context, io);
27478
- const credential = findCredential(io.env, endpoint);
27479
- if (credential === void 0) {
27480
- writeLine(io.stdout, `Not logged in to ${endpoint}.`);
27481
- return 0;
28066
+
28067
+ // src/scope.ts
28068
+ function resolveScope(options, context, now = /* @__PURE__ */ new Date()) {
28069
+ if (options.project !== void 0 && options.all === true) {
28070
+ throw new UsageError("--project and --all cannot be used together");
27482
28071
  }
27483
- const http = { debug: context.debug, fetch: io.fetch };
27484
- if (!await revoke(endpoint, credential.refreshToken, http)) {
27485
- writeLine(
27486
- io.stderr,
27487
- `quibble: warning: could not revoke the refresh token at ${endpoint}`
27488
- );
28072
+ if (options.open === true && options.status !== void 0) {
28073
+ throw new UsageError("--open and --status cannot be used together");
27489
28074
  }
27490
- removeCredential(io.env, endpoint);
27491
- writeLine(io.stdout, `Logged out of ${endpoint}.`);
27492
- return 0;
28075
+ const project = options.project ?? (options.all === true ? void 0 : context.project);
28076
+ const status = options.open === true ? "open" : options.status;
28077
+ return {
28078
+ // No list reads two organizations, because the path names one.
28079
+ org: requireOrg(context),
28080
+ ...project === void 0 ? {} : { project },
28081
+ ...status === void 0 ? {} : { status },
28082
+ ...options.since === void 0 ? {} : { since: parseSince(options.since, now) }
28083
+ };
27493
28084
  }
27494
- async function revoke(endpoint, refreshToken, http) {
27495
- try {
27496
- const config2 = await fetchAuthConfig(endpoint, http);
27497
- if (config2.mode === "local") return true;
27498
- await revokeToken(
27499
- { region: config2.region, ...http },
27500
- { clientId: config2.cliClientId, token: refreshToken }
27501
- );
27502
- return true;
27503
- } catch {
27504
- return false;
28085
+ function tagFilter(tags) {
28086
+ return tags === void 0 || tags.length === 0 ? {} : { tags: [...tags] };
28087
+ }
28088
+
28089
+ // src/commands/list.ts
28090
+ var COLUMNS = [
28091
+ "id",
28092
+ "created",
28093
+ "severity",
28094
+ "status",
28095
+ "project",
28096
+ "reporter",
28097
+ "tags",
28098
+ "group",
28099
+ "summary"
28100
+ ];
28101
+ var REPORTER_WIDTH = 20;
28102
+ var TAGS_WIDTH = 24;
28103
+ var SUMMARY_MAX = 60;
28104
+ var RECURRED_MARK = "\u21BB";
28105
+ function cut3(text, width) {
28106
+ if (text.length <= width) return text;
28107
+ return `${text.slice(0, width - 3)}...`;
28108
+ }
28109
+ function summaryOf(complaint) {
28110
+ return cut3(printable(summarize(complaint.message)), SUMMARY_MAX);
28111
+ }
28112
+ function limitNote(count, limit, more) {
28113
+ if (limit >= MAX_LIMIT) {
28114
+ return count >= MAX_LIMIT ? `Showing the newest ${MAX_LIMIT} complaints: older ones may exist, narrow the filters to see them.` : void 0;
27505
28115
  }
28116
+ return more ? `Showing the newest ${limit} complaints: pass --limit to see more (up to ${MAX_LIMIT}).` : void 0;
27506
28117
  }
27507
- async function runWhoami(options, context, io) {
27508
- if (context.mode === "local") {
27509
- checkActAs(context);
27510
- writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
28118
+ var BUDGET_NOTE = "The server stopped searching early, so older matches may exist: narrow the filters or pass --since.";
28119
+ async function runList(options, context, io) {
28120
+ if (options.ids === true && options.json === true) {
28121
+ throw new UsageError("--ids and --json cannot be used together");
28122
+ }
28123
+ const { store, client } = await openBackend(context, io);
28124
+ const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
28125
+ const filter = parseListFilter({
28126
+ ...resolveScope(options, context),
28127
+ ...options.severity === void 0 ? {} : { severity: options.severity },
28128
+ ...tagFilter(options.tag),
28129
+ ...options.search === void 0 ? {} : { q: options.search },
28130
+ ...reporter === void 0 ? {} : { reporter },
28131
+ ...options.limit === void 0 ? {} : { limit: options.limit }
28132
+ });
28133
+ const limit = filter.limit ?? DEFAULT_LIMIT;
28134
+ const page = await listComplaintPage(store, {
28135
+ ...filter,
28136
+ limit: Math.min(limit + 1, MAX_LIMIT)
28137
+ });
28138
+ const complaints = page.complaints.slice(0, limit);
28139
+ if (options.json === true) {
28140
+ writeJson(io.stdout, complaints);
28141
+ return 0;
28142
+ }
28143
+ const note = limitNote(complaints.length, limit, page.complaints.length > limit);
28144
+ if (note !== void 0) writeLine(io.stderr, note);
28145
+ if (page.truncated) writeLine(io.stderr, BUDGET_NOTE);
28146
+ if (options.ids === true) {
28147
+ for (const complaint of complaints) writeLine(io.stdout, complaint.id);
28148
+ return 0;
28149
+ }
28150
+ if (complaints.length === 0) {
28151
+ writeLine(io.stdout, "No complaints found.");
27511
28152
  return 0;
27512
28153
  }
28154
+ const lines = options.ungroup === true ? formatTable(complaints.map((complaint) => ({ complaint }))) : formatTable(
28155
+ groupComplaints(complaints).map((group) => ({
28156
+ complaint: group.head,
28157
+ group
28158
+ }))
28159
+ );
28160
+ for (const line of lines) writeLine(io.stdout, line);
28161
+ return 0;
28162
+ }
28163
+ function reporterOf(complaint) {
28164
+ return cut3(printable(complaint.reporterName ?? complaint.reporter), REPORTER_WIDTH);
28165
+ }
28166
+ function groupCell(group) {
28167
+ const count = group.count > 1 ? `\xD7${group.count}` : "";
28168
+ return `${count}${group.recurred ? RECURRED_MARK : ""}`;
28169
+ }
28170
+ function formatTable(rows2) {
28171
+ const grouped = rows2.some((row) => row.group !== void 0);
28172
+ const columns = COLUMNS.filter((name) => grouped || name !== "group");
28173
+ const cells = rows2.map(({ complaint, group }) => {
28174
+ const values = {
28175
+ id: complaint.id.slice(0, 8),
28176
+ created: complaint.createdAt.slice(0, 16),
28177
+ severity: complaint.severity,
28178
+ status: complaint.status,
28179
+ project: complaint.project,
28180
+ reporter: reporterOf(complaint),
28181
+ tags: cut3(printable(complaint.tags.join(",")), TAGS_WIDTH),
28182
+ group: group === void 0 ? "" : groupCell(group),
28183
+ summary: summaryOf(complaint)
28184
+ };
28185
+ return columns.map((name) => values[name]);
28186
+ });
28187
+ const widths = columns.map(
28188
+ (name, column) => cells.reduce(
28189
+ (width, row) => Math.max(width, (row[column] ?? "").length),
28190
+ name.length
28191
+ )
28192
+ );
28193
+ const format = (line) => line.map((cell2, column) => cell2.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
28194
+ return [format(columns), ...cells.map(format)];
28195
+ }
28196
+
28197
+ // src/commands/orgs.ts
28198
+ var ORGS_PAGE3 = "/orgs";
28199
+ var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
28200
+ async function runOrgs(options, context, io) {
28201
+ requireServer("orgs", context);
27513
28202
  const client = await openClient(context, io);
27514
28203
  const endpoint = requireEndpoint(context);
27515
- const me = await client.me();
27516
- const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
28204
+ const answer2 = await client.orgs();
27517
28205
  if (options.json === true) {
27518
- writeJson(io.stdout, {
27519
- ...me.body,
27520
- endpoint,
27521
- ...org === void 0 ? {} : { org: org.body }
27522
- });
28206
+ writeJson(io.stdout, answer2.body);
27523
28207
  return 0;
27524
28208
  }
27525
- const lines = whoamiLines(endpoint, me, org);
27526
- if (me.kind !== "token" && me.orgs.length === 0) {
28209
+ if (answer2.orgs.length === 0) {
27527
28210
  const config2 = await fetchAuthConfig(endpoint, {
27528
28211
  debug: context.debug,
27529
- fetch: io.fetch
28212
+ fetch: io.fetch,
28213
+ timeoutMs: context.timeoutMs
27530
28214
  });
27531
28215
  const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
27532
- lines.push("", `Create one at ${webUrl}${ORGS_PAGE2}.`);
28216
+ writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE3}.`);
28217
+ return 0;
27533
28218
  }
27534
- for (const line of lines) writeLine(io.stdout, line);
28219
+ for (const line of formatOrgs(answer2.orgs)) writeLine(io.stdout, line);
27535
28220
  return 0;
27536
28221
  }
27537
- function rows(pairs) {
27538
- const width = Math.max(...pairs.map(([key]) => key.length));
27539
- return pairs.map(([key, value]) => `${key.padEnd(width)} ${value}`);
27540
- }
27541
- function block(entries) {
27542
- if (entries.length === 0) return [" none"];
27543
- const width = Math.max(...entries.map(([key]) => key.length));
27544
- return entries.map(([key, value]) => ` ${key.padEnd(width)} ${value}`);
27545
- }
27546
- function whoamiLines(endpoint, me, org) {
27547
- if (me.kind === "token") {
27548
- return rows([
27549
- ["endpoint", endpoint],
27550
- ["token", me.name ?? me.reporter],
27551
- ["org", me.org ?? ""],
27552
- ["project", me.project ?? ""],
27553
- ["role", me.role ?? "filer"]
27554
- ]);
27555
- }
27556
- const lines = rows([
27557
- ["endpoint", endpoint],
27558
- ["email", me.email ?? ""],
27559
- ["name", me.name ?? ""],
27560
- ["sub", me.sub ?? ""],
27561
- ["platform", me.platform ? "yes" : "no"]
28222
+ function formatOrgs(orgs) {
28223
+ const rows2 = [...orgs].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((entry) => [
28224
+ entry.org,
28225
+ entry.name,
28226
+ entry.role,
28227
+ entry.openCount === void 0 ? "" : String(entry.openCount)
27562
28228
  ]);
27563
- lines.push("", "Organizations");
27564
- lines.push(...block(me.orgs.map((entry) => [entry.org, entry.role])));
27565
- if (org === void 0) return lines;
27566
- lines.push("", `Roles in ${org.org}`);
27567
- lines.push(...block(org.roles.map((entry) => [entry.project, entry.role])));
27568
- return lines;
28229
+ const widths = COLUMNS2.map(
28230
+ (name, column) => rows2.reduce((width, row) => Math.max(width, (row[column] ?? "").length), name.length)
28231
+ );
28232
+ const format = (cells) => cells.map(
28233
+ (cell2, column) => column === COLUMNS2.length - 1 ? cell2.padStart(widths[column] ?? 0) : cell2.padEnd(widths[column] ?? 0)
28234
+ ).join(" ").trimEnd();
28235
+ return [format(COLUMNS2), ...rows2.map(format)];
27569
28236
  }
27570
28237
 
27571
28238
  // src/commands/project.ts
@@ -28334,11 +29001,14 @@ function buildProgram(io, setCode) {
28334
29001
  }).exitOverride();
28335
29002
  withGlobalOptions(program2);
28336
29003
  withGlobalOptions(
28337
- 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")
29004
+ program2.command("init").description("write a .quibblerc in the current directory").option(
29005
+ "--project <name>",
29006
+ "project name (default: the git remote origin, else the directory name)"
29007
+ ).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("--no-login", "do not log in through the browser without a credential").option("--force", "overwrite an existing .quibblerc or skill file")
28338
29008
  ).action(async function() {
28339
29009
  const options = { ...this.opts(), ...globals(program2, this) };
28340
29010
  rejectEmptyOptions(options);
28341
- setCode(await runInit(options, io));
29011
+ setCode(await runInit({ ...options, version: packageVersion() }, io));
28342
29012
  });
28343
29013
  withGlobalOptions(
28344
29014
  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")
@@ -28487,11 +29157,17 @@ function buildProgram(io, setCode) {
28487
29157
  setCode(await runOrgs(options, contextFor(options, io), io));
28488
29158
  });
28489
29159
  withGlobalOptions(
28490
- program2.command("login").description("log in to the endpoint")
29160
+ 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")
28491
29161
  ).action(async function() {
28492
29162
  const options = { ...this.opts(), ...globals(program2, this) };
28493
29163
  rejectEmptyOptions(options);
28494
- setCode(await runLogin(options, contextFor(options, io), io));
29164
+ setCode(
29165
+ await runLogin(
29166
+ { ...options, version: packageVersion() },
29167
+ contextFor(options, io),
29168
+ io
29169
+ )
29170
+ );
28495
29171
  });
28496
29172
  withGlobalOptions(
28497
29173
  program2.command("logout").description("log out of the endpoint")