letthemquibble 2.0.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.
- package/README.md +16 -7
- package/dist/quibble.js +1283 -999
- package/package.json +3 -7
package/dist/quibble.js
CHANGED
|
@@ -23498,6 +23498,43 @@ function freeText(max, typeMessage) {
|
|
|
23498
23498
|
const message = lengthMessage(1, max);
|
|
23499
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 });
|
|
23500
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
|
+
}
|
|
23501
23538
|
var SEVERITY_MESSAGE = `must be one of ${SEVERITIES.join(", ")}`;
|
|
23502
23539
|
var STATUS_MESSAGE = `must be one of ${STATUSES.join(", ")}`;
|
|
23503
23540
|
var LIMIT_MESSAGE = `must be an integer between 1 and ${MAX_LIMIT}`;
|
|
@@ -23650,7 +23687,7 @@ var agentSchema = freeText(AGENT_MAX);
|
|
|
23650
23687
|
var newComplaintShape = {
|
|
23651
23688
|
org: orgSlugSchema,
|
|
23652
23689
|
project: projectNameSchema,
|
|
23653
|
-
message: freeText(MESSAGE_MAX),
|
|
23690
|
+
message: secretFree(freeText(MESSAGE_MAX), MESSAGE_MAX),
|
|
23654
23691
|
severity: severitySchema.optional(),
|
|
23655
23692
|
tags: tagsSchema.optional(),
|
|
23656
23693
|
agent: agentSchema.optional(),
|
|
@@ -23658,7 +23695,7 @@ var newComplaintShape = {
|
|
|
23658
23695
|
reporterName: freeText(REPORTER_MAX).optional(),
|
|
23659
23696
|
cwd: cwdSchema.optional(),
|
|
23660
23697
|
// Free text: it is trimmed and may hold newlines, and nothing parses it.
|
|
23661
|
-
cmd: freeText(CMD_MAX).optional()
|
|
23698
|
+
cmd: secretFree(freeText(CMD_MAX), CMD_MAX).optional()
|
|
23662
23699
|
};
|
|
23663
23700
|
var newComplaintObject = external_exports.strictObject(newComplaintShape);
|
|
23664
23701
|
function withDefaults(input2) {
|
|
@@ -25435,7 +25472,7 @@ function resolveEndpointSource(options, project, user) {
|
|
|
25435
25472
|
};
|
|
25436
25473
|
}
|
|
25437
25474
|
function resolveContext(options) {
|
|
25438
|
-
const project = findProjectConfig(options.cwd);
|
|
25475
|
+
const project = options.readProjectConfig === false ? void 0 : findProjectConfig(options.cwd);
|
|
25439
25476
|
const user = loadUserConfig(options.env);
|
|
25440
25477
|
const { mode, endpoint, endpointSource, localFix } = resolveEndpointSource(
|
|
25441
25478
|
options,
|
|
@@ -25676,7 +25713,7 @@ function readComplaint(body) {
|
|
|
25676
25713
|
throw new RemoteError(NOT_A_COMPLAINT, { cause: error62 });
|
|
25677
25714
|
}
|
|
25678
25715
|
}
|
|
25679
|
-
var ApiClient = class {
|
|
25716
|
+
var ApiClient = class _ApiClient {
|
|
25680
25717
|
endpoint;
|
|
25681
25718
|
/** The organization of every org-scoped path, when the command resolved one. */
|
|
25682
25719
|
org;
|
|
@@ -25724,6 +25761,19 @@ var ApiClient = class {
|
|
|
25724
25761
|
if (this.org === void 0) throw new ConfigError(NO_ORG_MESSAGE);
|
|
25725
25762
|
return `/v1/orgs/${encodeURIComponent(this.org)}`;
|
|
25726
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
|
+
}
|
|
25727
25777
|
/** Reads the caller's own identity. */
|
|
25728
25778
|
async me() {
|
|
25729
25779
|
return parseMe(await this.request("GET", "/v1/me"));
|
|
@@ -25751,6 +25801,18 @@ var ApiClient = class {
|
|
|
25751
25801
|
async project(name) {
|
|
25752
25802
|
return readProject(await this.request("GET", this.projectPath(name)), name);
|
|
25753
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
|
+
}
|
|
25754
25816
|
/**
|
|
25755
25817
|
* Replaces the allowed tag list of a project, or clears it with `null`. The
|
|
25756
25818
|
* route retags the stored complaints and answers how many it rewrote.
|
|
@@ -26314,7 +26376,9 @@ function projectOf(entry) {
|
|
|
26314
26376
|
}
|
|
26315
26377
|
function headlineOfEntry(entry) {
|
|
26316
26378
|
const message = entry.body["message"];
|
|
26317
|
-
|
|
26379
|
+
if (typeof message !== "string") return '"?"';
|
|
26380
|
+
const headline = summarize(message);
|
|
26381
|
+
return findSecret(headline) === void 0 ? `"${printable(headline)}"` : `key ${printable(entry.key)}`;
|
|
26318
26382
|
}
|
|
26319
26383
|
function queuedCount(n) {
|
|
26320
26384
|
return `${n} queued ${n === 1 ? "complaint" : "complaints"}`;
|
|
@@ -26385,7 +26449,7 @@ async function flushQueue(options) {
|
|
|
26385
26449
|
result.dropped += 1;
|
|
26386
26450
|
writeLine(
|
|
26387
26451
|
options.stderr,
|
|
26388
|
-
`quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)},
|
|
26452
|
+
`quibble: dropped a queued complaint for ${entry.org}/${projectOf(entry)}, ${headlineOfEntry(entry)}: ${printable(message)}`
|
|
26389
26453
|
);
|
|
26390
26454
|
}
|
|
26391
26455
|
return result;
|
|
@@ -26637,6 +26701,75 @@ async function runConfig(options, context, io) {
|
|
|
26637
26701
|
import { existsSync as existsSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
26638
26702
|
import { basename as basename2, join as join7, resolve as resolve3 } from "node:path";
|
|
26639
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
|
+
|
|
26640
26773
|
// src/hook.ts
|
|
26641
26774
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
26642
26775
|
import { dirname as dirname5, join as join5 } from "node:path";
|
|
@@ -26709,11 +26842,13 @@ var DEFAULT_SNIPPET_AGENT = "claude";
|
|
|
26709
26842
|
var AGENTS_FILE_AGENT = "codex";
|
|
26710
26843
|
var SNIPPET_INTRO = `This project uses Quibble. When the build, tests, docs or tooling waste your
|
|
26711
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.";
|
|
26712
26846
|
var SNIPPET_USAGE = `- Set severity with \`-s low|medium|high\`. Add tags with \`-t <tag>\` (repeatable).
|
|
26713
26847
|
- Put a one-line summary first; it is the headline everywhere. Add details below.
|
|
26714
26848
|
- Name the command that failed with \`--cmd "<command>"\`.
|
|
26715
26849
|
- Pipe a long message on stdin with \`quibble complain -\`.
|
|
26716
|
-
- 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}`;
|
|
26717
26852
|
var SKILL_FRONT_MATTER = `---
|
|
26718
26853
|
name: quibble
|
|
26719
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.
|
|
@@ -26831,1127 +26966,1273 @@ function refreshedLine(project, tags) {
|
|
|
26831
26966
|
return `${lead} tag each complaint with ${orList(tags)}, or ${OTHER_TAG_PREFIX}<name> when none fits.`;
|
|
26832
26967
|
}
|
|
26833
26968
|
|
|
26834
|
-
// src/commands/
|
|
26835
|
-
import {
|
|
26836
|
-
|
|
26837
|
-
|
|
26838
|
-
|
|
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");
|
|
26839
26980
|
}
|
|
26840
|
-
function
|
|
26841
|
-
|
|
26842
|
-
|
|
26843
|
-
|
|
26844
|
-
|
|
26845
|
-
|
|
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
|
+
];
|
|
26846
26991
|
}
|
|
26992
|
+
return [{ command: "xdg-open", args: [url2] }];
|
|
26847
26993
|
}
|
|
26848
|
-
function
|
|
26849
|
-
|
|
26850
|
-
|
|
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
|
+
});
|
|
26851
27027
|
}
|
|
26852
|
-
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
26856
|
-
|
|
26857
|
-
|
|
26858
|
-
|
|
26859
|
-
|
|
26860
|
-
|
|
26861
|
-
|
|
26862
|
-
|
|
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
|
+
}
|
|
26863
27041
|
};
|
|
26864
|
-
return { tags, scope };
|
|
26865
27042
|
}
|
|
26866
|
-
async function runSnippet(options, context, io) {
|
|
26867
|
-
const project = requireProject(context);
|
|
26868
|
-
const override = options.agent === void 0 ? void 0 : parseAgentName(options.agent);
|
|
26869
|
-
const appendPath = options.append === void 0 ? void 0 : resolve2(io.cwd, options.append);
|
|
26870
|
-
const skillPath = options.skill === true ? join6(context.projectRoot ?? io.cwd, SKILL_PATH) : void 0;
|
|
26871
|
-
const existing = appendPath === void 0 ? "" : readOptional2(appendPath);
|
|
26872
|
-
if (options.append !== void 0 && hasSnippet(existing)) {
|
|
26873
|
-
throw new UsageError(
|
|
26874
|
-
`${options.append} already has a "${SNIPPET_HEADING}" section: run quibble sync to refresh it`
|
|
26875
|
-
);
|
|
26876
|
-
}
|
|
26877
|
-
if (skillPath !== void 0 && existsSync(skillPath) && options.force !== true) {
|
|
26878
|
-
throw existsError(skillPath);
|
|
26879
|
-
}
|
|
26880
|
-
const hook = options.hook === true ? planHook(context.projectRoot ?? io.cwd) : void 0;
|
|
26881
|
-
const { tags, scope } = await snippetSource(project, context, io);
|
|
26882
|
-
let shown = { agent: override ?? DEFAULT_SNIPPET_AGENT, snippet: "" };
|
|
26883
|
-
shown.snippet = agentSnippet(tags, { ...scope, agent: shown.agent });
|
|
26884
|
-
if (appendPath !== void 0) {
|
|
26885
|
-
const agent = override ?? agentForFile(appendPath);
|
|
26886
|
-
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26887
|
-
const kept = existing.replace(/\s+$/, "");
|
|
26888
|
-
writeFileSync4(appendPath, kept === "" ? `${snippet}
|
|
26889
|
-
` : `${kept}
|
|
26890
27043
|
|
|
26891
|
-
|
|
26892
|
-
|
|
26893
|
-
|
|
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;
|
|
26894
27065
|
}
|
|
26895
|
-
|
|
26896
|
-
|
|
26897
|
-
const snippet = agentSnippet(tags, { ...scope, agent });
|
|
26898
|
-
writeSkill(skillPath, snippet);
|
|
26899
|
-
if (appendPath === void 0) shown = { agent, snippet };
|
|
27066
|
+
prompt(label) {
|
|
27067
|
+
return this.#read(label, false);
|
|
26900
27068
|
}
|
|
26901
|
-
|
|
26902
|
-
|
|
26903
|
-
writeJson(io.stdout, {
|
|
26904
|
-
snippet: shown.snippet,
|
|
26905
|
-
agent: shown.agent,
|
|
26906
|
-
appended: appendPath ?? null,
|
|
26907
|
-
skill: skillPath ?? null,
|
|
26908
|
-
hook: hook?.path ?? null
|
|
26909
|
-
});
|
|
26910
|
-
return 0;
|
|
27069
|
+
promptHidden(label) {
|
|
27070
|
+
return this.#read(label, true);
|
|
26911
27071
|
}
|
|
26912
|
-
|
|
26913
|
-
|
|
27072
|
+
close() {
|
|
27073
|
+
this.#closed = true;
|
|
27074
|
+
this.#rl?.close();
|
|
27075
|
+
this.#rl = void 0;
|
|
26914
27076
|
}
|
|
26915
|
-
|
|
26916
|
-
|
|
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;
|
|
26917
27120
|
}
|
|
26918
|
-
|
|
26919
|
-
|
|
26920
|
-
|
|
26921
|
-
|
|
26922
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
function agentLead(agent) {
|
|
26926
|
-
return agent === DEFAULT_SNIPPET_AGENT ? "Add this to your CLAUDE.md:" : "Add this to your agent's instructions file:";
|
|
26927
|
-
}
|
|
26928
|
-
async function runInit(options, io) {
|
|
26929
|
-
const directory = resolve3(io.cwd);
|
|
26930
|
-
const path = join7(directory, PROJECT_CONFIG_FILENAME);
|
|
26931
|
-
if (existsSync2(path) && options.force !== true) {
|
|
26932
|
-
throw new ConfigError(`${path} already exists: pass --force to overwrite it`, path);
|
|
26933
|
-
}
|
|
26934
|
-
const skillPath = options.skill === true ? join7(directory, SKILL_PATH) : void 0;
|
|
26935
|
-
if (skillPath !== void 0 && existsSync2(skillPath) && options.force !== true) {
|
|
26936
|
-
throw existsError(skillPath);
|
|
26937
|
-
}
|
|
26938
|
-
const project = resolveProjectName(options.project, directory);
|
|
26939
|
-
const org = options.org === void 0 || options.org.trim() === "" ? void 0 : parseOrgSlug(options.org);
|
|
26940
|
-
const tags = options.tag === void 0 ? void 0 : parseAllowedTags(options.tag);
|
|
26941
|
-
const agent = options.agent === void 0 ? DEFAULT_SNIPPET_AGENT : parseAgentName(options.agent);
|
|
26942
|
-
const hook = options.hook === true ? planHook(directory) : void 0;
|
|
26943
|
-
const endpoint = await endpointToWrite(options.endpoint, io);
|
|
26944
|
-
const config2 = {
|
|
26945
|
-
project,
|
|
26946
|
-
...org === void 0 ? {} : { org },
|
|
26947
|
-
...endpoint === void 0 ? {} : { endpoint },
|
|
26948
|
-
...tags === void 0 ? {} : { tags }
|
|
26949
|
-
};
|
|
26950
|
-
writeFileSync5(path, `${JSON.stringify(config2, null, 2)}
|
|
26951
|
-
`, "utf8");
|
|
26952
|
-
const context = {
|
|
26953
|
-
...resolveContext({
|
|
26954
|
-
cwd: io.cwd,
|
|
26955
|
-
env: io.env,
|
|
26956
|
-
endpoint: options.endpoint,
|
|
26957
|
-
org: options.org,
|
|
26958
|
-
local: options.local
|
|
26959
|
-
}),
|
|
26960
|
-
// `init` acts as nobody, so it ignores `QUIBBLE_ACT_AS`.
|
|
26961
|
-
actAs: void 0
|
|
26962
|
-
};
|
|
26963
|
-
const snippet = agentSnippet(tags, {
|
|
26964
|
-
agent,
|
|
26965
|
-
...context.mode === "remote" ? { endpoint: context.endpoint } : {},
|
|
26966
|
-
...org === void 0 ? {} : { org }
|
|
26967
|
-
});
|
|
26968
|
-
if (skillPath !== void 0) writeSkill(skillPath, snippet);
|
|
26969
|
-
const hookLine = hook === void 0 ? void 0 : applyHook(hook);
|
|
26970
|
-
const check2 = await checkServer(context, project, io);
|
|
26971
|
-
if (options.json === true) {
|
|
26972
|
-
writeJson(io.stdout, {
|
|
26973
|
-
path,
|
|
26974
|
-
project,
|
|
26975
|
-
org: org ?? null,
|
|
26976
|
-
tags: tags ?? null,
|
|
26977
|
-
skill: skillPath ?? null,
|
|
26978
|
-
hook: hook?.path ?? null,
|
|
26979
|
-
server: check2.server
|
|
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;
|
|
26980
27128
|
});
|
|
26981
|
-
return 0;
|
|
26982
27129
|
}
|
|
26983
|
-
|
|
26984
|
-
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26990
|
-
|
|
26991
|
-
|
|
26992
|
-
|
|
26993
|
-
|
|
26994
|
-
writeLine(io.stdout);
|
|
26995
|
-
writeLine(io.stdout, CODEX_LINE);
|
|
26996
|
-
}
|
|
26997
|
-
if (check2.line !== void 0) {
|
|
26998
|
-
writeLine(io.stdout);
|
|
26999
|
-
writeLine(io.stdout, check2.line);
|
|
27000
|
-
}
|
|
27001
|
-
return 0;
|
|
27002
|
-
}
|
|
27003
|
-
async function endpointToWrite(given, io) {
|
|
27004
|
-
if (given === void 0 || given.trim() === "") return void 0;
|
|
27005
|
-
const value = parseEndpoint(given, "--endpoint");
|
|
27006
|
-
if (value.kind === "local") return LOCAL_ENDPOINT;
|
|
27007
|
-
const api = await discoverApiUrl(value.url, {
|
|
27008
|
-
fetch: io.fetch,
|
|
27009
|
-
debug: debugEnabled(io.env)
|
|
27010
|
-
});
|
|
27011
|
-
return isDefaultEndpoint(api) ? void 0 : api;
|
|
27012
|
-
}
|
|
27013
|
-
async function checkServer(context, project, io) {
|
|
27014
|
-
const org = context.org;
|
|
27015
|
-
if (context.mode !== "remote" || org === void 0) return { server: "unchecked" };
|
|
27016
|
-
const endpoint = await resolveEndpoint(context, io);
|
|
27017
|
-
if (context.token === void 0 && context.credential === void 0 && !isLoopbackUrl(endpoint)) {
|
|
27018
|
-
return { server: "unchecked" };
|
|
27019
|
-
}
|
|
27020
|
-
const warn = (text) => {
|
|
27021
|
-
writeLine(io.stderr, `quibble: warning: ${text}`);
|
|
27022
|
-
};
|
|
27023
|
-
try {
|
|
27024
|
-
await (await openClient(context, io, { queue: false })).project(project);
|
|
27025
|
-
return { server: "found", line: `Project ${org}/${project} exists on ${endpoint}.` };
|
|
27026
|
-
} catch (error62) {
|
|
27027
|
-
if (error62 instanceof RemoteError && error62.status === 404) {
|
|
27028
|
-
if (error62.serverCode === ORG_NOT_FOUND) {
|
|
27029
|
-
warn(`no organization ${org} on ${endpoint} for this account`);
|
|
27030
|
-
return { server: "no_org" };
|
|
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");
|
|
27031
27141
|
}
|
|
27032
|
-
warn(
|
|
27033
|
-
`${org} has no project ${project} on ${endpoint}: an admin creates it in the organization settings`
|
|
27034
|
-
);
|
|
27035
|
-
return { server: "no_project" };
|
|
27036
27142
|
}
|
|
27037
|
-
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
|
|
27045
|
-
|
|
27046
|
-
|
|
27047
|
-
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
{ cause: error62 }
|
|
27052
|
-
);
|
|
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
|
+
}
|
|
27053
27157
|
}
|
|
27158
|
+
};
|
|
27159
|
+
function nodeTerminal(input2, output2) {
|
|
27160
|
+
return new NodeTerminal(input2, output2);
|
|
27054
27161
|
}
|
|
27055
27162
|
|
|
27056
|
-
// src/
|
|
27057
|
-
|
|
27058
|
-
|
|
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.`;
|
|
27059
27186
|
}
|
|
27060
|
-
|
|
27061
|
-
|
|
27062
|
-
if (context.reporter === DEFAULT_REPORTER) throw new UsageError(mineMessage(env));
|
|
27063
|
-
return context.reporter;
|
|
27064
|
-
}
|
|
27065
|
-
return (await client.me()).reporter;
|
|
27187
|
+
function terminalOf(io) {
|
|
27188
|
+
return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
|
|
27066
27189
|
}
|
|
27067
|
-
|
|
27068
|
-
|
|
27069
|
-
var SINCE_MESSAGE = "--since must be a duration like 7d, a date like 2026-09-01, or an ISO 8601 UTC timestamp";
|
|
27070
|
-
var DURATION_RE = /^(\d+)(m|h|d|w)$/;
|
|
27071
|
-
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
27072
|
-
var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
27073
|
-
var MINUTE_MS = 6e4;
|
|
27074
|
-
var HOUR_MS = 60 * MINUTE_MS;
|
|
27075
|
-
var DAY_MS3 = 24 * HOUR_MS;
|
|
27076
|
-
var UNIT_MS = {
|
|
27077
|
-
m: MINUTE_MS,
|
|
27078
|
-
h: HOUR_MS,
|
|
27079
|
-
d: DAY_MS3,
|
|
27080
|
-
w: 7 * DAY_MS3
|
|
27081
|
-
};
|
|
27082
|
-
var EXPIRES_MESSAGE = "--expires must be a duration like 30d, a date like 2026-12-01, or an ISO 8601 UTC timestamp";
|
|
27083
|
-
function reject(message = SINCE_MESSAGE) {
|
|
27084
|
-
throw new UsageError(message);
|
|
27190
|
+
function clockOf2(io) {
|
|
27191
|
+
return io.now ?? (() => /* @__PURE__ */ new Date());
|
|
27085
27192
|
}
|
|
27086
|
-
function
|
|
27087
|
-
|
|
27088
|
-
|
|
27193
|
+
function sleepOf(io) {
|
|
27194
|
+
return io.sleep ?? ((ms) => new Promise((resolve4) => {
|
|
27195
|
+
setTimeout(resolve4, ms);
|
|
27196
|
+
}));
|
|
27089
27197
|
}
|
|
27090
|
-
function
|
|
27091
|
-
|
|
27092
|
-
|
|
27093
|
-
return step === void 0 ? void 0 : Number(digits) * step;
|
|
27198
|
+
function ciIsSet(env) {
|
|
27199
|
+
const value = (env["CI"] ?? "").trim().toLowerCase();
|
|
27200
|
+
return value !== "" && value !== "0" && value !== "false";
|
|
27094
27201
|
}
|
|
27095
|
-
function
|
|
27096
|
-
const
|
|
27097
|
-
|
|
27098
|
-
|
|
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;
|
|
27099
27208
|
}
|
|
27100
|
-
function
|
|
27101
|
-
|
|
27102
|
-
|
|
27103
|
-
|
|
27104
|
-
return stamp2;
|
|
27209
|
+
function unsupportedStep(name) {
|
|
27210
|
+
return new RemoteError(
|
|
27211
|
+
`this account needs a sign-in step that quibble doesn't support (${name})`
|
|
27212
|
+
);
|
|
27105
27213
|
}
|
|
27106
|
-
function
|
|
27107
|
-
|
|
27108
|
-
|
|
27109
|
-
|
|
27110
|
-
|
|
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
|
+
}
|
|
27111
27223
|
}
|
|
27112
|
-
|
|
27113
|
-
if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
|
|
27114
|
-
return reject();
|
|
27224
|
+
return error62;
|
|
27115
27225
|
}
|
|
27116
|
-
function
|
|
27117
|
-
const
|
|
27118
|
-
const
|
|
27119
|
-
|
|
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;
|
|
27120
27230
|
try {
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
27125
|
-
|
|
27126
|
-
|
|
27127
|
-
|
|
27128
|
-
|
|
27129
|
-
|
|
27130
|
-
|
|
27231
|
+
let result = await initiateAuth(identity, {
|
|
27232
|
+
clientId,
|
|
27233
|
+
flow: "USER_PASSWORD_AUTH",
|
|
27234
|
+
username: email3,
|
|
27235
|
+
password
|
|
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
|
+
});
|
|
27131
27245
|
}
|
|
27132
|
-
|
|
27133
|
-
|
|
27134
|
-
|
|
27135
|
-
|
|
27136
|
-
}
|
|
27137
|
-
|
|
27138
|
-
|
|
27139
|
-
|
|
27140
|
-
|
|
27141
|
-
|
|
27142
|
-
}
|
|
27143
|
-
|
|
27144
|
-
throw new UsageError("--open and --status cannot be used together");
|
|
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);
|
|
27145
27258
|
}
|
|
27146
|
-
const project = options.project ?? (options.all === true ? void 0 : context.project);
|
|
27147
|
-
const status = options.open === true ? "open" : options.status;
|
|
27148
|
-
return {
|
|
27149
|
-
// No list reads two organizations, because the path names one.
|
|
27150
|
-
org: requireOrg(context),
|
|
27151
|
-
...project === void 0 ? {} : { project },
|
|
27152
|
-
...status === void 0 ? {} : { status },
|
|
27153
|
-
...options.since === void 0 ? {} : { since: parseSince(options.since, now) }
|
|
27154
|
-
};
|
|
27155
|
-
}
|
|
27156
|
-
function tagFilter(tags) {
|
|
27157
|
-
return tags === void 0 || tags.length === 0 ? {} : { tags: [...tags] };
|
|
27158
|
-
}
|
|
27159
|
-
|
|
27160
|
-
// src/commands/list.ts
|
|
27161
|
-
var COLUMNS = [
|
|
27162
|
-
"id",
|
|
27163
|
-
"created",
|
|
27164
|
-
"severity",
|
|
27165
|
-
"status",
|
|
27166
|
-
"project",
|
|
27167
|
-
"reporter",
|
|
27168
|
-
"tags",
|
|
27169
|
-
"group",
|
|
27170
|
-
"summary"
|
|
27171
|
-
];
|
|
27172
|
-
var REPORTER_WIDTH = 20;
|
|
27173
|
-
var TAGS_WIDTH = 24;
|
|
27174
|
-
var SUMMARY_MAX = 60;
|
|
27175
|
-
var RECURRED_MARK = "\u21BB";
|
|
27176
|
-
function cut3(text, width) {
|
|
27177
|
-
if (text.length <= width) return text;
|
|
27178
|
-
return `${text.slice(0, width - 3)}...`;
|
|
27179
27259
|
}
|
|
27180
|
-
function
|
|
27181
|
-
|
|
27182
|
-
|
|
27183
|
-
|
|
27184
|
-
|
|
27185
|
-
return count >= MAX_LIMIT ? `Showing the newest ${MAX_LIMIT} complaints: older ones may exist, narrow the filters to see them.` : void 0;
|
|
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);
|
|
27186
27265
|
}
|
|
27187
|
-
return
|
|
27266
|
+
return browserLogin(options, context, io);
|
|
27188
27267
|
}
|
|
27189
|
-
|
|
27190
|
-
|
|
27191
|
-
if (
|
|
27192
|
-
|
|
27193
|
-
}
|
|
27194
|
-
const
|
|
27195
|
-
|
|
27196
|
-
|
|
27197
|
-
...resolveScope(options, context),
|
|
27198
|
-
...options.severity === void 0 ? {} : { severity: options.severity },
|
|
27199
|
-
...tagFilter(options.tag),
|
|
27200
|
-
...options.search === void 0 ? {} : { q: options.search },
|
|
27201
|
-
...reporter === void 0 ? {} : { reporter },
|
|
27202
|
-
...options.limit === void 0 ? {} : { limit: options.limit }
|
|
27203
|
-
});
|
|
27204
|
-
const limit = filter.limit ?? DEFAULT_LIMIT;
|
|
27205
|
-
const page = await listComplaintPage(store, {
|
|
27206
|
-
...filter,
|
|
27207
|
-
limit: Math.min(limit + 1, MAX_LIMIT)
|
|
27208
|
-
});
|
|
27209
|
-
const complaints = page.complaints.slice(0, limit);
|
|
27210
|
-
if (options.json === true) {
|
|
27211
|
-
writeJson(io.stdout, complaints);
|
|
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));
|
|
27212
27276
|
return 0;
|
|
27213
27277
|
}
|
|
27214
|
-
|
|
27215
|
-
|
|
27216
|
-
|
|
27217
|
-
|
|
27218
|
-
|
|
27219
|
-
return 0;
|
|
27278
|
+
let tokens;
|
|
27279
|
+
try {
|
|
27280
|
+
tokens = await signIn(terminal, { region: config2.region, ...http }, config2);
|
|
27281
|
+
} finally {
|
|
27282
|
+
terminal.close();
|
|
27220
27283
|
}
|
|
27221
|
-
|
|
27222
|
-
|
|
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));
|
|
27223
27293
|
return 0;
|
|
27224
27294
|
}
|
|
27225
|
-
|
|
27226
|
-
groupComplaints(complaints).map((group) => ({
|
|
27227
|
-
complaint: group.head,
|
|
27228
|
-
group
|
|
27229
|
-
}))
|
|
27230
|
-
);
|
|
27231
|
-
for (const line of lines) writeLine(io.stdout, line);
|
|
27232
|
-
return 0;
|
|
27295
|
+
return reportLogin(session.me, endpoint, options, context, io);
|
|
27233
27296
|
}
|
|
27234
|
-
function
|
|
27235
|
-
|
|
27236
|
-
|
|
27237
|
-
|
|
27238
|
-
const
|
|
27239
|
-
|
|
27240
|
-
|
|
27241
|
-
|
|
27242
|
-
|
|
27243
|
-
const columns = COLUMNS.filter((name) => grouped || name !== "group");
|
|
27244
|
-
const cells = rows2.map(({ complaint, group }) => {
|
|
27245
|
-
const values = {
|
|
27246
|
-
id: complaint.id.slice(0, 8),
|
|
27247
|
-
created: complaint.createdAt.slice(0, 16),
|
|
27248
|
-
severity: complaint.severity,
|
|
27249
|
-
status: complaint.status,
|
|
27250
|
-
project: complaint.project,
|
|
27251
|
-
reporter: reporterOf(complaint),
|
|
27252
|
-
tags: cut3(printable(complaint.tags.join(",")), TAGS_WIDTH),
|
|
27253
|
-
group: group === void 0 ? "" : groupCell(group),
|
|
27254
|
-
summary: summaryOf(complaint)
|
|
27255
|
-
};
|
|
27256
|
-
return columns.map((name) => values[name]);
|
|
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
|
|
27257
27306
|
});
|
|
27258
|
-
const
|
|
27259
|
-
|
|
27260
|
-
|
|
27261
|
-
|
|
27262
|
-
)
|
|
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...`
|
|
27263
27311
|
);
|
|
27264
|
-
|
|
27265
|
-
|
|
27266
|
-
|
|
27267
|
-
|
|
27268
|
-
// src/commands/orgs.ts
|
|
27269
|
-
var ORGS_PAGE = "/orgs";
|
|
27270
|
-
var COLUMNS2 = ["SLUG", "NAME", "ROLE", "OPEN"];
|
|
27271
|
-
async function runOrgs(options, context, io) {
|
|
27272
|
-
requireServer("orgs", context);
|
|
27273
|
-
const client = await openClient(context, io);
|
|
27274
|
-
const endpoint = requireEndpoint(context);
|
|
27275
|
-
const answer2 = await client.orgs();
|
|
27276
|
-
if (options.json === true) {
|
|
27277
|
-
writeJson(io.stdout, answer2.body);
|
|
27278
|
-
return 0;
|
|
27279
|
-
}
|
|
27280
|
-
if (answer2.orgs.length === 0) {
|
|
27281
|
-
const config2 = await fetchAuthConfig(endpoint, {
|
|
27282
|
-
debug: context.debug,
|
|
27283
|
-
fetch: io.fetch,
|
|
27284
|
-
timeoutMs: context.timeoutMs
|
|
27285
|
-
});
|
|
27286
|
-
const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
|
|
27287
|
-
writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE}.`);
|
|
27288
|
-
return 0;
|
|
27312
|
+
if (options.browser !== false) {
|
|
27313
|
+
const opener = io.opener ?? platformOpener({ env: io.env });
|
|
27314
|
+
void opener.open(started.url).catch(() => false);
|
|
27289
27315
|
}
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
}
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
27296
|
-
entry.name,
|
|
27297
|
-
entry.role,
|
|
27298
|
-
entry.openCount === void 0 ? "" : String(entry.openCount)
|
|
27299
|
-
]);
|
|
27300
|
-
const widths = COLUMNS2.map(
|
|
27301
|
-
(name, column) => rows2.reduce((width, row) => Math.max(width, (row[column] ?? "").length), name.length)
|
|
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
|
|
27302
27322
|
);
|
|
27303
|
-
const
|
|
27304
|
-
|
|
27305
|
-
).join(" ").trimEnd();
|
|
27306
|
-
return [format(COLUMNS2), ...rows2.map(format)];
|
|
27323
|
+
const me = await storeSession(tokens, endpoint, config2, http, io);
|
|
27324
|
+
return { me, webUrl: config2.webUrl };
|
|
27307
27325
|
}
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27312
|
-
|
|
27313
|
-
|
|
27314
|
-
|
|
27315
|
-
var OPENABLE_URL_RE = /^https?:\/\/[A-Za-z0-9.:[\]-]+(?:\/[A-Za-z0-9/?=._~-]*)?$/;
|
|
27316
|
-
function isWsl(platform, env) {
|
|
27317
|
-
if (platform !== "linux") return false;
|
|
27318
|
-
const set2 = (name) => (env[name] ?? "") !== "";
|
|
27319
|
-
return set2("WSL_DISTRO_NAME") || set2("WSL_INTEROP");
|
|
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
|
+
});
|
|
27320
27333
|
}
|
|
27321
|
-
function
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
if (
|
|
27327
|
-
|
|
27328
|
-
{
|
|
27329
|
-
{
|
|
27330
|
-
|
|
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
|
+
);
|
|
27331
27344
|
}
|
|
27332
|
-
|
|
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;
|
|
27333
27351
|
}
|
|
27334
|
-
function
|
|
27335
|
-
|
|
27336
|
-
|
|
27337
|
-
|
|
27338
|
-
|
|
27339
|
-
|
|
27340
|
-
|
|
27341
|
-
|
|
27342
|
-
|
|
27343
|
-
|
|
27344
|
-
|
|
27345
|
-
|
|
27346
|
-
|
|
27347
|
-
|
|
27348
|
-
|
|
27349
|
-
|
|
27350
|
-
|
|
27351
|
-
|
|
27352
|
-
|
|
27353
|
-
|
|
27354
|
-
|
|
27355
|
-
|
|
27356
|
-
|
|
27357
|
-
|
|
27358
|
-
}
|
|
27359
|
-
child.on("error", () => {
|
|
27360
|
-
settle2(false);
|
|
27361
|
-
});
|
|
27362
|
-
child.on("exit", (code2) => {
|
|
27363
|
-
settle2(code2 === 0);
|
|
27364
|
-
});
|
|
27365
|
-
child.unref();
|
|
27366
|
-
});
|
|
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;
|
|
27358
|
+
try {
|
|
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 };
|
|
27367
27376
|
}
|
|
27368
|
-
function
|
|
27369
|
-
const
|
|
27370
|
-
const
|
|
27371
|
-
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
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 };
|
|
27378
27403
|
}
|
|
27379
|
-
|
|
27404
|
+
default:
|
|
27405
|
+
throw new RemoteError(UNKNOWN_STATUS_MESSAGE);
|
|
27380
27406
|
}
|
|
27381
|
-
};
|
|
27382
|
-
}
|
|
27383
|
-
|
|
27384
|
-
// src/terminal.ts
|
|
27385
|
-
import { createInterface } from "node:readline";
|
|
27386
|
-
import { Writable } from "node:stream";
|
|
27387
|
-
var NodeTerminal = class {
|
|
27388
|
-
isTTY;
|
|
27389
|
-
#input;
|
|
27390
|
-
#output;
|
|
27391
|
-
#rl;
|
|
27392
|
-
/**
|
|
27393
|
-
* True unless a visible prompt is waiting, so a line that arrives early,
|
|
27394
|
-
* a pasted password after the email for one, echoes nowhere.
|
|
27395
|
-
*/
|
|
27396
|
-
#muted = true;
|
|
27397
|
-
/** Lines that arrived before a prompt asked for them. */
|
|
27398
|
-
#lines = [];
|
|
27399
|
-
#waiting;
|
|
27400
|
-
#closed = false;
|
|
27401
|
-
constructor(input2, output2) {
|
|
27402
|
-
this.#input = input2;
|
|
27403
|
-
this.#output = output2;
|
|
27404
|
-
this.isTTY = input2.isTTY === true;
|
|
27405
27407
|
}
|
|
27406
|
-
|
|
27407
|
-
|
|
27408
|
-
}
|
|
27409
|
-
|
|
27410
|
-
|
|
27408
|
+
}
|
|
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;
|
|
27414
|
+
try {
|
|
27415
|
+
response = await postJson(url2, { loginKey }, http);
|
|
27416
|
+
text = await readText(response, site);
|
|
27417
|
+
} catch (error62) {
|
|
27418
|
+
if (error62 instanceof RemoteError && error62.unanswered) return void 0;
|
|
27419
|
+
throw error62;
|
|
27411
27420
|
}
|
|
27412
|
-
|
|
27413
|
-
|
|
27414
|
-
|
|
27415
|
-
this.#rl = void 0;
|
|
27421
|
+
if (!response.ok) {
|
|
27422
|
+
if (response.status >= 500 || response.status === 429) return void 0;
|
|
27423
|
+
throw remoteFailure(site, response, text);
|
|
27416
27424
|
}
|
|
27417
|
-
|
|
27418
|
-
|
|
27419
|
-
|
|
27420
|
-
|
|
27421
|
-
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
});
|
|
27427
|
-
Object.defineProperties(sink2, {
|
|
27428
|
-
columns: { get: () => output2.columns },
|
|
27429
|
-
rows: { get: () => output2.rows }
|
|
27430
|
-
});
|
|
27431
|
-
const rl = createInterface({
|
|
27432
|
-
input: this.#input,
|
|
27433
|
-
output: sink2,
|
|
27434
|
-
terminal: true,
|
|
27435
|
-
// No answer enters the history, so an arrow key recalls no password.
|
|
27436
|
-
historySize: 0
|
|
27437
|
-
});
|
|
27438
|
-
rl.on("line", (line) => {
|
|
27439
|
-
this.#muted = true;
|
|
27440
|
-
if (this.#waiting !== void 0) {
|
|
27441
|
-
const settle2 = this.#waiting;
|
|
27442
|
-
this.#waiting = void 0;
|
|
27443
|
-
settle2(line);
|
|
27444
|
-
return;
|
|
27445
|
-
}
|
|
27446
|
-
this.#lines.push(line);
|
|
27447
|
-
});
|
|
27448
|
-
rl.on("close", () => {
|
|
27449
|
-
this.#closed = true;
|
|
27450
|
-
this.#waiting?.("");
|
|
27451
|
-
this.#waiting = void 0;
|
|
27452
|
-
});
|
|
27453
|
-
rl.on("SIGINT", () => {
|
|
27454
|
-
this.close();
|
|
27455
|
-
this.#output.write("\n");
|
|
27456
|
-
process.kill(process.pid, "SIGINT");
|
|
27425
|
+
return asRecord(parseJson(text, site, response.status)) ?? {};
|
|
27426
|
+
}
|
|
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
|
|
27457
27434
|
});
|
|
27458
|
-
|
|
27459
|
-
|
|
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
27460
|
}
|
|
27461
|
-
|
|
27462
|
-
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
if (
|
|
27466
|
-
|
|
27467
|
-
|
|
27461
|
+
}
|
|
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
|
|
27468
27472
|
});
|
|
27473
|
+
} finally {
|
|
27474
|
+
terminal.close();
|
|
27469
27475
|
}
|
|
27470
|
-
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
|
|
27479
|
-
|
|
27480
|
-
|
|
27481
|
-
|
|
27482
|
-
|
|
27483
|
-
|
|
27484
|
-
|
|
27485
|
-
|
|
27486
|
-
|
|
27487
|
-
|
|
27488
|
-
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
|
|
27495
|
-
|
|
27476
|
+
}
|
|
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;
|
|
27504
|
+
}
|
|
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;
|
|
27516
|
+
}
|
|
27517
|
+
function loggedInLine(endpoint, me) {
|
|
27518
|
+
return `Logged in to ${endpoint} as ${me.email ?? me.reporter}.`;
|
|
27519
|
+
}
|
|
27520
|
+
function saveEndpoint(context, endpoint, env) {
|
|
27521
|
+
if (context.endpointSource !== "--endpoint" || isDefaultEndpoint(endpoint)) {
|
|
27522
|
+
return void 0;
|
|
27523
|
+
}
|
|
27524
|
+
return rememberEndpoint(env, endpoint);
|
|
27525
|
+
}
|
|
27526
|
+
async function readProfile(client, webUrl) {
|
|
27527
|
+
try {
|
|
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}`);
|
|
27496
27532
|
}
|
|
27533
|
+
throw error62;
|
|
27497
27534
|
}
|
|
27498
|
-
};
|
|
27499
|
-
function nodeTerminal(input2, output2) {
|
|
27500
|
-
return new NodeTerminal(input2, output2);
|
|
27501
27535
|
}
|
|
27502
|
-
|
|
27503
|
-
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
27517
|
-
|
|
27518
|
-
|
|
27519
|
-
|
|
27520
|
-
var PLAIN_HOST_RE = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::[0-9]+)?$/;
|
|
27521
|
-
var LOGIN_PAGE = "/login";
|
|
27522
|
-
var ORGS_PAGE2 = "/orgs";
|
|
27523
|
-
var NO_PROFILE_STATUSES = /* @__PURE__ */ new Set([404, 409]);
|
|
27524
|
-
function terminalOf(io) {
|
|
27525
|
-
return io.terminal ?? nodeTerminal(io.stdin, io.stderr);
|
|
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;
|
|
27543
|
+
}
|
|
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
|
+
);
|
|
27550
|
+
}
|
|
27551
|
+
removeCredential(io.env, endpoint);
|
|
27552
|
+
writeLine(io.stdout, `Logged out of ${endpoint}.`);
|
|
27553
|
+
return 0;
|
|
27526
27554
|
}
|
|
27527
|
-
function
|
|
27528
|
-
|
|
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;
|
|
27566
|
+
}
|
|
27529
27567
|
}
|
|
27530
|
-
function
|
|
27531
|
-
|
|
27532
|
-
|
|
27533
|
-
|
|
27568
|
+
async function runWhoami(options, context, io) {
|
|
27569
|
+
if (context.mode === "local") {
|
|
27570
|
+
checkActAs(context);
|
|
27571
|
+
writeLine(io.stdout, LOCAL_IDENTITY_MESSAGE);
|
|
27572
|
+
return 0;
|
|
27573
|
+
}
|
|
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
|
+
});
|
|
27584
|
+
return 0;
|
|
27585
|
+
}
|
|
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}.`);
|
|
27594
|
+
}
|
|
27595
|
+
for (const line of lines) writeLine(io.stdout, line);
|
|
27596
|
+
return 0;
|
|
27534
27597
|
}
|
|
27535
|
-
function
|
|
27536
|
-
const
|
|
27537
|
-
return
|
|
27598
|
+
function rows(pairs) {
|
|
27599
|
+
const width = Math.max(...pairs.map(([key]) => key.length));
|
|
27600
|
+
return pairs.map(([key, value]) => `${key.padEnd(width)} ${value}`);
|
|
27538
27601
|
}
|
|
27539
|
-
|
|
27540
|
-
|
|
27541
|
-
const
|
|
27542
|
-
|
|
27543
|
-
throw new UsageError(`${label.replace(/:$/, "").toLowerCase()} must not be empty`);
|
|
27544
|
-
return value;
|
|
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}`);
|
|
27545
27606
|
}
|
|
27546
|
-
function
|
|
27547
|
-
|
|
27548
|
-
|
|
27549
|
-
|
|
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;
|
|
27550
27630
|
}
|
|
27551
|
-
|
|
27552
|
-
|
|
27553
|
-
|
|
27554
|
-
|
|
27555
|
-
|
|
27556
|
-
|
|
27557
|
-
if (error62.type === "UserNotConfirmedException") {
|
|
27558
|
-
return new ConfigError(`finish your sign-up at ${page}`);
|
|
27559
|
-
}
|
|
27560
|
-
}
|
|
27561
|
-
return error62;
|
|
27631
|
+
|
|
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`);
|
|
27562
27637
|
}
|
|
27563
|
-
|
|
27564
|
-
const email3 = await answer(terminal, "Email:");
|
|
27565
|
-
const password = await answer(terminal, "Password:", true);
|
|
27566
|
-
const clientId = config2.cliClientId;
|
|
27638
|
+
function readOptional2(path) {
|
|
27567
27639
|
try {
|
|
27568
|
-
|
|
27569
|
-
clientId,
|
|
27570
|
-
flow: "USER_PASSWORD_AUTH",
|
|
27571
|
-
username: email3,
|
|
27572
|
-
password
|
|
27573
|
-
});
|
|
27574
|
-
if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
|
|
27575
|
-
const code2 = await answer(terminal, "Code:");
|
|
27576
|
-
result = await respondToAuthChallenge(identity, {
|
|
27577
|
-
clientId,
|
|
27578
|
-
challenge: result.challenge,
|
|
27579
|
-
email: email3,
|
|
27580
|
-
code: code2
|
|
27581
|
-
});
|
|
27582
|
-
}
|
|
27583
|
-
if (result.kind === "challenge") {
|
|
27584
|
-
const name = result.challenge.challengeName;
|
|
27585
|
-
if (name === NEW_PASSWORD_CHALLENGE) {
|
|
27586
|
-
throw new ConfigError(
|
|
27587
|
-
`finish your first sign-in at ${trimSlash2(config2.webUrl)}${LOGIN_PAGE}`
|
|
27588
|
-
);
|
|
27589
|
-
}
|
|
27590
|
-
throw unsupportedStep(name);
|
|
27591
|
-
}
|
|
27592
|
-
return result.tokens;
|
|
27640
|
+
return readFileSync5(path, "utf8");
|
|
27593
27641
|
} catch (error62) {
|
|
27594
|
-
|
|
27642
|
+
if (isNotFound(error62)) return "";
|
|
27643
|
+
throw error62;
|
|
27595
27644
|
}
|
|
27596
27645
|
}
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
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;
|
|
27602
27657
|
}
|
|
27603
|
-
|
|
27658
|
+
const scope = {
|
|
27659
|
+
...context.mode === "remote" ? { endpoint: context.endpoint } : {},
|
|
27660
|
+
...context.slug === void 0 ? {} : { org: context.slug }
|
|
27661
|
+
};
|
|
27662
|
+
return { tags, scope };
|
|
27604
27663
|
}
|
|
27605
|
-
async function
|
|
27606
|
-
const
|
|
27607
|
-
|
|
27608
|
-
const
|
|
27609
|
-
const
|
|
27610
|
-
const
|
|
27611
|
-
if (
|
|
27612
|
-
|
|
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
|
|
27707
|
+
});
|
|
27613
27708
|
return 0;
|
|
27614
27709
|
}
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27618
|
-
|
|
27619
|
-
|
|
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}`);
|
|
27620
27715
|
}
|
|
27621
|
-
|
|
27716
|
+
if (skillPath !== void 0) writeLine(io.stdout, `Wrote ${skillPath}`);
|
|
27717
|
+
if (hookLine !== void 0) writeLine(io.stdout, hookLine);
|
|
27718
|
+
return 0;
|
|
27622
27719
|
}
|
|
27623
|
-
|
|
27624
|
-
|
|
27625
|
-
|
|
27626
|
-
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27630
|
-
|
|
27720
|
+
|
|
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);
|
|
27631
27740
|
}
|
|
27632
|
-
const
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
os: process.platform,
|
|
27636
|
-
version: options.version
|
|
27637
|
-
});
|
|
27638
|
-
const deadline = clock().getTime() + (CLI_LOGIN_TTL_SECONDS + started.interval) * 1e3;
|
|
27639
|
-
writeLine(
|
|
27640
|
-
io.stderr,
|
|
27641
|
-
`Approve this login in your browser: ${started.url} (code ${started.code}). Waiting...`
|
|
27642
|
-
);
|
|
27643
|
-
if (options.browser !== false) {
|
|
27644
|
-
const opener = io.opener ?? platformOpener({ env: io.env });
|
|
27645
|
-
void opener.open(started.url).catch(() => false);
|
|
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);
|
|
27646
27744
|
}
|
|
27647
|
-
const
|
|
27648
|
-
const
|
|
27649
|
-
|
|
27650
|
-
|
|
27651
|
-
|
|
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
|
+
}
|
|
27774
|
+
}
|
|
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;
|
|
27780
|
+
}
|
|
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,
|
|
27652
27809
|
io
|
|
27653
|
-
);
|
|
27654
|
-
return finishLogin(tokens, endpoint, config2, options, context, io);
|
|
27655
|
-
}
|
|
27656
|
-
function postJson(url2, body, http) {
|
|
27657
|
-
return send(url2, {
|
|
27658
|
-
method: "POST",
|
|
27659
|
-
headers: { "content-type": "application/json" },
|
|
27660
|
-
body: JSON.stringify(body),
|
|
27661
|
-
...http
|
|
27662
27810
|
});
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
27667
|
-
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
);
|
|
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
|
|
27821
|
+
});
|
|
27822
|
+
return 0;
|
|
27823
|
+
}
|
|
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);
|
|
27674
27837
|
}
|
|
27675
|
-
if (
|
|
27676
|
-
|
|
27677
|
-
|
|
27678
|
-
throw new RemoteError(`${MALFORMED_MESSAGE}${detail(site, response.status)}`);
|
|
27838
|
+
if (check2.line !== void 0) {
|
|
27839
|
+
writeLine(io.stdout);
|
|
27840
|
+
writeLine(io.stdout, check2.line);
|
|
27679
27841
|
}
|
|
27680
|
-
return
|
|
27842
|
+
return 0;
|
|
27681
27843
|
}
|
|
27682
|
-
function
|
|
27683
|
-
|
|
27684
|
-
const
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
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;
|
|
27853
|
+
}
|
|
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);
|
|
27694
27859
|
try {
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27700
|
-
|
|
27860
|
+
return { project: parseProjectName(name), source: "directory" };
|
|
27861
|
+
} catch (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
|
+
);
|
|
27701
27867
|
}
|
|
27702
|
-
if (url2 !== cliLoginUrl(origin, code2)) return void 0;
|
|
27703
|
-
const raw = body?.["interval"];
|
|
27704
|
-
const interval = typeof raw === "number" && Number.isInteger(raw) ? Math.min(MAX_INTERVAL_SECONDS, Math.max(MIN_INTERVAL_SECONDS, raw)) : CLI_LOGIN_POLL_SECONDS;
|
|
27705
|
-
return { loginKey, code: code2, url: url2, interval };
|
|
27706
27868
|
}
|
|
27707
|
-
async function
|
|
27708
|
-
const
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
if (
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
const
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
const username = answer2["username"];
|
|
27725
|
-
const secret = answer2["secret"];
|
|
27726
|
-
if (typeof username !== "string" || username === "") {
|
|
27727
|
-
throw new RemoteError(MALFORMED_MESSAGE);
|
|
27728
|
-
}
|
|
27729
|
-
if (typeof secret !== "string" || secret === "") {
|
|
27730
|
-
throw new RemoteError(MALFORMED_MESSAGE);
|
|
27731
|
-
}
|
|
27732
|
-
return { username, secret };
|
|
27733
|
-
}
|
|
27734
|
-
default:
|
|
27735
|
-
throw new RemoteError(UNKNOWN_STATUS_MESSAGE);
|
|
27869
|
+
async function logIn(context, options, io) {
|
|
27870
|
+
const endpoint = await resolveEndpoint(context, io);
|
|
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";
|
|
27736
27886
|
}
|
|
27887
|
+
writeLine(io.stderr, loggedInLine(endpoint, session.me));
|
|
27888
|
+
webUrl = session.webUrl;
|
|
27889
|
+
context.credential = findCredential(io.env, endpoint);
|
|
27737
27890
|
}
|
|
27891
|
+
const client = await openClient(context, io, { queue: false });
|
|
27892
|
+
return { endpoint, client, webUrl };
|
|
27738
27893
|
}
|
|
27739
|
-
async function
|
|
27740
|
-
const
|
|
27741
|
-
|
|
27742
|
-
let response;
|
|
27743
|
-
let text;
|
|
27894
|
+
async function findOrganization(client, remote, context, io) {
|
|
27895
|
+
const endpoint = remote.endpoint;
|
|
27896
|
+
let me;
|
|
27744
27897
|
try {
|
|
27745
|
-
|
|
27746
|
-
text = await readText(response, site);
|
|
27898
|
+
me = await client.me();
|
|
27747
27899
|
} catch (error62) {
|
|
27748
|
-
if (error62 instanceof RemoteError && error62.
|
|
27749
|
-
|
|
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 };
|
|
27750
27909
|
}
|
|
27751
|
-
if (
|
|
27752
|
-
|
|
27753
|
-
|
|
27910
|
+
if (me.kind === "token") {
|
|
27911
|
+
return {
|
|
27912
|
+
org: me.org,
|
|
27913
|
+
project: me.project === void 0 ? void 0 : parseProjectName(me.project)
|
|
27914
|
+
};
|
|
27754
27915
|
}
|
|
27755
|
-
|
|
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`
|
|
27924
|
+
);
|
|
27925
|
+
}
|
|
27926
|
+
throw new UsageError(
|
|
27927
|
+
`${who} belongs to several organizations on ${endpoint}: ${slugs.join(", ")}. Pass --org <slug>`
|
|
27928
|
+
);
|
|
27756
27929
|
}
|
|
27757
|
-
async function
|
|
27758
|
-
|
|
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
|
|
27935
|
+
});
|
|
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
|
+
};
|
|
27759
27947
|
try {
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
|
|
27764
|
-
|
|
27765
|
-
|
|
27766
|
-
const name = result.challenge.challengeName;
|
|
27767
|
-
if (name !== CUSTOM_CHALLENGE) throw unsupportedStep(name);
|
|
27768
|
-
result = await respondToCustomChallenge(identity, {
|
|
27769
|
-
clientId,
|
|
27770
|
-
challenge: result.challenge,
|
|
27771
|
-
username: approval.username,
|
|
27772
|
-
answer: approval.secret
|
|
27773
|
-
});
|
|
27774
|
-
}
|
|
27775
|
-
if (result.kind === "challenge" && result.challenge.challengeName === MFA_CHALLENGE) {
|
|
27776
|
-
result = await answerCode(
|
|
27777
|
-
identity,
|
|
27778
|
-
clientId,
|
|
27779
|
-
result.challenge,
|
|
27780
|
-
approval.username,
|
|
27781
|
-
io
|
|
27782
|
-
);
|
|
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" };
|
|
27783
27954
|
}
|
|
27784
|
-
if (
|
|
27785
|
-
|
|
27955
|
+
if (error62.serverCode === ORG_NOT_FOUND) {
|
|
27956
|
+
warn(`no organization ${org} on ${endpoint} for this account`);
|
|
27957
|
+
return { server: "no_org" };
|
|
27786
27958
|
}
|
|
27787
|
-
return result.tokens;
|
|
27788
|
-
} catch (error62) {
|
|
27789
|
-
throw pageError(error62, config2.webUrl);
|
|
27790
27959
|
}
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
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();
|
|
27794
27967
|
try {
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
|
|
27804
|
-
|
|
27805
|
-
|
|
27806
|
-
}
|
|
27807
|
-
async function finishLogin(tokens, endpoint, config2, options, context, io) {
|
|
27808
|
-
if (tokens.refreshToken === void 0) {
|
|
27809
|
-
throw new RemoteError("sign-in didn't return a refresh token");
|
|
27810
|
-
}
|
|
27811
|
-
const now = clockOf2(io)();
|
|
27812
|
-
const me = await readProfile(
|
|
27813
|
-
new ApiClient({
|
|
27814
|
-
endpoint,
|
|
27815
|
-
authorizer: bearerAuthorizer(tokens.accessToken),
|
|
27816
|
-
debug: context.debug,
|
|
27817
|
-
fetch: io.fetch
|
|
27818
|
-
}),
|
|
27819
|
-
config2.webUrl
|
|
27820
|
-
);
|
|
27821
|
-
if (me.sub === void 0 || me.email === void 0) {
|
|
27822
|
-
throw new RemoteError(
|
|
27823
|
-
"unexpected response from the API: identity has no sub or email"
|
|
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)}`
|
|
27824
27980
|
);
|
|
27981
|
+
return { server: "no_project" };
|
|
27825
27982
|
}
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
|
|
27833
|
-
|
|
27834
|
-
|
|
27835
|
-
|
|
27836
|
-
writeJson(io.stdout, { ...me.body, endpoint, saved: saved ?? null });
|
|
27837
|
-
return 0;
|
|
27838
|
-
}
|
|
27839
|
-
writeLine(io.stdout, `Logged in to ${endpoint} as ${me.email}.`);
|
|
27840
|
-
if (saved !== void 0) {
|
|
27841
|
-
writeLine(io.stdout, `Saved ${endpoint} as your endpoint in ${saved}.`);
|
|
27983
|
+
}
|
|
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;
|
|
27842
27993
|
}
|
|
27843
|
-
return
|
|
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;
|
|
27844
28034
|
}
|
|
27845
|
-
function
|
|
27846
|
-
|
|
27847
|
-
|
|
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);
|
|
27848
28040
|
}
|
|
27849
|
-
return
|
|
28041
|
+
if (DATE_RE.test(value)) return fromDate(value);
|
|
28042
|
+
if (TIMESTAMP_RE.test(value)) return iso(new Date(value));
|
|
28043
|
+
return reject();
|
|
27850
28044
|
}
|
|
27851
|
-
|
|
28045
|
+
function parseExpires(text, now) {
|
|
28046
|
+
const value = text.trim();
|
|
28047
|
+
const duration3 = DURATION_RE.exec(value);
|
|
28048
|
+
let stamp2;
|
|
27852
28049
|
try {
|
|
27853
|
-
|
|
27854
|
-
|
|
27855
|
-
|
|
27856
|
-
|
|
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);
|
|
27857
28060
|
}
|
|
27858
|
-
|
|
28061
|
+
} catch {
|
|
28062
|
+
return reject(EXPIRES_MESSAGE);
|
|
27859
28063
|
}
|
|
28064
|
+
return stamp2;
|
|
27860
28065
|
}
|
|
27861
|
-
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
27866
|
-
writeLine(io.stdout, `Not logged in to ${endpoint}.`);
|
|
27867
|
-
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");
|
|
27868
28071
|
}
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
writeLine(
|
|
27872
|
-
io.stderr,
|
|
27873
|
-
`quibble: warning: could not revoke the refresh token at ${endpoint}`
|
|
27874
|
-
);
|
|
28072
|
+
if (options.open === true && options.status !== void 0) {
|
|
28073
|
+
throw new UsageError("--open and --status cannot be used together");
|
|
27875
28074
|
}
|
|
27876
|
-
|
|
27877
|
-
|
|
27878
|
-
return
|
|
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
|
+
};
|
|
27879
28084
|
}
|
|
27880
|
-
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27888
|
-
|
|
27889
|
-
|
|
27890
|
-
|
|
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;
|
|
27891
28115
|
}
|
|
28116
|
+
return more ? `Showing the newest ${limit} complaints: pass --limit to see more (up to ${MAX_LIMIT}).` : void 0;
|
|
27892
28117
|
}
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
|
|
27896
|
-
|
|
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.");
|
|
27897
28152
|
return 0;
|
|
27898
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);
|
|
27899
28202
|
const client = await openClient(context, io);
|
|
27900
28203
|
const endpoint = requireEndpoint(context);
|
|
27901
|
-
const
|
|
27902
|
-
const org = me.kind === "token" || client.org === void 0 ? void 0 : await client.organization();
|
|
28204
|
+
const answer2 = await client.orgs();
|
|
27903
28205
|
if (options.json === true) {
|
|
27904
|
-
writeJson(io.stdout,
|
|
27905
|
-
...me.body,
|
|
27906
|
-
endpoint,
|
|
27907
|
-
...org === void 0 ? {} : { org: org.body }
|
|
27908
|
-
});
|
|
28206
|
+
writeJson(io.stdout, answer2.body);
|
|
27909
28207
|
return 0;
|
|
27910
28208
|
}
|
|
27911
|
-
|
|
27912
|
-
if (me.kind !== "token" && me.orgs.length === 0) {
|
|
28209
|
+
if (answer2.orgs.length === 0) {
|
|
27913
28210
|
const config2 = await fetchAuthConfig(endpoint, {
|
|
27914
28211
|
debug: context.debug,
|
|
27915
|
-
fetch: io.fetch
|
|
28212
|
+
fetch: io.fetch,
|
|
28213
|
+
timeoutMs: context.timeoutMs
|
|
27916
28214
|
});
|
|
27917
28215
|
const webUrl = config2.mode === "cognito" ? trimSlash2(config2.webUrl) : endpoint;
|
|
27918
|
-
|
|
28216
|
+
writeLine(io.stdout, `No organizations yet: create one at ${webUrl}${ORGS_PAGE3}.`);
|
|
28217
|
+
return 0;
|
|
27919
28218
|
}
|
|
27920
|
-
for (const line of
|
|
28219
|
+
for (const line of formatOrgs(answer2.orgs)) writeLine(io.stdout, line);
|
|
27921
28220
|
return 0;
|
|
27922
28221
|
}
|
|
27923
|
-
function
|
|
27924
|
-
const
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
const width = Math.max(...entries.map(([key]) => key.length));
|
|
27930
|
-
return entries.map(([key, value]) => ` ${key.padEnd(width)} ${value}`);
|
|
27931
|
-
}
|
|
27932
|
-
function whoamiLines(endpoint, me, org) {
|
|
27933
|
-
if (me.kind === "token") {
|
|
27934
|
-
return rows([
|
|
27935
|
-
["endpoint", endpoint],
|
|
27936
|
-
["token", me.name ?? me.reporter],
|
|
27937
|
-
["org", me.org ?? ""],
|
|
27938
|
-
["project", me.project ?? ""],
|
|
27939
|
-
["role", me.role ?? "filer"]
|
|
27940
|
-
]);
|
|
27941
|
-
}
|
|
27942
|
-
const lines = rows([
|
|
27943
|
-
["endpoint", endpoint],
|
|
27944
|
-
["email", me.email ?? ""],
|
|
27945
|
-
["name", me.name ?? ""],
|
|
27946
|
-
["sub", me.sub ?? ""],
|
|
27947
|
-
["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)
|
|
27948
28228
|
]);
|
|
27949
|
-
|
|
27950
|
-
|
|
27951
|
-
|
|
27952
|
-
|
|
27953
|
-
|
|
27954
|
-
|
|
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)];
|
|
27955
28236
|
}
|
|
27956
28237
|
|
|
27957
28238
|
// src/commands/project.ts
|
|
@@ -28720,11 +29001,14 @@ function buildProgram(io, setCode) {
|
|
|
28720
29001
|
}).exitOverride();
|
|
28721
29002
|
withGlobalOptions(program2);
|
|
28722
29003
|
withGlobalOptions(
|
|
28723
|
-
program2.command("init").description("write a .quibblerc in the current directory").option(
|
|
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")
|
|
28724
29008
|
).action(async function() {
|
|
28725
29009
|
const options = { ...this.opts(), ...globals(program2, this) };
|
|
28726
29010
|
rejectEmptyOptions(options);
|
|
28727
|
-
setCode(await runInit(options, io));
|
|
29011
|
+
setCode(await runInit({ ...options, version: packageVersion() }, io));
|
|
28728
29012
|
});
|
|
28729
29013
|
withGlobalOptions(
|
|
28730
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")
|