letthemquibble 1.1.0 → 1.2.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 +55 -14
  2. package/dist/quibble.js +801 -271
  3. package/package.json +2 -1
package/dist/quibble.js CHANGED
@@ -51,11 +51,11 @@ var require_error = __commonJS({
51
51
  * @param {string} code an id string representing the error
52
52
  * @param {string} message human-readable description of the error
53
53
  */
54
- constructor(exitCode, code, message) {
54
+ constructor(exitCode, code2, message) {
55
55
  super(message);
56
56
  Error.captureStackTrace(this, this.constructor);
57
57
  this.name = this.constructor.name;
58
- this.code = code;
58
+ this.code = code2;
59
59
  this.exitCode = exitCode;
60
60
  this.nestedError = void 0;
61
61
  }
@@ -1649,9 +1649,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1649
1649
  * @return never
1650
1650
  * @private
1651
1651
  */
1652
- _exit(exitCode, code, message) {
1652
+ _exit(exitCode, code2, message) {
1653
1653
  if (this._exitCallback) {
1654
- this._exitCallback(new CommanderError2(exitCode, code, message));
1654
+ this._exitCallback(new CommanderError2(exitCode, code2, message));
1655
1655
  }
1656
1656
  process3.exit(exitCode);
1657
1657
  }
@@ -2289,14 +2289,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
2289
2289
  });
2290
2290
  }
2291
2291
  const exitCallback = this._exitCallback;
2292
- proc.on("close", (code) => {
2293
- code = code ?? 1;
2292
+ proc.on("close", (code2) => {
2293
+ code2 = code2 ?? 1;
2294
2294
  if (!exitCallback) {
2295
- process3.exit(code);
2295
+ process3.exit(code2);
2296
2296
  } else {
2297
2297
  exitCallback(
2298
2298
  new CommanderError2(
2299
- code,
2299
+ code2,
2300
2300
  "commander.executeSubCommandAsync",
2301
2301
  "(close)"
2302
2302
  )
@@ -2803,8 +2803,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
2803
2803
  }
2804
2804
  const config2 = errorOptions || {};
2805
2805
  const exitCode = config2.exitCode || 1;
2806
- const code = config2.code || "commander.error";
2807
- this._exit(exitCode, code, message);
2806
+ const code2 = config2.code || "commander.error";
2807
+ this._exit(exitCode, code2, message);
2808
2808
  }
2809
2809
  /**
2810
2810
  * Apply any option related environment variables, if option does
@@ -6578,12 +6578,12 @@ function isIso7064Mod97(iban3) {
6578
6578
  let remainder = 0;
6579
6579
  const len = iban3.length;
6580
6580
  for (let i = 4; i < len; i++) {
6581
- const code = iban3.charCodeAt(i);
6582
- remainder = (code >= 65 ? remainder * 100 + (code - 55) : remainder * 10 + (code - 48)) % 97;
6581
+ const code2 = iban3.charCodeAt(i);
6582
+ remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
6583
6583
  }
6584
6584
  for (let i = 0; i < 4; i++) {
6585
- const code = iban3.charCodeAt(i);
6586
- remainder = (code >= 65 ? remainder * 100 + (code - 55) : remainder * 10 + (code - 48)) % 97;
6585
+ const code2 = iban3.charCodeAt(i);
6586
+ remainder = (code2 >= 65 ? remainder * 100 + (code2 - 55) : remainder * 10 + (code2 - 48)) % 97;
6587
6587
  }
6588
6588
  return remainder === 1;
6589
6589
  }
@@ -7013,7 +7013,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
7013
7013
  }
7014
7014
  return propValues;
7015
7015
  });
7016
- const isObject2 = isObject;
7016
+ const isObject3 = isObject;
7017
7017
  const catchall = def.catchall;
7018
7018
  let value;
7019
7019
  const memo2 = globalConfig.memoizer;
@@ -7021,7 +7021,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
7021
7021
  inst._zod.parse = (payload, ctx) => {
7022
7022
  value ?? (value = _normalized.value);
7023
7023
  const input2 = payload.value;
7024
- if (!isObject2(input2)) {
7024
+ if (!isObject3(input2)) {
7025
7025
  payload.issues.push({
7026
7026
  expected: "object",
7027
7027
  code: "invalid_type",
@@ -7157,7 +7157,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
7157
7157
  return doc.compile();
7158
7158
  };
7159
7159
  let fastpass;
7160
- const isObject2 = isObject;
7160
+ const isObject3 = isObject;
7161
7161
  const jit = !globalConfig.jitless;
7162
7162
  const allowsEval2 = allowsEval;
7163
7163
  const fastEnabled = jit && allowsEval2.value;
@@ -7166,7 +7166,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
7166
7166
  inst._zod.parse = (payload, ctx) => {
7167
7167
  value ?? (value = _normalized.value);
7168
7168
  const input2 = payload.value;
7169
- if (!isObject2(input2)) {
7169
+ if (!isObject3(input2)) {
7170
7170
  payload.issues.push({
7171
7171
  expected: "object",
7172
7172
  code: "invalid_type",
@@ -16501,12 +16501,12 @@ function compileFn(schema, options) {
16501
16501
  doc.write(outputAccessor === null ? `return true;` : `return ${outputAccessor};`);
16502
16502
  const constantNames = ["INVALID", ...ctx.constants.keys()];
16503
16503
  const constantValues = [INVALID, ...ctx.constants.values()];
16504
- const code = doc.content.join("\n");
16504
+ const code2 = doc.content.join("\n");
16505
16505
  const fullCode = options?.debug ? constantNames.length > 0 ? `// Constants: ${constantNames.join(", ")}
16506
- ${code}` : code : "";
16506
+ ${code2}` : code2 : "";
16507
16507
  const F = Function;
16508
16508
  const factoryCode = `return (input) => {
16509
- ${code}
16509
+ ${code2}
16510
16510
  }`;
16511
16511
  let fn;
16512
16512
  try {
@@ -19432,8 +19432,8 @@ function finalize(ctx, schema) {
19432
19432
  compactTypeUnion(entry[1].def ?? entry[1].schema);
19433
19433
  }
19434
19434
  }
19435
- for (const rewrite of ctx.deferred)
19436
- rewrite();
19435
+ for (const rewrite2 of ctx.deferred)
19436
+ rewrite2();
19437
19437
  if (ctx.intersections.length) {
19438
19438
  const carriers = /* @__PURE__ */ new Map();
19439
19439
  for (const seen of ctx.seen.values()) {
@@ -23222,7 +23222,7 @@ var RESERVED_SLUGS = [
23222
23222
  ];
23223
23223
  var LOCAL_ORG = "local";
23224
23224
  var DISPLAY_NAME_MAX = 64;
23225
- var PROJECT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
23225
+ var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
23226
23226
  var DEFAULT_LIMIT = 50;
23227
23227
  var MAX_LIMIT = 200;
23228
23228
  var DEFAULT_SEVERITY = "medium";
@@ -23238,9 +23238,9 @@ var IDEMPOTENCY_KEY_MAX = 128;
23238
23238
  var QuibbleError = class extends Error {
23239
23239
  /** Stable machine-readable code, also used by the API error body. */
23240
23240
  code;
23241
- constructor(code, message, options) {
23241
+ constructor(code2, message, options) {
23242
23242
  super(message, options);
23243
- this.code = code;
23243
+ this.code = code2;
23244
23244
  this.name = new.target.name;
23245
23245
  }
23246
23246
  };
@@ -23476,7 +23476,10 @@ var timestampSchema = external_exports.string({ message: TIMESTAMP_MESSAGE }).re
23476
23476
  function toMilliseconds(timestamp) {
23477
23477
  return new Date(timestamp).toISOString();
23478
23478
  }
23479
- var projectNameSchema = external_exports.string({ message: PROJECT_MESSAGE }).regex(PROJECT_NAME_RE, { message: PROJECT_MESSAGE });
23479
+ function foldProjectName(name) {
23480
+ return name.replace(/[A-Z]+/g, (letters) => letters.toLowerCase());
23481
+ }
23482
+ var projectNameSchema = external_exports.string({ message: PROJECT_MESSAGE }).overwrite(foldProjectName).regex(PROJECT_NAME_RE, { message: PROJECT_MESSAGE });
23480
23483
  var orgSlugSchema = external_exports.string({ message: SLUG_MESSAGE }).refine((value) => ORG_SLUG_RE.test(value) && !value.includes("--"), {
23481
23484
  message: SLUG_MESSAGE
23482
23485
  });
@@ -24046,6 +24049,34 @@ function listQueryOf(filter) {
24046
24049
  return pairs;
24047
24050
  }
24048
24051
 
24052
+ // ../core/dist/endpoint.js
24053
+ var DEFAULT_ENDPOINT = "https://api.letthemquibble.com";
24054
+ var DISCOVERY_PATH = "/.well-known/quibble.json";
24055
+ var LOCAL_ENDPOINT = "local";
24056
+ var LOOPBACK_HOSTS = ["localhost", "127.0.0.1"];
24057
+ function trimSlash(url2) {
24058
+ return url2.replace(/\/+$/, "");
24059
+ }
24060
+ function normalizeEndpoint(url2) {
24061
+ const text = trimSlash(url2.trim());
24062
+ try {
24063
+ const parsed = new URL(text);
24064
+ return `${parsed.protocol}//${parsed.host}${trimSlash(parsed.pathname)}`;
24065
+ } catch {
24066
+ return text;
24067
+ }
24068
+ }
24069
+ function isDefaultEndpoint(url2) {
24070
+ return normalizeEndpoint(url2) === DEFAULT_ENDPOINT;
24071
+ }
24072
+ function isLoopbackUrl(url2) {
24073
+ try {
24074
+ return LOOPBACK_HOSTS.includes(new URL(url2).hostname);
24075
+ } catch {
24076
+ return false;
24077
+ }
24078
+ }
24079
+
24049
24080
  // ../core/dist/service.js
24050
24081
  import { createHash, randomUUID } from "node:crypto";
24051
24082
  function applyTagRules(tags, allowedTags) {
@@ -24459,7 +24490,7 @@ function defaultConfigPath(env = process.env) {
24459
24490
  }
24460
24491
 
24461
24492
  // src/config.ts
24462
- import { readFileSync as readFileSync2 } from "node:fs";
24493
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
24463
24494
  import { dirname as dirname3, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
24464
24495
 
24465
24496
  // src/credentials.ts
@@ -24473,6 +24504,92 @@ import {
24473
24504
  writeFileSync
24474
24505
  } from "node:fs";
24475
24506
  import { dirname as dirname2, join as join2 } from "node:path";
24507
+ var CREDENTIALS_FILENAME = "credentials.json";
24508
+ var CREDENTIALS_MODE = 384;
24509
+ var HOME_MODE = 448;
24510
+ function defaultCredentialsPath(env) {
24511
+ return join2(defaultQuibbleHome(env), CREDENTIALS_FILENAME);
24512
+ }
24513
+ function readEntry(value) {
24514
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
24515
+ return void 0;
24516
+ }
24517
+ const raw = value;
24518
+ const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
24519
+ for (const key of keys) {
24520
+ if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
24521
+ }
24522
+ return {
24523
+ endpoint: raw["endpoint"],
24524
+ refreshToken: raw["refreshToken"],
24525
+ accessToken: raw["accessToken"],
24526
+ expiresAt: raw["expiresAt"],
24527
+ sub: raw["sub"],
24528
+ email: raw["email"]
24529
+ };
24530
+ }
24531
+ function readCredentials(env) {
24532
+ const path = defaultCredentialsPath(env);
24533
+ let text;
24534
+ try {
24535
+ text = readFileSync(path, "utf8");
24536
+ } catch (error62) {
24537
+ if (isNotFound(error62)) return {};
24538
+ throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
24539
+ }
24540
+ let parsed;
24541
+ try {
24542
+ parsed = JSON.parse(text);
24543
+ } catch (error62) {
24544
+ throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
24545
+ }
24546
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24547
+ throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
24548
+ }
24549
+ const entries = {};
24550
+ for (const [key, value] of Object.entries(parsed)) {
24551
+ const entry = readEntry(value);
24552
+ if (entry !== void 0) entries[key] = entry;
24553
+ }
24554
+ return entries;
24555
+ }
24556
+ function findCredential(env, endpoint) {
24557
+ return readCredentials(env)[normalizeEndpoint(endpoint)];
24558
+ }
24559
+ function writeCredentials(env, entries) {
24560
+ const path = defaultCredentialsPath(env);
24561
+ mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
24562
+ const temp = join2(
24563
+ dirname2(path),
24564
+ `.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
24565
+ );
24566
+ try {
24567
+ writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
24568
+ `, {
24569
+ encoding: "utf8",
24570
+ mode: CREDENTIALS_MODE
24571
+ });
24572
+ chmodSync(temp, CREDENTIALS_MODE);
24573
+ renameSync(temp, path);
24574
+ } catch (error62) {
24575
+ rmSync(temp, { force: true });
24576
+ throw error62;
24577
+ }
24578
+ }
24579
+ function saveCredential(env, credential) {
24580
+ const key = normalizeEndpoint(credential.endpoint);
24581
+ const stored = { ...credential, endpoint: key };
24582
+ writeCredentials(env, { ...readCredentials(env), [key]: stored });
24583
+ return stored;
24584
+ }
24585
+ function removeCredential(env, endpoint) {
24586
+ const entries = readCredentials(env);
24587
+ const key = normalizeEndpoint(endpoint);
24588
+ if (entries[key] === void 0) return false;
24589
+ delete entries[key];
24590
+ writeCredentials(env, entries);
24591
+ return true;
24592
+ }
24476
24593
 
24477
24594
  // src/errors.ts
24478
24595
  var AMBIGUOUS_ID = "ambiguous_id";
@@ -24525,7 +24642,7 @@ var DEBUG_ENV = "QUIBBLE_DEBUG";
24525
24642
  function debugEnabled(env) {
24526
24643
  return env[DEBUG_ENV] === "1";
24527
24644
  }
24528
- function trimSlash(url2) {
24645
+ function trimSlash2(url2) {
24529
24646
  return url2.replace(/\/+$/, "");
24530
24647
  }
24531
24648
  function originOf(url2) {
@@ -24629,103 +24746,6 @@ function remoteFailure(site, response, text) {
24629
24746
  });
24630
24747
  }
24631
24748
 
24632
- // src/credentials.ts
24633
- var CREDENTIALS_FILENAME = "credentials.json";
24634
- var CREDENTIALS_MODE = 384;
24635
- var HOME_MODE = 448;
24636
- function defaultCredentialsPath(env) {
24637
- return join2(defaultQuibbleHome(env), CREDENTIALS_FILENAME);
24638
- }
24639
- function normalizeEndpoint(endpoint) {
24640
- const text = trimSlash(endpoint.trim());
24641
- try {
24642
- const url2 = new URL(text);
24643
- return `${url2.protocol}//${url2.host}${trimSlash(url2.pathname)}`;
24644
- } catch {
24645
- return text;
24646
- }
24647
- }
24648
- function readEntry(value) {
24649
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
24650
- return void 0;
24651
- }
24652
- const raw = value;
24653
- const keys = ["endpoint", "refreshToken", "accessToken", "expiresAt", "sub", "email"];
24654
- for (const key of keys) {
24655
- if (typeof raw[key] !== "string" || raw[key] === "") return void 0;
24656
- }
24657
- return {
24658
- endpoint: raw["endpoint"],
24659
- refreshToken: raw["refreshToken"],
24660
- accessToken: raw["accessToken"],
24661
- expiresAt: raw["expiresAt"],
24662
- sub: raw["sub"],
24663
- email: raw["email"]
24664
- };
24665
- }
24666
- function readCredentials(env) {
24667
- const path = defaultCredentialsPath(env);
24668
- let text;
24669
- try {
24670
- text = readFileSync(path, "utf8");
24671
- } catch (error62) {
24672
- if (isNotFound(error62)) return {};
24673
- throw new ConfigError(`cannot read ${path}`, path, { cause: error62 });
24674
- }
24675
- let parsed;
24676
- try {
24677
- parsed = JSON.parse(text);
24678
- } catch (error62) {
24679
- throw new ConfigError(`invalid JSON in ${path}`, path, { cause: error62 });
24680
- }
24681
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24682
- throw new ConfigError(`invalid credentials in ${path}: must be a JSON object`, path);
24683
- }
24684
- const entries = {};
24685
- for (const [key, value] of Object.entries(parsed)) {
24686
- const entry = readEntry(value);
24687
- if (entry !== void 0) entries[key] = entry;
24688
- }
24689
- return entries;
24690
- }
24691
- function findCredential(env, endpoint) {
24692
- return readCredentials(env)[normalizeEndpoint(endpoint)];
24693
- }
24694
- function writeCredentials(env, entries) {
24695
- const path = defaultCredentialsPath(env);
24696
- mkdirSync(dirname2(path), { recursive: true, mode: HOME_MODE });
24697
- const temp = join2(
24698
- dirname2(path),
24699
- `.${CREDENTIALS_FILENAME}.${randomBytes(6).toString("hex")}`
24700
- );
24701
- try {
24702
- writeFileSync(temp, `${JSON.stringify(entries, null, 2)}
24703
- `, {
24704
- encoding: "utf8",
24705
- mode: CREDENTIALS_MODE
24706
- });
24707
- chmodSync(temp, CREDENTIALS_MODE);
24708
- renameSync(temp, path);
24709
- } catch (error62) {
24710
- rmSync(temp, { force: true });
24711
- throw error62;
24712
- }
24713
- }
24714
- function saveCredential(env, credential) {
24715
- const key = normalizeEndpoint(credential.endpoint);
24716
- const stored = { ...credential, endpoint: key };
24717
- writeCredentials(env, { ...readCredentials(env), [key]: stored });
24718
- return stored;
24719
- }
24720
- function removeCredential(env, endpoint) {
24721
- const entries = readCredentials(env);
24722
- const key = normalizeEndpoint(endpoint);
24723
- if (entries[key] === void 0) return false;
24724
- delete entries[key];
24725
- writeCredentials(env, entries);
24726
- return true;
24727
- }
24728
-
24729
24749
  // src/cognito.ts
24730
24750
  var TARGET_PREFIX = "AWSCognitoIdentityProviderService.";
24731
24751
  var CONTENT_TYPE = "application/x-amz-json-1.1";
@@ -24925,9 +24945,8 @@ function actAsRefused(sub, endpoint) {
24925
24945
  function clockOf(options) {
24926
24946
  return options.now ?? (() => /* @__PURE__ */ new Date());
24927
24947
  }
24928
- var LOCAL_HOSTS = ["localhost", "127.0.0.1"];
24929
24948
  async function fetchAuthConfig(endpoint, options = {}) {
24930
- const url2 = `${trimSlash(endpoint)}${AUTH_CONFIG_PATH}`;
24949
+ const url2 = `${trimSlash2(endpoint)}${AUTH_CONFIG_PATH}`;
24931
24950
  const site = { method: "GET", url: url2, debug: options.debug };
24932
24951
  const response = await send(url2, { method: "GET", ...options });
24933
24952
  const text = await readText(response, site);
@@ -24956,13 +24975,6 @@ function bearerAuthorizer(token) {
24956
24975
  };
24957
24976
  }
24958
24977
  var DEV_TOKEN = "dev";
24959
- function isLoopback(endpoint) {
24960
- try {
24961
- return LOCAL_HOSTS.includes(new URL(endpoint).hostname);
24962
- } catch {
24963
- return false;
24964
- }
24965
- }
24966
24978
  var DevAuthorizer = class {
24967
24979
  actAs;
24968
24980
  #endpoint;
@@ -25067,12 +25079,97 @@ function createAuthorizer(endpoint, options) {
25067
25079
  return new SessionAuthorizer(endpoint, options);
25068
25080
  }
25069
25081
 
25082
+ // src/endpoint.ts
25083
+ var DISCOVERY_TIMEOUT_MS = 5e3;
25084
+ var SCHEMES = ["http:", "https:"];
25085
+ function parseEndpoint(value, from) {
25086
+ const text = value.trim();
25087
+ if (text.toLowerCase() === LOCAL_ENDPOINT) return { kind: "local" };
25088
+ const invalid = new ConfigError(
25089
+ `invalid endpoint "${text}" from ${from}: pass an https URL, a host, or local`
25090
+ );
25091
+ let url2 = text;
25092
+ if (!text.includes("://")) {
25093
+ url2 = isLoopbackUrl(`http://${text}`) ? `http://${text}` : `https://${text}`;
25094
+ }
25095
+ let parsed;
25096
+ try {
25097
+ parsed = new URL(url2);
25098
+ } catch {
25099
+ throw invalid;
25100
+ }
25101
+ if (!SCHEMES.includes(parsed.protocol) || parsed.hostname === "") throw invalid;
25102
+ return { kind: "url", url: normalizeEndpoint(url2) };
25103
+ }
25104
+ var discoveries = /* @__PURE__ */ new Map();
25105
+ function resetDiscovery() {
25106
+ discoveries.clear();
25107
+ }
25108
+ function discoverApiUrl(url2, options = {}) {
25109
+ const key = normalizeEndpoint(url2);
25110
+ if (isDefaultEndpoint(key)) return Promise.resolve(DEFAULT_ENDPOINT);
25111
+ const known = discoveries.get(key);
25112
+ if (known !== void 0) return known;
25113
+ const found = lookup(key, options).then((api) => {
25114
+ if (!discoveries.has(api)) discoveries.set(api, Promise.resolve(api));
25115
+ return api;
25116
+ });
25117
+ discoveries.set(key, found);
25118
+ return found;
25119
+ }
25120
+ async function lookup(url2, options) {
25121
+ const target = `${url2}${DISCOVERY_PATH}`;
25122
+ try {
25123
+ const response = await send(target, {
25124
+ method: "GET",
25125
+ headers: { accept: "application/json" },
25126
+ fetch: options.fetch,
25127
+ debug: options.debug,
25128
+ timeoutMs: options.timeoutMs ?? DISCOVERY_TIMEOUT_MS
25129
+ });
25130
+ const text = await readText(response, { method: "GET", url: target });
25131
+ if (!response.ok) return url2;
25132
+ const api = stringOf(asRecord(parseQuietly(text)), "api");
25133
+ if (api === void 0) return url2;
25134
+ const parsed = new URL(api.trim());
25135
+ if (!SCHEMES.includes(parsed.protocol)) return url2;
25136
+ return normalizeEndpoint(api);
25137
+ } catch {
25138
+ return url2;
25139
+ }
25140
+ }
25141
+
25070
25142
  // src/config.ts
25071
- var NO_ENDPOINT_MESSAGE = "no endpoint: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
25072
25143
  function requireEndpoint(context) {
25073
- if (context.endpoint === void 0) throw new ConfigError(NO_ENDPOINT_MESSAGE);
25144
+ if (context.endpoint === void 0) {
25145
+ throw new ConfigError(`local mode has no endpoint: ${context.localFix ?? ""}`);
25146
+ }
25074
25147
  return context.endpoint;
25075
25148
  }
25149
+ async function resolveEndpoint(context, io) {
25150
+ const endpoint = requireEndpoint(context);
25151
+ if (context.discovered) return endpoint;
25152
+ const api = await discoverApiUrl(endpoint, {
25153
+ fetch: io.fetch,
25154
+ debug: context.debug,
25155
+ timeoutMs: context.timeoutMs
25156
+ });
25157
+ context.endpoint = api;
25158
+ context.credential = findCredential(io.env, api);
25159
+ context.discovered = true;
25160
+ return api;
25161
+ }
25162
+ function foreignOrgMessage(context) {
25163
+ const slug = context.slug;
25164
+ if (context.mode !== "local" || slug === void 0 || slug === LOCAL_ORG) {
25165
+ return void 0;
25166
+ }
25167
+ return `local mode holds the organization ${LOCAL_ORG} only, not ${slug}: ${context.localFix ?? ""} to file to ${slug} on a server, or pass --org ${LOCAL_ORG}`;
25168
+ }
25169
+ function refuseForeignOrg(context) {
25170
+ const message = foreignOrgMessage(context);
25171
+ if (message !== void 0) throw new ConfigError(message);
25172
+ }
25076
25173
  var NO_ORG_MESSAGE = "no organization: pass --org <slug> or run `quibble init --org <slug>`";
25077
25174
  function requireOrg(context) {
25078
25175
  if (context.org === void 0) throw new ConfigError(NO_ORG_MESSAGE);
@@ -25196,20 +25293,76 @@ function firstValue(...values) {
25196
25293
  }
25197
25294
  return void 0;
25198
25295
  }
25296
+ function localFixOf(candidate) {
25297
+ switch (candidate.source) {
25298
+ case "--endpoint":
25299
+ return "drop --endpoint local";
25300
+ case "QUIBBLE_ENDPOINT":
25301
+ return "unset QUIBBLE_ENDPOINT";
25302
+ default:
25303
+ return `remove "endpoint": "local" from ${candidate.path ?? ""}`;
25304
+ }
25305
+ }
25306
+ function resolveEndpointSource(options, project, user) {
25307
+ if (options.local === true) {
25308
+ return {
25309
+ mode: "local",
25310
+ endpoint: void 0,
25311
+ endpointSource: "--local",
25312
+ localFix: "drop --local"
25313
+ };
25314
+ }
25315
+ const candidates2 = [
25316
+ { source: "--endpoint", value: options.endpoint },
25317
+ { source: "QUIBBLE_ENDPOINT", value: options.env["QUIBBLE_ENDPOINT"] },
25318
+ { source: ".quibblerc", value: project?.endpoint, path: project?.path },
25319
+ {
25320
+ source: "config.json",
25321
+ value: user.endpoint,
25322
+ path: defaultConfigPath(options.env)
25323
+ }
25324
+ ];
25325
+ const chosen = candidates2.find(
25326
+ (candidate) => candidate.value !== void 0 && candidate.value.trim() !== ""
25327
+ );
25328
+ if (chosen === void 0) {
25329
+ return {
25330
+ mode: "remote",
25331
+ endpoint: DEFAULT_ENDPOINT,
25332
+ endpointSource: "default",
25333
+ localFix: void 0
25334
+ };
25335
+ }
25336
+ const value = parseEndpoint(chosen.value ?? "", chosen.path ?? chosen.source);
25337
+ if (value.kind === "local") {
25338
+ return {
25339
+ mode: "local",
25340
+ endpoint: void 0,
25341
+ endpointSource: chosen.source,
25342
+ localFix: localFixOf(chosen)
25343
+ };
25344
+ }
25345
+ return {
25346
+ mode: "remote",
25347
+ endpoint: value.url,
25348
+ endpointSource: chosen.source,
25349
+ localFix: void 0
25350
+ };
25351
+ }
25199
25352
  function resolveContext(options) {
25200
25353
  const project = findProjectConfig(options.cwd);
25201
25354
  const user = loadUserConfig(options.env);
25202
- const endpoint = firstValue(
25203
- options.endpoint,
25204
- options.env["QUIBBLE_ENDPOINT"],
25205
- project?.endpoint,
25206
- user.endpoint
25355
+ const { mode, endpoint, endpointSource, localFix } = resolveEndpointSource(
25356
+ options,
25357
+ project,
25358
+ user
25207
25359
  );
25208
- const name = firstValue(options.project, project?.project);
25209
- const given = firstValue(options.org, project?.org);
25210
- const slug = given === void 0 ? void 0 : parseOrgSlug(given);
25360
+ const given = firstValue(options.project, project?.project);
25361
+ const name = given === void 0 ? void 0 : foldProjectName(given);
25362
+ const org = firstValue(options.org, project?.org);
25363
+ const slug = org === void 0 ? void 0 : parseOrgSlug(org);
25211
25364
  const token = firstValue(options.env[TOKEN_ENV]);
25212
- const mode = options.local === true || endpoint === void 0 ? "local" : "remote";
25365
+ const discovered = endpoint !== void 0 && isDefaultEndpoint(endpoint);
25213
25366
  return {
25214
25367
  project: name,
25215
25368
  org: mode === "local" ? LOCAL_ORG : slug,
@@ -25219,10 +25372,12 @@ function resolveContext(options) {
25219
25372
  cwd: project === void 0 ? void 0 : relativeCwd(project.root, options.cwd),
25220
25373
  mode,
25221
25374
  endpoint,
25375
+ endpointSource,
25376
+ localFix,
25377
+ discovered,
25378
+ timeoutMs: void 0,
25222
25379
  token,
25223
- // `quibble config` diagnoses a missing session, so the entry is read here
25224
- // and no command has to read the file to describe the session.
25225
- credential: endpoint === void 0 ? void 0 : findCredential(options.env, endpoint),
25380
+ credential: discovered && endpoint !== void 0 ? findCredential(options.env, endpoint) : void 0,
25226
25381
  actAs: firstValue(options.env[ACT_AS_ENV]),
25227
25382
  debug: debugEnabled(options.env),
25228
25383
  agent: firstValue(options.agent, user.agent) ?? DEFAULT_AGENT,
@@ -25235,6 +25390,15 @@ function resolveContext(options) {
25235
25390
  projectConfig: project
25236
25391
  };
25237
25392
  }
25393
+ function rememberEndpoint(env, endpoint) {
25394
+ const path = defaultConfigPath(env);
25395
+ const raw = readJsonFile(path) ?? {};
25396
+ if (stringField(raw, "endpoint", path) !== void 0) return void 0;
25397
+ mkdirSync2(dirname3(path), { recursive: true });
25398
+ writeFileSync2(path, `${JSON.stringify({ ...raw, endpoint }, null, 2)}
25399
+ `, "utf8");
25400
+ return path;
25401
+ }
25238
25402
 
25239
25403
  // src/ids.ts
25240
25404
  var FULL_ID_LENGTH = 36;
@@ -25438,7 +25602,7 @@ var ApiClient = class {
25438
25602
  constructor(options) {
25439
25603
  this.endpoint = options.endpoint;
25440
25604
  this.org = options.org;
25441
- this.#base = trimSlash(options.endpoint);
25605
+ this.#base = trimSlash2(options.endpoint);
25442
25606
  this.#auth = options.authorizer;
25443
25607
  this.#beforeFirst = options.beforeFirst;
25444
25608
  this.#http = {
@@ -25516,6 +25680,13 @@ var ApiClient = class {
25516
25680
  const retagged = answer2.body["retagged"];
25517
25681
  return { ...answer2, retagged: typeof retagged === "number" ? retagged : 0 };
25518
25682
  }
25683
+ /**
25684
+ * Soft-deletes a project (section 7.5). The route hides it and its
25685
+ * complaints everywhere and revokes its tokens, and answers `204`.
25686
+ */
25687
+ async deleteProject(name) {
25688
+ await this.request("DELETE", this.projectPath(name));
25689
+ }
25519
25690
  /** Lists the tokens of a project, newest first, as the route sorts them. */
25520
25691
  async tokens(project) {
25521
25692
  const body = await this.request("GET", `${this.projectPath(project)}/tokens`);
@@ -25804,7 +25975,7 @@ import {
25804
25975
  closeSync,
25805
25976
  fstatSync,
25806
25977
  linkSync,
25807
- mkdirSync as mkdirSync2,
25978
+ mkdirSync as mkdirSync3,
25808
25979
  openSync,
25809
25980
  readFileSync as readFileSync3,
25810
25981
  readSync,
@@ -25878,7 +26049,7 @@ function endsInsideLine(path) {
25878
26049
  }
25879
26050
  function appendLine(path, value) {
25880
26051
  try {
25881
- mkdirSync2(dirname4(path), { recursive: true, mode: HOME_MODE });
26052
+ mkdirSync3(dirname4(path), { recursive: true, mode: HOME_MODE });
25882
26053
  const lead = endsInsideLine(path) ? "\n" : "";
25883
26054
  appendFileSync(path, `${lead}${JSON.stringify(value)}
25884
26055
  `, {
@@ -25996,7 +26167,7 @@ function createLock(lock, owner = { pid: process.pid, host: hostname3(), at: (/*
25996
26167
  const text = JSON.stringify(owner);
25997
26168
  let fd;
25998
26169
  try {
25999
- mkdirSync2(dirname4(lock), { recursive: true, mode: HOME_MODE });
26170
+ mkdirSync3(dirname4(lock), { recursive: true, mode: HOME_MODE });
26000
26171
  fd = openSync(lock, "wx", QUEUE_MODE);
26001
26172
  } catch (error62) {
26002
26173
  if (error62.code === "EEXIST") return void 0;
@@ -26046,8 +26217,8 @@ function releaseLock(lock, text) {
26046
26217
  }
26047
26218
  function stopReason(status, text) {
26048
26219
  if (status >= 500) return `the API answered ${status}`;
26049
- const code = serverCode(text);
26050
- const stops = status === 429 || status === 401 || status === 403 || status === 404 && code === ORG_NOT_FOUND;
26220
+ const code2 = serverCode(text);
26221
+ const stops = status === 429 || status === 401 || status === 403 || status === 404 && code2 === ORG_NOT_FOUND;
26051
26222
  if (!stops) return void 0;
26052
26223
  const message = stringOf(asRecord(asRecord(parseQuietly(text))?.["error"]), "message");
26053
26224
  return message ?? `the API answered ${status}`;
@@ -26117,12 +26288,12 @@ async function flushQueue(options) {
26117
26288
  return result;
26118
26289
  }
26119
26290
  const record2 = asRecord(asRecord(parseQuietly(text))?.["error"]);
26120
- const code = stringOf(record2, "code") ?? `http_${status}`;
26291
+ const code2 = stringOf(record2, "code") ?? `http_${status}`;
26121
26292
  const message = stringOf(record2, "message") ?? `the API answered ${status}`;
26122
26293
  appendLine(options.path, {
26123
26294
  op: "dropped",
26124
26295
  key: entry.key,
26125
- code,
26296
+ code: code2,
26126
26297
  message,
26127
26298
  droppedAt: now().toISOString()
26128
26299
  });
@@ -26176,23 +26347,23 @@ function actAsProblem(context) {
26176
26347
  if (context.credential !== void 0) {
26177
26348
  return `you are logged in to ${endpoint}`;
26178
26349
  }
26179
- if (!isLoopback(endpoint)) return `${endpoint} is not a loopback endpoint`;
26350
+ if (!isLoopbackUrl(endpoint)) return `${endpoint} is not a loopback endpoint`;
26180
26351
  return void 0;
26181
26352
  }
26182
26353
  function checkActAs(context) {
26183
26354
  const problem = actAsProblem(context);
26184
26355
  if (problem !== void 0) throw actAsNotApplicable(problem);
26185
26356
  }
26186
- function openClient(context, io, options = {}) {
26357
+ async function openClient(context, io, options = {}) {
26358
+ const endpoint = await resolveEndpoint(context, io);
26187
26359
  checkActAs(context);
26188
- const endpoint = requireEndpoint(context);
26189
- const http = { debug: context.debug, fetch: io.fetch };
26360
+ const http = { debug: context.debug, fetch: io.fetch, timeoutMs: context.timeoutMs };
26190
26361
  const org = {
26191
26362
  ...context.org === void 0 ? {} : { org: context.org },
26192
26363
  ...options.queue === false ? {} : { beforeFirst: queueSender(context, endpoint, io) }
26193
26364
  };
26194
26365
  if (context.token === void 0 && context.credential === void 0) {
26195
- if (!isLoopback(endpoint)) throw notLoggedIn(endpoint);
26366
+ if (!isLoopbackUrl(endpoint)) throw notLoggedIn(endpoint);
26196
26367
  return new ApiClient({
26197
26368
  endpoint,
26198
26369
  authorizer: devAuthorizer(endpoint, context.actAs, http),
@@ -26207,26 +26378,27 @@ function openClient(context, io, options = {}) {
26207
26378
  ...http
26208
26379
  });
26209
26380
  }
26210
- function openOrgClient(context, io) {
26211
- checkActAs(context);
26381
+ async function openOrgClient(context, io, options = {}) {
26382
+ if (context.mode === "local") checkActAs(context);
26212
26383
  requireOrg(context);
26213
- return openClient(context, io);
26384
+ return openClient(context, io, options);
26214
26385
  }
26215
- function openBackend(context, io) {
26386
+ async function openBackend(context, io) {
26216
26387
  if (context.mode === "local") {
26217
26388
  checkActAs(context);
26389
+ refuseForeignOrg(context);
26218
26390
  const local = new FileStore(context.storePath);
26219
26391
  return { store: local, client: void 0, local };
26220
26392
  }
26221
- const client = openOrgClient(context, io);
26393
+ const client = await openOrgClient(context, io);
26222
26394
  return { store: new RemoteStore(client), client, local: void 0 };
26223
26395
  }
26224
- function openStore(context, io) {
26225
- return openBackend(context, io).store;
26396
+ async function openStore(context, io) {
26397
+ return (await openBackend(context, io)).store;
26226
26398
  }
26227
26399
  function needsServer(command, context) {
26228
26400
  return new ConfigError(
26229
- context.endpoint === void 0 ? `${command} needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc` : `${command} needs a server: drop --local`
26401
+ `${command} needs a server: ${context.localFix ?? "drop --local"}`
26230
26402
  );
26231
26403
  }
26232
26404
  function requireServer(command, context) {
@@ -26242,7 +26414,7 @@ async function runComplain(message, options, context, io) {
26242
26414
  throw new UsageError(REMOTE_REPORTER_MESSAGE);
26243
26415
  }
26244
26416
  const body = await readMessage(message, io);
26245
- const store = openStore(context, io);
26417
+ const store = await openStore(context, io);
26246
26418
  const input2 = {
26247
26419
  org,
26248
26420
  project,
@@ -26266,6 +26438,7 @@ async function runComplain(message, options, context, io) {
26266
26438
  }
26267
26439
  if (options.json === true) {
26268
26440
  writeJson(io.stdout, complaint);
26441
+ localNotice(context, io);
26269
26442
  return 0;
26270
26443
  }
26271
26444
  const scope = context.mode === "remote" ? `${complaint.org}/${complaint.project}` : complaint.project;
@@ -26274,8 +26447,16 @@ async function runComplain(message, options, context, io) {
26274
26447
  `Filed complaint ${complaint.id} for ${scope} (${complaint.severity})`
26275
26448
  );
26276
26449
  for (const line of matchLines(complaint)) writeLine(io.stdout, line);
26450
+ localNotice(context, io);
26277
26451
  return 0;
26278
26452
  }
26453
+ function localNotice(context, io) {
26454
+ if (context.mode !== "local") return;
26455
+ writeLine(
26456
+ io.stderr,
26457
+ `quibble: local mode: nothing was sent; the complaint is in ${context.storePath}`
26458
+ );
26459
+ }
26279
26460
  function matchLines(complaint) {
26280
26461
  const lines = [];
26281
26462
  if (complaint.openMatches === 1) {
@@ -26319,7 +26500,7 @@ function describeCredential(context) {
26319
26500
  if (context.credential !== void 0) {
26320
26501
  return `logged in as ${context.credential.email}`;
26321
26502
  }
26322
- if (context.endpoint !== void 0 && isLoopback(context.endpoint)) {
26503
+ if (context.endpoint !== void 0 && isLoopbackUrl(context.endpoint)) {
26323
26504
  return "loopback dev token";
26324
26505
  }
26325
26506
  return "not logged in";
@@ -26331,14 +26512,18 @@ function describeAuth(context) {
26331
26512
  if (problem === void 0) return `${credential}, acting as ${context.actAs}`;
26332
26513
  return `${credential}; ${ACT_AS_ENV} does not apply: ${problem}`;
26333
26514
  }
26334
- function runConfig(options, context, io) {
26515
+ async function runConfig(options, context, io) {
26516
+ if (context.mode === "remote") await resolveEndpoint(context, io);
26517
+ const ignored = foreignOrgMessage(context) === void 0 ? void 0 : context.slug;
26335
26518
  const report = {
26336
26519
  project: context.project ?? "none",
26337
26520
  org: context.org ?? null,
26521
+ orgIgnored: ignored ?? null,
26338
26522
  projectRoot: context.projectRoot ?? null,
26339
26523
  configPath: context.configPath ?? null,
26340
26524
  mode: context.mode,
26341
- endpoint: context.endpoint ?? null,
26525
+ endpoint: context.endpoint ?? "local",
26526
+ endpointSource: context.endpointSource,
26342
26527
  storePath: context.storePath,
26343
26528
  agent: context.agent,
26344
26529
  reporter: context.reporter,
@@ -26351,16 +26536,84 @@ function runConfig(options, context, io) {
26351
26536
  writeJson(io.stdout, report);
26352
26537
  return 0;
26353
26538
  }
26354
- const width = Math.max(...Object.keys(report).map((key) => key.length));
26355
- for (const [key, value] of Object.entries(report)) {
26356
- writeLine(io.stdout, `${key.padEnd(width)} ${value ?? "none"}`);
26539
+ const { orgIgnored, ...lines } = report;
26540
+ const text = { ...lines };
26541
+ if (orgIgnored !== null) {
26542
+ text["org"] = `${report.org ?? ""}; ${orgIgnored} does not apply: ${context.localFix ?? ""}`;
26543
+ }
26544
+ const width = Math.max(...Object.keys(text).map((key) => key.length));
26545
+ for (const [key, value] of Object.entries(text)) {
26546
+ writeLine(io.stdout, `${key.padEnd(width)} ${String(value ?? "none")}`);
26357
26547
  }
26358
26548
  return 0;
26359
26549
  }
26360
26550
 
26361
26551
  // src/commands/init.ts
26362
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "node:fs";
26363
- import { basename as basename2, join as join6, resolve as resolve3 } from "node:path";
26552
+ import { existsSync as existsSync2, writeFileSync as writeFileSync5 } from "node:fs";
26553
+ import { basename as basename2, join as join7, resolve as resolve3 } from "node:path";
26554
+
26555
+ // src/hook.ts
26556
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
26557
+ import { dirname as dirname5, join as join5 } from "node:path";
26558
+ var SETTINGS_PATH = ".claude/settings.json";
26559
+ var HOOK_COMMAND = "command -v quibble >/dev/null 2>&1 && quibble sync --quiet || true";
26560
+ var HOOK_MARK = "quibble sync --quiet";
26561
+ function hookEntry() {
26562
+ return {
26563
+ matcher: "startup",
26564
+ hooks: [{ type: "command", command: HOOK_COMMAND }]
26565
+ };
26566
+ }
26567
+ function isObject2(value) {
26568
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26569
+ }
26570
+ function runsSync(entry) {
26571
+ if (!isObject2(entry) || !Array.isArray(entry["hooks"])) return false;
26572
+ return entry["hooks"].some(
26573
+ (hook) => isObject2(hook) && typeof hook["command"] === "string" && hook["command"].includes(HOOK_MARK)
26574
+ );
26575
+ }
26576
+ function planHook(root) {
26577
+ const path = join5(root, SETTINGS_PATH);
26578
+ let text;
26579
+ try {
26580
+ text = readFileSync4(path, "utf8");
26581
+ } catch (error62) {
26582
+ if (!isNotFound(error62)) throw error62;
26583
+ const created = { hooks: { SessionStart: [hookEntry()] } };
26584
+ return { path, text: `${JSON.stringify(created, null, 2)}
26585
+ ` };
26586
+ }
26587
+ const invalid = new UsageError(
26588
+ `${path} is not valid JSON: fix it, then run the command again`
26589
+ );
26590
+ let settings;
26591
+ try {
26592
+ settings = JSON.parse(text);
26593
+ } catch {
26594
+ throw invalid;
26595
+ }
26596
+ if (!isObject2(settings)) throw invalid;
26597
+ const hooks = settings["hooks"] === void 0 ? {} : settings["hooks"];
26598
+ if (!isObject2(hooks)) throw new UsageError(`${path}: hooks must be an object`);
26599
+ const start = hooks["SessionStart"] === void 0 ? [] : hooks["SessionStart"];
26600
+ if (!Array.isArray(start)) {
26601
+ throw new UsageError(`${path}: hooks.SessionStart must be an array`);
26602
+ }
26603
+ if (start.some(runsSync)) return { path, text: void 0 };
26604
+ const merged = {
26605
+ ...settings,
26606
+ hooks: { ...hooks, SessionStart: [...start, hookEntry()] }
26607
+ };
26608
+ return { path, text: `${JSON.stringify(merged, null, 2)}
26609
+ ` };
26610
+ }
26611
+ function applyHook(plan) {
26612
+ if (plan.text === void 0) return `${plan.path} already runs quibble sync`;
26613
+ mkdirSync4(dirname5(plan.path), { recursive: true });
26614
+ writeFileSync3(plan.path, plan.text, "utf8");
26615
+ return `Added the quibble sync hook to ${plan.path}`;
26616
+ }
26364
26617
 
26365
26618
  // src/snippet.ts
26366
26619
  import { basename } from "node:path";
@@ -26423,57 +26676,131 @@ function agentForFile(path) {
26423
26676
  function hasSnippet(text) {
26424
26677
  return text.split(/\r?\n/).some((line) => line.trimEnd() === SNIPPET_HEADING);
26425
26678
  }
26679
+ function splitLines(text) {
26680
+ const lines = [];
26681
+ const pattern = /([^\r\n]*)(\r\n|\n|\r)?/g;
26682
+ let match;
26683
+ while ((match = pattern.exec(text)) !== null) {
26684
+ const [whole, content = "", eol = ""] = match;
26685
+ if (whole === "") break;
26686
+ lines.push({ text: content, eol });
26687
+ }
26688
+ return lines;
26689
+ }
26690
+ function fenceOf(text) {
26691
+ if (text.startsWith("```")) return "```";
26692
+ if (text.startsWith("~~~")) return "~~~";
26693
+ return void 0;
26694
+ }
26695
+ function endsSection(text) {
26696
+ return text.startsWith("# ") || text.startsWith("## ");
26697
+ }
26698
+ function agentOfText(lines) {
26699
+ const line = lines.find(
26700
+ (text) => text.includes("quibble complain") && text.includes("--agent")
26701
+ );
26702
+ if (line === void 0) return void 0;
26703
+ return /--agent\s+(\S+)/.exec(line)?.[1];
26704
+ }
26705
+ function findSection(text) {
26706
+ const lines = splitLines(text);
26707
+ const start = lines.findIndex((line) => line.text.trimEnd() === SNIPPET_HEADING);
26708
+ if (start < 0) return void 0;
26709
+ let next = lines.length;
26710
+ let fence;
26711
+ for (let index = start + 1; index < lines.length; index += 1) {
26712
+ const content = lines[index]?.text ?? "";
26713
+ if (fence !== void 0) {
26714
+ if (content.startsWith(fence)) fence = void 0;
26715
+ continue;
26716
+ }
26717
+ fence = fenceOf(content);
26718
+ if (fence === void 0 && endsSection(content)) {
26719
+ next = index;
26720
+ break;
26721
+ }
26722
+ }
26723
+ let end = next;
26724
+ while (end > start + 1 && (lines[end - 1]?.text.trim() ?? "") === "") end -= 1;
26725
+ const eol = lines[start]?.eol || "\n";
26726
+ return { lines, start, end, eol: eol === "\r\n" ? "\r\n" : "\n" };
26727
+ }
26728
+ function sectionText(section) {
26729
+ return section.lines.slice(section.start, section.end).map((line) => line.text);
26730
+ }
26731
+ function replaceSection(section, snippet) {
26732
+ const join9 = (lines) => lines.map((line) => `${line.text}${line.eol}`).join("");
26733
+ const body = snippet.split("\n").map((line) => `${line}${section.eol}`).join("");
26734
+ return `${join9(section.lines.slice(0, section.start))}${body}${join9(section.lines.slice(section.end))}`;
26735
+ }
26736
+ function withLineBreak(text, old) {
26737
+ return old.includes("\r\n") ? text.replace(/\r?\n/g, "\r\n") : text;
26738
+ }
26739
+ function orList(items) {
26740
+ if (items.length <= 1) return items.join("");
26741
+ return `${items.slice(0, -1).join(", ")} or ${items.at(-1) ?? ""}`;
26742
+ }
26743
+ function refreshedLine(project, tags) {
26744
+ const lead = `Quibble refreshed its instructions for ${project}:`;
26745
+ if (tags === void 0 || tags.length === 0) return `${lead} any tag is allowed.`;
26746
+ return `${lead} tag each complaint with ${orList(tags)}, or ${OTHER_TAG_PREFIX}<name> when none fits.`;
26747
+ }
26426
26748
 
26427
26749
  // src/commands/snippet.ts
26428
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
26429
- import { dirname as dirname5, join as join5, resolve as resolve2 } from "node:path";
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";
26430
26752
  function existsError(path) {
26431
26753
  return new UsageError(`${path} already exists: pass --force to overwrite it`);
26432
26754
  }
26433
26755
  function readOptional2(path) {
26434
26756
  try {
26435
- return readFileSync4(path, "utf8");
26757
+ return readFileSync5(path, "utf8");
26436
26758
  } catch (error62) {
26437
26759
  if (isNotFound(error62)) return "";
26438
26760
  throw error62;
26439
26761
  }
26440
26762
  }
26441
26763
  function writeSkill(path, snippet) {
26442
- mkdirSync3(dirname5(path), { recursive: true });
26443
- writeFileSync2(path, skillFile(snippet), "utf8");
26764
+ mkdirSync5(dirname6(path), { recursive: true });
26765
+ writeFileSync4(path, skillFile(snippet), "utf8");
26766
+ }
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 }
26778
+ };
26779
+ return { tags, scope };
26444
26780
  }
26445
26781
  async function runSnippet(options, context, io) {
26446
26782
  const project = requireProject(context);
26447
26783
  const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
26448
26784
  const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
26449
- const skillPath = options.skill === true ? join5(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
26785
+ const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
26450
26786
  const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
26451
26787
  if (options.append !== void 0 && hasSnippet(existing)) {
26452
26788
  throw new UsageError(
26453
- `${options.append} already has a "${SNIPPET_HEADING}" section: edit it by hand`
26789
+ `${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
26454
26790
  );
26455
26791
  }
26456
26792
  if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
26457
26793
  throw existsError(skillPath);
26458
26794
  }
26459
- let tags;
26460
- if (context.mode === "remote") {
26461
- tags = (await openOrgClient(context, io).project(project)).tags;
26462
- } else {
26463
- checkActAs(context);
26464
- tags = context.allowedTags;
26465
- }
26466
- const scope = {
26467
- ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26468
- ...context.slug === void 0 ? {} : { org: context.slug }
26469
- };
26795
+ const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
26796
+ const { tags, scope } = await snippetSource(project, context, io);
26470
26797
  let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
26471
26798
  shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
26472
26799
  if (appendPath !== void 0) {
26473
26800
  const agent = override ?? agentForFile(appendPath);
26474
26801
  const snippet = agentSnippet(tags, { ...scope, agent });
26475
26802
  const kept = existing.replace(/\s+$/, "");
26476
- writeFileSync2(appendPath, kept === "" ? `${snippet}
26803
+ writeFileSync4(appendPath, kept === "" ? `${snippet}
26477
26804
  ` : `${kept}
26478
26805
 
26479
26806
  ${snippet}
@@ -26486,23 +26813,25 @@ ${snippet}
26486
26813
  writeSkill(skillPath, snippet);
26487
26814
  if (appendPath === void 0) shown = { agent, snippet };
26488
26815
  }
26816
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26489
26817
  if (options.json === true) {
26490
26818
  writeJson(io.stdout, {
26491
26819
  snippet: shown.snippet,
26492
26820
  agent: shown.agent,
26493
26821
  appended: appendPath ?? null,
26494
- skill: skillPath ?? null
26822
+ skill: skillPath ?? null,
26823
+ hook: hook?.path ?? null
26495
26824
  });
26496
26825
  return 0;
26497
26826
  }
26498
26827
  if (appendPath === void 0 && skillPath === void 0) {
26499
26828
  writeLine(io.stdout, shown.snippet);
26500
- return 0;
26501
26829
  }
26502
26830
  if (appendPath !== void 0) {
26503
26831
  writeLine(io.stdout, `Appended the Quibble snippet to ${appendPath}`);
26504
26832
  }
26505
26833
  if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26834
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26506
26835
  return 0;
26507
26836
  }
26508
26837
 
@@ -26513,34 +26842,47 @@ function agentLead(agent) {
26513
26842
  }
26514
26843
  async function runInit(options, io) {
26515
26844
  const directory = resolve3(io.cwd);
26516
- const path = join6(directory, PROJECT_CONFIG_FILENAME);
26845
+ const path = join7(directory, PROJECT_CONFIG_FILENAME);
26517
26846
  if (existsSync2(path) && options.force !== true) {
26518
26847
  throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
26519
26848
  }
26520
- const skillPath = options.skill === true ? join6(directory, SKILL_PATH) : void 0;
26849
+ const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
26521
26850
  if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
26522
26851
  throw existsError(skillPath);
26523
26852
  }
26524
26853
  const project = resolveProjectName(options.project, directory);
26525
26854
  const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
26526
- const endpoint = options.endpoint?.trim();
26527
26855
  const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
26528
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);
26529
26859
  const config2 = {
26530
26860
  project,
26531
26861
  ...org === void 0 ? {} : { org },
26532
- ...endpoint === void 0 || endpoint === "" ? {} : { endpoint },
26862
+ ...endpoint === void 0 ? {} : { endpoint },
26533
26863
  ...tags === void 0 ? {} : { tags }
26534
26864
  };
26535
- writeFileSync3(path, `${JSON.stringify(config2, null, 2)}
26865
+ writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
26536
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
+ };
26537
26878
  const snippet = agentSnippet(tags, {
26538
26879
  agent,
26539
- ...endpoint === void 0 ? {} : { endpoint },
26880
+ ...context.mode === "remote" ? { endpoint: context.endpoint } : {},
26540
26881
  ...org === void 0 ? {} : { org }
26541
26882
  });
26542
26883
  if (skillPath !== void 0) writeSkill(skillPath, snippet);
26543
- const check2 = await checkServer(options, project, io);
26884
+ const hookLine = hook === void 0 ? void 0 : applyHook(hook);
26885
+ const check2 = await checkServer(context, project, io);
26544
26886
  if (options.json === true) {
26545
26887
  writeJson(io.stdout, {
26546
26888
  path,
@@ -26548,12 +26890,14 @@ async function runInit(options, io) {
26548
26890
  org: org ?? null,
26549
26891
  tags: tags ?? null,
26550
26892
  skill: skillPath ?? null,
26893
+ hook: hook?.path ?? null,
26551
26894
  server: check2.server
26552
26895
  });
26553
26896
  return 0;
26554
26897
  }
26555
26898
  writeLine(io.stdout, `Wrote ${path}`);
26556
26899
  if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
26900
+ if (hookLine !== void 0) writeLine(io.stdout, hookLine);
26557
26901
  writeLine(io.stdout, `Project: ${project}`);
26558
26902
  if (org !== void 0) writeLine(io.stdout, `Organization: ${org}`);
26559
26903
  if (tags !== void 0) writeLine(io.stdout, `Tags: ${tags.join(", ")}`);
@@ -26571,26 +26915,28 @@ async function runInit(options, io) {
26571
26915
  }
26572
26916
  return 0;
26573
26917
  }
26574
- async function checkServer(options, project, io) {
26575
- const context = {
26576
- ...resolveContext({
26577
- cwd: io.cwd,
26578
- env: io.env,
26579
- endpoint: options.endpoint,
26580
- org: options.org,
26581
- local: options.local
26582
- }),
26583
- actAs: void 0
26584
- };
26585
- const { endpoint, org } = context;
26586
- if (context.mode !== "remote" || endpoint === void 0 || org === void 0 || context.token === void 0 && context.credential === void 0 && !isLoopback(endpoint)) {
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)
26925
+ });
26926
+ return isDefaultEndpoint(api) ? void 0 : api;
26927
+ }
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)) {
26587
26933
  return { server: "unchecked" };
26588
26934
  }
26589
26935
  const warn = (text) => {
26590
26936
  writeLine(io.stderr, `quibble: warning: ${text}`);
26591
26937
  };
26592
26938
  try {
26593
- await openClient(context, io, { queue: false }).project(project);
26939
+ await (await openClient(context, io, { queue: false })).project(project);
26594
26940
  return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
26595
26941
  } catch (error62) {
26596
26942
  if (error62 instanceof RemoteError && error62.status === 404) {
@@ -26760,7 +27106,7 @@ async function runList(options, context, io) {
26760
27106
  if (options.ids === true && options.json === true) {
26761
27107
  throw new UsageError("--ids and --json cannot be used together");
26762
27108
  }
26763
- const { store, client } = openBackend(context, io);
27109
+ const { store, client } = await openBackend(context, io);
26764
27110
  const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
26765
27111
  const filter = parseListFilter({
26766
27112
  ...resolveScope(options, context),
@@ -26835,13 +27181,13 @@ function formatTable(rows2) {
26835
27181
  }
26836
27182
 
26837
27183
  // src/commands/orgs.ts
26838
- var ORGS_NEEDS_SERVER = "orgs needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
26839
27184
  var ORGS_PAGE = "/orgs";
26840
27185
  var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
26841
27186
  async function runOrgs(options, context, io) {
26842
- if (context.mode !== "remote") throw new ConfigError(ORGS_NEEDS_SERVER);
27187
+ requireServer("orgs", context);
27188
+ const client = await openClient(context, io);
26843
27189
  const endpoint = requireEndpoint(context);
26844
- const answer2 = await openClient(context, io).orgs();
27190
+ const answer2 = await client.orgs();
26845
27191
  if (options.json === true) {
26846
27192
  writeJson(io.stdout, answer2.body);
26847
27193
  return 0;
@@ -26849,9 +27195,10 @@ async function runOrgs(options, context, io) {
26849
27195
  if (answer2.orgs.length === 0) {
26850
27196
  const config2 = await fetchAuthConfig(endpoint, {
26851
27197
  debug: context.debug,
26852
- fetch: io.fetch
27198
+ fetch: io.fetch,
27199
+ timeoutMs: context.timeoutMs
26853
27200
  });
26854
- const webUrl = config2.mode === "cognito" ? trimSlash(config2.webUrl) : endpoint;
27201
+ const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
26855
27202
  writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE}.`);
26856
27203
  return 0;
26857
27204
  }
@@ -26911,19 +27258,19 @@ var NodeTerminal = class {
26911
27258
  #interface() {
26912
27259
  if (this.#rl !== void 0) return this.#rl;
26913
27260
  const output2 = this.#output;
26914
- const sink = new Writable({
27261
+ const sink2 = new Writable({
26915
27262
  write: (chunk, _encoding, done) => {
26916
27263
  if (!this.#muted) output2.write(chunk);
26917
27264
  done();
26918
27265
  }
26919
27266
  });
26920
- Object.defineProperties(sink, {
27267
+ Object.defineProperties(sink2, {
26921
27268
  columns: { get: () => output2.columns },
26922
27269
  rows: { get: () => output2.rows }
26923
27270
  });
26924
27271
  const rl = createInterface({
26925
27272
  input: this.#input,
26926
- output: sink,
27273
+ output: sink2,
26927
27274
  terminal: true,
26928
27275
  // No answer enters the history, so an arrow key recalls no password.
26929
27276
  historySize: 0
@@ -27023,19 +27370,19 @@ async function signIn(terminal, identity, config2) {
27023
27370
  password
27024
27371
  });
27025
27372
  if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
27026
- const code = await answer(terminal, "Code:");
27373
+ const code2 = await answer(terminal, "Code:");
27027
27374
  result = await respondToAuthChallenge(identity, {
27028
27375
  clientId,
27029
27376
  challenge: result.challenge,
27030
27377
  email: email3,
27031
- code
27378
+ code: code2
27032
27379
  });
27033
27380
  }
27034
27381
  if (result.kind === "challenge") {
27035
27382
  const name = result.challenge.challengeName;
27036
27383
  if (name === NEW_PASSWORD_CHALLENGE) {
27037
27384
  throw new ConfigError(
27038
- `finish your first sign-in at ${trimSlash(config2.webUrl)}${LOGIN_PAGE}`
27385
+ `finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
27039
27386
  );
27040
27387
  }
27041
27388
  throw new RemoteError(
@@ -27045,7 +27392,7 @@ async function signIn(terminal, identity, config2) {
27045
27392
  return result.tokens;
27046
27393
  } catch (error62) {
27047
27394
  if (error62 instanceof CognitoError) {
27048
- const page = `${trimSlash(config2.webUrl)}${LOGIN_PAGE}`;
27395
+ const page = `${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`;
27049
27396
  if (error62.type === "PasswordResetRequiredException") {
27050
27397
  throw new ConfigError(`reset your password at ${page}`);
27051
27398
  }
@@ -27057,9 +27404,10 @@ async function signIn(terminal, identity, config2) {
27057
27404
  }
27058
27405
  }
27059
27406
  async function runLogin(options, context, io) {
27060
- const endpoint = requireEndpoint(context);
27407
+ requireServer("login", context);
27061
27408
  const terminal = terminalOf(io);
27062
27409
  if (!terminal.isTTY) throw new UsageError(NEEDS_TERMINAL_MESSAGE);
27410
+ const endpoint = await resolveEndpoint(context, io);
27063
27411
  const http = { debug: context.debug, fetch: io.fetch };
27064
27412
  const config2 = await fetchAuthConfig(endpoint, http);
27065
27413
  if (config2.mode === "local") {
@@ -27097,25 +27445,36 @@ async function runLogin(options, context, io) {
27097
27445
  sub: me.sub,
27098
27446
  email: me.email
27099
27447
  });
27448
+ const saved = saveEndpoint(context, endpoint, io.env);
27100
27449
  if (options.json === true) {
27101
- writeJson(io.stdout, { ...me.body, endpoint });
27450
+ writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
27102
27451
  return 0;
27103
27452
  }
27104
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}.`);
27456
+ }
27105
27457
  return 0;
27106
27458
  }
27459
+ function saveEndpoint(context, endpoint, env) {
27460
+ if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
27461
+ return void 0;
27462
+ }
27463
+ return rememberEndpoint(env, endpoint);
27464
+ }
27107
27465
  async function readProfile(client, webUrl) {
27108
27466
  try {
27109
27467
  return await client.me();
27110
27468
  } catch (error62) {
27111
27469
  if (error62 instanceof RemoteError && NO_PROFILE_STATUSES.has(error62.status ?? 0)) {
27112
- throw new ConfigError(`finish your account at ${trimSlash(webUrl)}${LOGIN_PAGE}`);
27470
+ throw new ConfigError(`finish your account at ${trimSlash2(webUrl)}${LOGIN_PAGE}`);
27113
27471
  }
27114
27472
  throw error62;
27115
27473
  }
27116
27474
  }
27117
27475
  async function runLogout(context, io) {
27118
- const endpoint = requireEndpoint(context);
27476
+ requireServer("logout", context);
27477
+ const endpoint = await resolveEndpoint(context, io);
27119
27478
  const credential = findCredential(io.env, endpoint);
27120
27479
  if (credential === void 0) {
27121
27480
  writeLine(io.stdout, `Not logged in to ${endpoint}.`);
@@ -27151,8 +27510,8 @@ async function runWhoami(options, context, io) {
27151
27510
  writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
27152
27511
  return 0;
27153
27512
  }
27513
+ const client = await openClient(context, io);
27154
27514
  const endpoint = requireEndpoint(context);
27155
- const client = openClient(context, io);
27156
27515
  const me = await client.me();
27157
27516
  const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
27158
27517
  if (options.json === true) {
@@ -27169,7 +27528,7 @@ async function runWhoami(options, context, io) {
27169
27528
  debug: context.debug,
27170
27529
  fetch: io.fetch
27171
27530
  });
27172
- const webUrl = config2.mode === "cognito" ? trimSlash(config2.webUrl) : endpoint;
27531
+ const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
27173
27532
  lines.push("", `Create one at ${webUrl}${ORGS_PAGE2}.`);
27174
27533
  }
27175
27534
  for (const line of lines) writeLine(io.stdout, line);
@@ -27209,18 +27568,40 @@ function whoamiLines(endpoint, me, org) {
27209
27568
  return lines;
27210
27569
  }
27211
27570
 
27571
+ // src/commands/project.ts
27572
+ var DELETE_NEEDS_YES = "project delete needs --yes when stdin is not a terminal";
27573
+ var DELETE_MISMATCH = "the name did not match: nothing was deleted";
27574
+ async function confirm(org, project, io) {
27575
+ const terminal = io.terminal ?? nodeTerminal(io.stdin, io.stderr);
27576
+ if (!terminal.isTTY) throw new UsageError(DELETE_NEEDS_YES);
27577
+ let answer2;
27578
+ try {
27579
+ answer2 = await terminal.prompt(`Type the project name to delete ${org}/${project}:`);
27580
+ } finally {
27581
+ terminal.close();
27582
+ }
27583
+ if (foldProjectName(answer2.trim()) !== project) throw new UsageError(DELETE_MISMATCH);
27584
+ }
27585
+ async function runProjectDelete(name, options, context, io) {
27586
+ requireServer("project delete", context);
27587
+ const project = parseProjectName(name);
27588
+ const org = requireOrg(context);
27589
+ if (options.yes !== true) await confirm(org, project, io);
27590
+ await (await openOrgClient(context, io)).deleteProject(project);
27591
+ if (options.json === true) {
27592
+ writeJson(io.stdout, { org, project, deleted: true });
27593
+ return 0;
27594
+ }
27595
+ writeLine(io.stdout, `Deleted project ${org}/${project}.`);
27596
+ return 0;
27597
+ }
27598
+
27212
27599
  // src/commands/promote.ts
27213
- var PROMOTE_NEEDS_SERVER = "promote needs a server: pass --endpoint <url>, set QUIBBLE_ENDPOINT, or add one to .quibblerc";
27214
- var PROMOTE_NEEDS_SERVER_LOCAL = "promote needs a server: drop --local";
27215
27600
  async function runPromote(tag, options, context, io) {
27216
- if (context.mode !== "remote") {
27217
- throw new ConfigError(
27218
- context.endpoint === void 0 ? PROMOTE_NEEDS_SERVER : PROMOTE_NEEDS_SERVER_LOCAL
27219
- );
27220
- }
27601
+ requireServer("promote", context);
27221
27602
  const project = requireProject(context);
27222
27603
  const promotion = parseTagPromotion({ tag });
27223
- const result = await openOrgClient(context, io).promoteTag(project, promotion.tag);
27604
+ const result = await (await openOrgClient(context, io)).promoteTag(project, promotion.tag);
27224
27605
  if (options.json === true) {
27225
27606
  writeJson(io.stdout, {
27226
27607
  project,
@@ -27245,7 +27626,7 @@ var NO_TAG_MESSAGE = "retag needs at least one --tag";
27245
27626
  async function runRetag(value, options, context, io) {
27246
27627
  const tags = options.tag ?? [];
27247
27628
  if (tags.length === 0) throw new UsageError(NO_TAG_MESSAGE);
27248
- const { store, client, local } = openBackend(context, io);
27629
+ const { store, client, local } = await openBackend(context, io);
27249
27630
  const [id] = await resolveIds([value], { store, local, context });
27250
27631
  if (id === void 0) throw new NotFoundError(`no complaint with id ${value}`);
27251
27632
  const allowedTags = client === void 0 ? await localAllowedTags(store, id, context) : void 0;
@@ -27274,9 +27655,9 @@ async function localAllowedTags(store, id, context) {
27274
27655
  // src/commands/retry.ts
27275
27656
  async function runRetry(options, context, io) {
27276
27657
  requireServer("retry", context);
27658
+ const client = await openClient(context, io, { queue: false });
27277
27659
  const endpoint = requireEndpoint(context);
27278
27660
  const path = queuePath(io.env);
27279
- const client = openClient(context, io, { queue: false });
27280
27661
  const result = await flushQueue({
27281
27662
  path,
27282
27663
  endpoint,
@@ -27348,7 +27729,7 @@ function showLines(complaint, groupCount, remote) {
27348
27729
  return [...lines, "", printableLines(complaint.message)];
27349
27730
  }
27350
27731
  async function runShow(value, options, context, io) {
27351
- const { store, local } = openBackend(context, io);
27732
+ const { store, local } = await openBackend(context, io);
27352
27733
  const [id] = await resolveIds([value], { store, local, context });
27353
27734
  const complaint = id === void 0 ? void 0 : await getComplaint(store, id);
27354
27735
  if (id === void 0 || complaint === void 0) {
@@ -27403,7 +27784,7 @@ async function runStatusChange(status, values, options, context, io) {
27403
27784
  };
27404
27785
  parseStatusChange(draft);
27405
27786
  const given = await readIdArguments(values, io);
27406
- const backend = openBackend(context, io);
27787
+ const backend = await openBackend(context, io);
27407
27788
  const extra = options.duplicateOf === void 0 ? [] : [options.duplicateOf];
27408
27789
  const resolved = await resolveIds([...given, ...extra], {
27409
27790
  store: backend.store,
@@ -27442,10 +27823,10 @@ async function runStatusChange(status, values, options, context, io) {
27442
27823
  writeLine(io.stdout, successLine(status, result.complaint));
27443
27824
  continue;
27444
27825
  }
27445
- const code = result.error?.code ?? NOT_FOUND;
27446
- const message = code === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
27826
+ const code2 = result.error?.code ?? NOT_FOUND;
27827
+ const message = code2 === NOT_FOUND ? noComplaint(result.id) : result.error?.message;
27447
27828
  writeLine(io.stderr, `quibble: ${result.id}: ${message ?? "the change failed"}`);
27448
- failures.push({ remote: backend.store instanceof RemoteStore && code !== NOT_FOUND });
27829
+ failures.push({ remote: backend.store instanceof RemoteStore && code2 !== NOT_FOUND });
27449
27830
  }
27450
27831
  if (options.json === true) writeJson(io.stdout, results);
27451
27832
  if (error62 !== void 0) throw error62;
@@ -27499,7 +27880,7 @@ async function runSummary(options, context, io) {
27499
27880
  throw new UsageError("--json and --format cannot be used together");
27500
27881
  }
27501
27882
  const format = readFormat(options.format);
27502
- const { store, client } = openBackend(context, io);
27883
+ const { store, client } = await openBackend(context, io);
27503
27884
  const reporter = options.mine === true ? await resolveMine(context, client, io.env) : void 0;
27504
27885
  const filter = parseListFilter({
27505
27886
  ...resolveScope(options, context),
@@ -27609,11 +27990,127 @@ function markdownLines(filter, summary, note) {
27609
27990
  return lines;
27610
27991
  }
27611
27992
 
27993
+ // src/commands/sync.ts
27994
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
27995
+ import { join as join8 } from "node:path";
27996
+ import { Writable as Writable2 } from "node:stream";
27997
+ var SYNC_TIMEOUT_MS = 3e3;
27998
+ var SECTION_FILES = ["CLAUDE.md", "AGENTS.md"];
27999
+ function readOptional3(path) {
28000
+ try {
28001
+ return readFileSync6(path, "utf8");
28002
+ } catch (error62) {
28003
+ if (isNotFound(error62)) return void 0;
28004
+ throw error62;
28005
+ }
28006
+ }
28007
+ function holders(root) {
28008
+ const found = [];
28009
+ for (const name of SECTION_FILES) {
28010
+ const path = join8(root, name);
28011
+ const text = readOptional3(path);
28012
+ if (text !== void 0 && findSection(text) !== void 0) {
28013
+ found.push({ path, text, skill: false });
28014
+ }
28015
+ }
28016
+ const skill = join8(root, SKILL_PATH);
28017
+ if (existsSync3(skill)) {
28018
+ found.push({ path: skill, text: readOptional3(skill) ?? "", skill: true });
28019
+ }
28020
+ return found;
28021
+ }
28022
+ function agentOf(lines, path) {
28023
+ const named = agentOfText(lines);
28024
+ if (named !== void 0) {
28025
+ try {
28026
+ return parseAgentName(named);
28027
+ } catch {
28028
+ }
28029
+ }
28030
+ return agentForFile(path);
28031
+ }
28032
+ function rewrite(holder, tags, scope) {
28033
+ if (holder.skill) {
28034
+ const agent2 = agentOf(holder.text.split(/\r?\n/), holder.path);
28035
+ return withLineBreak(skillFile(agentSnippet(tags, { ...scope, agent: agent2 })), holder.text);
28036
+ }
28037
+ const section = findSection(holder.text);
28038
+ if (section === void 0) return holder.text;
28039
+ const agent = agentOf(sectionText(section), holder.path);
28040
+ return replaceSection(section, agentSnippet(tags, { ...scope, agent }));
28041
+ }
28042
+ function sink() {
28043
+ return new Writable2({
28044
+ write(_chunk, _encoding, done) {
28045
+ done();
28046
+ }
28047
+ });
28048
+ }
28049
+ async function runSync(options, contextOf, io) {
28050
+ if (options.quiet !== true) return sync(options, contextOf(), io);
28051
+ const silent = { ...io, stdout: sink(), stderr: sink() };
28052
+ try {
28053
+ const context = contextOf();
28054
+ context.timeoutMs = SYNC_TIMEOUT_MS;
28055
+ const result = await syncFiles({ hook: options.hook }, context, silent);
28056
+ if (result.files.some((file2) => file2.updated)) {
28057
+ writeLine(io.stdout, refreshedLine(result.project, result.tags));
28058
+ }
28059
+ } catch {
28060
+ }
28061
+ return 0;
28062
+ }
28063
+ async function sync(options, context, io) {
28064
+ const result = await syncFiles(options, context, io);
28065
+ if (options.json === true) {
28066
+ writeJson(io.stdout, {
28067
+ project: result.project,
28068
+ tags: result.tags ?? null,
28069
+ files: result.files,
28070
+ hook: result.hook ?? null
28071
+ });
28072
+ return 0;
28073
+ }
28074
+ for (const file2 of result.files) {
28075
+ writeLine(
28076
+ io.stdout,
28077
+ file2.updated ? `Updated ${file2.path}` : `${file2.path} is up to date`
28078
+ );
28079
+ }
28080
+ return 0;
28081
+ }
28082
+ async function syncFiles(options, context, io) {
28083
+ const project = requireProject(context);
28084
+ const root = context.projectRoot ?? io.cwd;
28085
+ let hook;
28086
+ if (options.hook === true) {
28087
+ const plan = planHook(root);
28088
+ const line = applyHook(plan);
28089
+ hook = plan.path;
28090
+ if (options.json !== true) writeLine(io.stdout, line);
28091
+ }
28092
+ const found = holders(root);
28093
+ if (found.length === 0) {
28094
+ throw new UsageError(
28095
+ `no Quibble snippet under ${root}: run quibble snippet --append CLAUDE.md`
28096
+ );
28097
+ }
28098
+ const { tags, scope } = await snippetSource(project, context, io, { queue: false });
28099
+ const files = [];
28100
+ for (const holder of found) {
28101
+ const next = rewrite(holder, tags, scope);
28102
+ const updated = next !== holder.text;
28103
+ if (updated) writeFileSync6(holder.path, next, "utf8");
28104
+ files.push({ path: holder.path, updated });
28105
+ }
28106
+ return { project, tags, files, hook };
28107
+ }
28108
+
27612
28109
  // src/commands/tags.ts
27613
28110
  async function runTags(options, context, io) {
27614
28111
  const project = requireProject(context);
27615
28112
  if (context.mode !== "remote") checkActAs(context);
27616
- const tags = context.mode === "remote" ? (await openOrgClient(context, io).project(project)).tags : context.allowedTags;
28113
+ const tags = context.mode === "remote" ? (await (await openOrgClient(context, io)).project(project)).tags : context.allowedTags;
27617
28114
  if (options.json === true) {
27618
28115
  writeJson(io.stdout, { project, tags: tags ?? null });
27619
28116
  return 0;
@@ -27634,7 +28131,7 @@ async function runTagsEdit(edit, given, options, context, io) {
27634
28131
  const project = requireProject(context);
27635
28132
  if (given.length === 0) throw new UsageError(`tags ${edit} needs at least one tag`);
27636
28133
  const named = parseAllowedTags(given);
27637
- const client = openOrgClient(context, io);
28134
+ const client = await openOrgClient(context, io);
27638
28135
  let next;
27639
28136
  if (edit === "set") {
27640
28137
  next = named;
@@ -27695,7 +28192,7 @@ async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */
27695
28192
  { name, ...expiresAt === void 0 ? {} : { expiresAt } },
27696
28193
  now
27697
28194
  );
27698
- const client = openOrgClient(context, io);
28195
+ const client = await openOrgClient(context, io);
27699
28196
  const minted = await client.createToken(project, body);
27700
28197
  if (options.json === true) {
27701
28198
  writeJson(io.stdout, minted.body);
@@ -27705,13 +28202,13 @@ async function runTokenCreate(name, options, context, io, now = /* @__PURE__ */
27705
28202
  io.stdout,
27706
28203
  `Created token ${minted.token.name} for ${org}/${project}. It is shown once:`
27707
28204
  );
27708
- writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash(client.endpoint)}`);
28205
+ writeLine(io.stdout, `QUIBBLE_ENDPOINT=${trimSlash2(client.endpoint)}`);
27709
28206
  writeLine(io.stdout, `QUIBBLE_TOKEN=${minted.plaintext}`);
27710
28207
  return 0;
27711
28208
  }
27712
28209
  async function runTokenList(options, context, io, now = /* @__PURE__ */ new Date()) {
27713
28210
  const { project } = scopeOf(context);
27714
- const answer2 = await openOrgClient(context, io).tokens(project);
28211
+ const answer2 = await (await openOrgClient(context, io)).tokens(project);
27715
28212
  if (options.json === true) {
27716
28213
  writeJson(io.stdout, answer2.body);
27717
28214
  return 0;
@@ -27750,7 +28247,7 @@ function findToken(tokens, value) {
27750
28247
  }
27751
28248
  async function runTokenRevoke(value, options, context, io) {
27752
28249
  const { project, org } = scopeOf(context);
27753
- const client = openOrgClient(context, io);
28250
+ const client = await openOrgClient(context, io);
27754
28251
  const token = findToken((await client.tokens(project)).tokens, value);
27755
28252
  await client.revokeToken(project, token.id);
27756
28253
  if (options.json === true) {
@@ -27785,6 +28282,7 @@ var TAGS_EDITS = [
27785
28282
  var ID_HELP = `complaint id, or a unique prefix of at least ${SHORT_ID_MIN} characters`;
27786
28283
  var REASON_HELP = `${REASONS.slice(0, -1).join(", ")} or ${REASONS.at(-1) ?? ""}`;
27787
28284
  function packageVersion() {
28285
+ if (typeof QUIBBLE_BUILD_VERSION === "string") return QUIBBLE_BUILD_VERSION;
27788
28286
  const require2 = createRequire(import.meta.url);
27789
28287
  const manifest = require2("../package.json");
27790
28288
  return typeof manifest.version === "string" ? manifest.version : "0.0.0";
@@ -27798,7 +28296,7 @@ function rejectEmptyOptions(options) {
27798
28296
  }
27799
28297
  }
27800
28298
  function withGlobalOptions(command) {
27801
- return command.option("--local", "use the local store even if an endpoint is set").option("--endpoint <url>", "base URL of the Quibble API").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
28299
+ return command.option("--local", "use the local store, whatever the endpoint says").option("--endpoint <url>", "API URL, web URL, host, or local").option("--org <slug>", "organization to use in remote mode").option("--json", "print JSON instead of text");
27802
28300
  }
27803
28301
  function globals(program2, command) {
27804
28302
  const outer = program2.opts();
@@ -27836,19 +28334,34 @@ function buildProgram(io, setCode) {
27836
28334
  }).exitOverride();
27837
28335
  withGlobalOptions(program2);
27838
28336
  withGlobalOptions(
27839
- program2.command("init").description("write a .quibblerc in the current directory").option("--project <name>", "project name (default: the directory name)").option("-t, --tag <tag>", "allowed tag (repeatable)", collect).option("--agent <name>", "agent named in the snippet (default: claude)").option("--skill", "also write .claude/skills/quibble/SKILL.md").option("--force", "overwrite an existing .quibblerc or skill file")
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")
27840
28338
  ).action(async function() {
27841
28339
  const options = { ...this.opts(), ...globals(program2, this) };
27842
28340
  rejectEmptyOptions(options);
27843
28341
  setCode(await runInit(options, io));
27844
28342
  });
27845
28343
  withGlobalOptions(
27846
- program2.command("snippet").description("print or write the agent snippet of this project").option("--append <file>", "append the snippet to a file such as CLAUDE.md").option("--skill", "write .claude/skills/quibble/SKILL.md").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
28344
+ program2.command("snippet").description("print or write the agent snippet of this project").option("--append <file>", "append the snippet to a file such as CLAUDE.md").option("--skill", "write .claude/skills/quibble/SKILL.md").option("--hook", "add the quibble sync hook to .claude/settings.json").option("--agent <name>", "agent named in the snippet").option("--project <name>", "project name (overrides .quibblerc)").option("--force", "overwrite an existing skill file")
27847
28345
  ).action(async function() {
27848
28346
  const options = { ...this.opts(), ...globals(program2, this) };
27849
28347
  rejectEmptyOptions(options);
27850
28348
  setCode(await runSnippet(options, contextFor(options, io), io));
27851
28349
  });
28350
+ withGlobalOptions(
28351
+ program2.command("sync").description("rewrite the agent snippet in place with the current tags").option("--hook", "also add the quibble sync hook to .claude/settings.json").option("--quiet", "print nothing but a changed tag list, and always exit 0").option("--project <name>", "project name (overrides .quibblerc)")
28352
+ ).action(async function() {
28353
+ const options = { ...this.opts(), ...globals(program2, this) };
28354
+ setCode(
28355
+ await runSync(
28356
+ options,
28357
+ () => {
28358
+ rejectEmptyOptions(options);
28359
+ return contextFor(options, io);
28360
+ },
28361
+ io
28362
+ )
28363
+ );
28364
+ });
27852
28365
  withGlobalOptions(
27853
28366
  program2.command("complain").description("file a complaint").argument("[message]", "what went wrong; omit it or pass `-` to read stdin").option("-s, --severity <level>", "low, medium or high").option("-t, --tag <tag>", "tag (repeatable)", collect).option("--agent <name>", "name of the agent filing it").option("--reporter <name>", "person you are filing for (local mode only)").option("--project <name>", "project name (overrides .quibblerc)").option("--cmd <command>", "the command that failed")
27854
28367
  ).action(async function(message) {
@@ -27944,6 +28457,14 @@ function buildProgram(io, setCode) {
27944
28457
  rejectEmptyOptions(options);
27945
28458
  setCode(await runTokenRevoke(value, options, contextFor(options, io), io));
27946
28459
  });
28460
+ const projectCommand = program2.command("project").description("manage the projects of the organization (remote only)");
28461
+ withGlobalOptions(
28462
+ projectCommand.command("delete").description("delete a project and hide its complaints").argument("<name>", "project name").option("--yes", "skip the question")
28463
+ ).action(async function(name) {
28464
+ const options = { ...this.opts(), ...globals(program2, this) };
28465
+ rejectEmptyOptions(options);
28466
+ setCode(await runProjectDelete(name, options, contextFor(options, io), io));
28467
+ });
27947
28468
  withGlobalOptions(
27948
28469
  program2.command("retry").description("send the complaints queued while offline")
27949
28470
  ).action(async function() {
@@ -27988,10 +28509,10 @@ function buildProgram(io, setCode) {
27988
28509
  });
27989
28510
  withGlobalOptions(
27990
28511
  program2.command("config").description("print the resolved configuration")
27991
- ).action(function() {
28512
+ ).action(async function() {
27992
28513
  const options = { ...this.opts(), ...globals(program2, this) };
27993
28514
  rejectEmptyOptions(options);
27994
- setCode(runConfig(options, contextFor(options, io), io));
28515
+ setCode(await runConfig(options, contextFor(options, io), io));
27995
28516
  });
27996
28517
  return program2;
27997
28518
  }
@@ -27999,9 +28520,10 @@ function stripErrorPrefix(message) {
27999
28520
  return message.replace(/^error:\s*/i, "");
28000
28521
  }
28001
28522
  async function main(argv, io) {
28002
- let code = 0;
28523
+ resetDiscovery();
28524
+ let code2 = 0;
28003
28525
  const program2 = buildProgram(io, (value) => {
28004
- code = value;
28526
+ code2 = value;
28005
28527
  });
28006
28528
  if (argv.length === 0) {
28007
28529
  io.stdout.write(program2.helpInformation());
@@ -28009,7 +28531,7 @@ async function main(argv, io) {
28009
28531
  }
28010
28532
  try {
28011
28533
  await program2.parseAsync(argv, { from: "user" });
28012
- return code;
28534
+ return code2;
28013
28535
  } catch (error62) {
28014
28536
  if (error62 instanceof CommanderError) {
28015
28537
  if (error62.code === "commander.help" || error62.code === "commander.helpDisplayed") {
@@ -28031,10 +28553,18 @@ for (const stream of [process2.stdout, process2.stderr]) {
28031
28553
  throw error62;
28032
28554
  });
28033
28555
  }
28034
- process2.exitCode = await main(process2.argv.slice(2), {
28556
+ function flushed(stream) {
28557
+ return new Promise((resolve4) => {
28558
+ stream.write("", () => resolve4());
28559
+ });
28560
+ }
28561
+ var code = await main(process2.argv.slice(2), {
28035
28562
  stdin: process2.stdin,
28036
28563
  stdout: process2.stdout,
28037
28564
  stderr: process2.stderr,
28038
28565
  env: process2.env,
28039
28566
  cwd: process2.cwd()
28040
28567
  });
28568
+ process2.exitCode = code;
28569
+ await Promise.all([flushed(process2.stdout), flushed(process2.stderr)]);
28570
+ process2.exit(code);