cleargate 0.3.0 → 0.5.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/dist/MANIFEST.json +4 -4
- package/dist/cli.cjs +644 -95
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +643 -95
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +14 -3
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +16 -3
- package/dist/templates/cleargate-planning/MANIFEST.json +4 -4
- package/package.json +1 -1
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +14 -3
- package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +16 -3
- package/templates/cleargate-planning/MANIFEST.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Command } from "commander";
|
|
|
14
14
|
// package.json
|
|
15
15
|
var package_default = {
|
|
16
16
|
name: "cleargate",
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.5.0",
|
|
18
18
|
private: false,
|
|
19
19
|
type: "module",
|
|
20
20
|
description: "Planning framework for Claude Code agents \u2014 sprint/epic/story protocol, four-agent loop (architect/developer/qa/reporter), Karpathy-style awareness wiki.",
|
|
@@ -318,13 +318,294 @@ function matchesGlob(filePath, glob) {
|
|
|
318
318
|
|
|
319
319
|
// src/commands/join.ts
|
|
320
320
|
import * as os from "os";
|
|
321
|
+
|
|
322
|
+
// src/auth/identity-flow.ts
|
|
323
|
+
import * as readline from "readline";
|
|
324
|
+
var IdentityFlowError = class extends Error {
|
|
325
|
+
constructor(code, message) {
|
|
326
|
+
super(message ?? code);
|
|
327
|
+
this.code = code;
|
|
328
|
+
this.name = "IdentityFlowError";
|
|
329
|
+
}
|
|
330
|
+
code;
|
|
331
|
+
};
|
|
332
|
+
var DeviceFlowError = class extends Error {
|
|
333
|
+
constructor(code, message) {
|
|
334
|
+
super(message ?? code);
|
|
335
|
+
this.code = code;
|
|
336
|
+
this.name = "DeviceFlowError";
|
|
337
|
+
}
|
|
338
|
+
code;
|
|
339
|
+
};
|
|
340
|
+
async function pickProvider(opts) {
|
|
341
|
+
const available = opts.available ?? ["github", "email"];
|
|
342
|
+
if (opts.flag !== void 0) {
|
|
343
|
+
const flagLower = opts.flag.toLowerCase();
|
|
344
|
+
if (!available.includes(flagLower)) {
|
|
345
|
+
throw new IdentityFlowError(
|
|
346
|
+
"provider_unknown",
|
|
347
|
+
`cleargate: unknown provider '${opts.flag}'. Available: ${available.join(", ")}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return flagLower;
|
|
351
|
+
}
|
|
352
|
+
if (!opts.isTTY) {
|
|
353
|
+
throw new IdentityFlowError(
|
|
354
|
+
"provider_required",
|
|
355
|
+
"cleargate: --auth required in non-interactive mode"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (available.length === 1) {
|
|
359
|
+
return available[0];
|
|
360
|
+
}
|
|
361
|
+
return promptPicker(available, opts);
|
|
362
|
+
}
|
|
363
|
+
var PROVIDER_LABELS = {
|
|
364
|
+
github: "GitHub OAuth",
|
|
365
|
+
email: "Email magic-link"
|
|
366
|
+
};
|
|
367
|
+
async function promptPicker(options, { stdin, stdout } = {}) {
|
|
368
|
+
const write = stdout ?? ((s) => process.stdout.write(s));
|
|
369
|
+
write("How would you like to verify your email?\n");
|
|
370
|
+
options.forEach((p, i) => {
|
|
371
|
+
write(` ${i + 1}. ${PROVIDER_LABELS[p]}
|
|
372
|
+
`);
|
|
373
|
+
});
|
|
374
|
+
write(`Choice [1-${options.length}]: `);
|
|
375
|
+
const inputStream = stdin ?? process.stdin;
|
|
376
|
+
return new Promise((resolve14, reject) => {
|
|
377
|
+
let settled = false;
|
|
378
|
+
const rl = readline.createInterface({
|
|
379
|
+
input: inputStream,
|
|
380
|
+
output: void 0,
|
|
381
|
+
terminal: false
|
|
382
|
+
});
|
|
383
|
+
rl.once("line", (line) => {
|
|
384
|
+
settled = true;
|
|
385
|
+
rl.close();
|
|
386
|
+
const idx = parseInt(line.trim(), 10) - 1;
|
|
387
|
+
if (isNaN(idx) || idx < 0 || idx >= options.length) {
|
|
388
|
+
reject(
|
|
389
|
+
new IdentityFlowError(
|
|
390
|
+
"invalid_choice",
|
|
391
|
+
`cleargate: invalid choice '${line.trim()}'. Enter a number between 1 and ${options.length}.`
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
resolve14(options[idx]);
|
|
397
|
+
});
|
|
398
|
+
rl.once("error", (err) => {
|
|
399
|
+
if (!settled) {
|
|
400
|
+
settled = true;
|
|
401
|
+
reject(err);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
rl.once("close", () => {
|
|
405
|
+
if (!settled) {
|
|
406
|
+
settled = true;
|
|
407
|
+
reject(new IdentityFlowError("provider_required", "cleargate: no provider selected"));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function defaultSleep(ms) {
|
|
413
|
+
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
414
|
+
}
|
|
415
|
+
async function startDeviceFlow(opts) {
|
|
416
|
+
const sleepFn = opts.sleepFn ?? defaultSleep;
|
|
417
|
+
let currentIntervalMs = opts.intervalOverrideMs !== void 0 ? opts.intervalOverrideMs : Math.max(opts.interval, 5) * 1e3;
|
|
418
|
+
const expiresAtMs = Date.now() + opts.expiresIn * 1e3;
|
|
419
|
+
const deadline = expiresAtMs + (opts.deadlineGraceMs ?? 1e4);
|
|
420
|
+
while (Date.now() < deadline) {
|
|
421
|
+
await sleepFn(currentIntervalMs);
|
|
422
|
+
let pollRes;
|
|
423
|
+
try {
|
|
424
|
+
pollRes = await opts.fetchPoll(opts.deviceCode);
|
|
425
|
+
} catch {
|
|
426
|
+
throw new DeviceFlowError("unreachable");
|
|
427
|
+
}
|
|
428
|
+
if (pollRes.status === 403) {
|
|
429
|
+
let body2 = {};
|
|
430
|
+
try {
|
|
431
|
+
body2 = await pollRes.json();
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
if (body2["error"] === "access_denied") {
|
|
435
|
+
throw new DeviceFlowError("access_denied");
|
|
436
|
+
}
|
|
437
|
+
throw new DeviceFlowError("not_admin");
|
|
438
|
+
}
|
|
439
|
+
if (pollRes.status === 410) {
|
|
440
|
+
throw new DeviceFlowError("expired_token");
|
|
441
|
+
}
|
|
442
|
+
if (!pollRes.status || pollRes.status < 200 || pollRes.status >= 300) {
|
|
443
|
+
if (pollRes.status >= 500 || pollRes.status < 100) {
|
|
444
|
+
throw new DeviceFlowError("server_error");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let body;
|
|
448
|
+
try {
|
|
449
|
+
body = await pollRes.json();
|
|
450
|
+
} catch {
|
|
451
|
+
throw new DeviceFlowError("server_error");
|
|
452
|
+
}
|
|
453
|
+
const errorField = body["error"];
|
|
454
|
+
if (typeof errorField === "string") {
|
|
455
|
+
if (errorField === "authorization_pending") {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (errorField === "slow_down") {
|
|
459
|
+
const retryAfter = body["interval"];
|
|
460
|
+
if (typeof retryAfter === "number") {
|
|
461
|
+
const bumped = retryAfter * 1e3;
|
|
462
|
+
if (bumped > currentIntervalMs) {
|
|
463
|
+
currentIntervalMs = bumped;
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
currentIntervalMs += 5e3;
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (errorField === "access_denied") {
|
|
471
|
+
throw new DeviceFlowError("access_denied");
|
|
472
|
+
}
|
|
473
|
+
if (errorField === "expired_token") {
|
|
474
|
+
throw new DeviceFlowError("expired_token");
|
|
475
|
+
}
|
|
476
|
+
throw new DeviceFlowError("server_error");
|
|
477
|
+
}
|
|
478
|
+
if (body["pending"] === true) {
|
|
479
|
+
const shouldApplyBump = opts.sleepFn !== void 0 || opts.intervalOverrideMs === void 0;
|
|
480
|
+
if (shouldApplyBump && typeof body["retry_after"] === "number") {
|
|
481
|
+
const bumped = body["retry_after"] * 1e3;
|
|
482
|
+
if (bumped > currentIntervalMs) {
|
|
483
|
+
currentIntervalMs = bumped;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (typeof body["access_token"] === "string") {
|
|
489
|
+
return { accessToken: body["access_token"] };
|
|
490
|
+
}
|
|
491
|
+
if (typeof body["admin_token"] === "string") {
|
|
492
|
+
return { accessToken: body["admin_token"] };
|
|
493
|
+
}
|
|
494
|
+
throw new DeviceFlowError("server_error");
|
|
495
|
+
}
|
|
496
|
+
throw new DeviceFlowError("timeout");
|
|
497
|
+
}
|
|
498
|
+
function mapProviderError(httpStatus, errorCode, retryAfterSeconds) {
|
|
499
|
+
if (httpStatus === 400) {
|
|
500
|
+
switch (errorCode) {
|
|
501
|
+
case "provider_not_allowed":
|
|
502
|
+
return {
|
|
503
|
+
message: "cleargate: this invite requires a different provider \u2014 re-run with `--auth <pinned>`",
|
|
504
|
+
exitCode: 9,
|
|
505
|
+
retryable: true
|
|
506
|
+
};
|
|
507
|
+
case "provider_unknown":
|
|
508
|
+
return {
|
|
509
|
+
message: "cleargate: server does not have that provider registered \u2014 contact the project admin",
|
|
510
|
+
exitCode: 9,
|
|
511
|
+
retryable: false
|
|
512
|
+
};
|
|
513
|
+
case "identity_proof_required":
|
|
514
|
+
return {
|
|
515
|
+
message: "cleargate: this CLI is out of date \u2014 please upgrade and retry (`npm i -g cleargate@latest`)",
|
|
516
|
+
exitCode: 11,
|
|
517
|
+
retryable: false
|
|
518
|
+
};
|
|
519
|
+
default:
|
|
520
|
+
return {
|
|
521
|
+
message: "cleargate: invalid request to server (please file a bug)",
|
|
522
|
+
exitCode: 7,
|
|
523
|
+
retryable: false
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (httpStatus === 403 && errorCode === "email_mismatch") {
|
|
528
|
+
return {
|
|
529
|
+
message: "cleargate: verified email does not match the invitee \u2014 ask your admin to re-issue the invite",
|
|
530
|
+
exitCode: 10,
|
|
531
|
+
retryable: false
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (httpStatus === 404) {
|
|
535
|
+
return {
|
|
536
|
+
message: "cleargate: invite not found",
|
|
537
|
+
exitCode: 4,
|
|
538
|
+
retryable: false
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (httpStatus === 410) {
|
|
542
|
+
switch (errorCode) {
|
|
543
|
+
case "invite_expired":
|
|
544
|
+
return {
|
|
545
|
+
message: "cleargate: invite expired. Request a new invite",
|
|
546
|
+
exitCode: 3,
|
|
547
|
+
retryable: false
|
|
548
|
+
};
|
|
549
|
+
case "invite_already_consumed":
|
|
550
|
+
return {
|
|
551
|
+
message: "cleargate: invite already consumed. Request a new invite",
|
|
552
|
+
exitCode: 3,
|
|
553
|
+
retryable: false
|
|
554
|
+
};
|
|
555
|
+
case "challenge_expired":
|
|
556
|
+
return {
|
|
557
|
+
message: "cleargate: code expired. Re-run `cleargate join <url>` to start over",
|
|
558
|
+
exitCode: 3,
|
|
559
|
+
retryable: false
|
|
560
|
+
};
|
|
561
|
+
default:
|
|
562
|
+
return {
|
|
563
|
+
message: "cleargate: invite no longer valid. Request a new invite",
|
|
564
|
+
exitCode: 3,
|
|
565
|
+
retryable: false
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (httpStatus === 429) {
|
|
570
|
+
const retryHint = retryAfterSeconds !== void 0 ? `${retryAfterSeconds}` : "900";
|
|
571
|
+
return {
|
|
572
|
+
message: `cleargate: too many requests. Retry after ${retryHint}s`,
|
|
573
|
+
exitCode: 8,
|
|
574
|
+
retryable: true
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (httpStatus === 502 && errorCode === "provider_error") {
|
|
578
|
+
return {
|
|
579
|
+
message: "cleargate: code didn't match. Try again, or restart with `cleargate join <url>`",
|
|
580
|
+
exitCode: 12,
|
|
581
|
+
retryable: true
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (httpStatus >= 500) {
|
|
585
|
+
return {
|
|
586
|
+
message: `cleargate: server error ${httpStatus}`,
|
|
587
|
+
exitCode: 6,
|
|
588
|
+
retryable: false
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
message: `cleargate: unexpected error ${httpStatus} ${errorCode}`,
|
|
593
|
+
exitCode: 7,
|
|
594
|
+
retryable: false
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/commands/join.ts
|
|
599
|
+
import * as readline2 from "readline";
|
|
321
600
|
var UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
601
|
+
var GITHUB_DEVICE_FLOW_URL = "https://github.com/login/oauth/access_token";
|
|
322
602
|
async function joinHandler(opts) {
|
|
323
603
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
324
604
|
const stdout = opts.stdout ?? ((s) => process.stdout.write(s));
|
|
325
605
|
const stderr = opts.stderr ?? ((s) => process.stderr.write(s));
|
|
326
606
|
const exit = opts.exit ?? ((c) => process.exit(c));
|
|
327
607
|
const hostname3 = opts.hostname ?? (() => os.hostname());
|
|
608
|
+
const isTTY = opts.isTTY ?? process.stdin.isTTY === true;
|
|
328
609
|
let token;
|
|
329
610
|
let baseUrl;
|
|
330
611
|
try {
|
|
@@ -355,60 +636,309 @@ async function joinHandler(opts) {
|
|
|
355
636
|
exit(5);
|
|
356
637
|
return;
|
|
357
638
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
} catch (err) {
|
|
362
|
-
stderr(
|
|
363
|
-
`cleargate: cannot reach ${baseUrl} (${err instanceof Error ? err.message : String(err)}).
|
|
364
|
-
`
|
|
365
|
-
);
|
|
366
|
-
exit(2);
|
|
639
|
+
if (opts.nonInteractive && !opts.auth) {
|
|
640
|
+
stderr("cleargate: --auth required in non-interactive mode\n");
|
|
641
|
+
exit(1);
|
|
367
642
|
return;
|
|
368
643
|
}
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
stderr("cleargate: invite expired. Request a new invite.\n");
|
|
373
|
-
} else {
|
|
374
|
-
stderr("cleargate: invite already consumed. Request a new invite.\n");
|
|
375
|
-
}
|
|
376
|
-
exit(3);
|
|
644
|
+
if (opts.nonInteractive && opts.auth === "email" && !opts.code) {
|
|
645
|
+
stderr("cleargate: --code required for email provider in non-interactive mode\n");
|
|
646
|
+
exit(1);
|
|
377
647
|
return;
|
|
378
648
|
}
|
|
379
|
-
if (
|
|
380
|
-
stderr("cleargate:
|
|
381
|
-
exit(
|
|
649
|
+
if (opts.nonInteractive && opts.auth === "github") {
|
|
650
|
+
stderr("cleargate: GitHub auth requires browser interaction; use `--auth email` for non-interactive flows\n");
|
|
651
|
+
exit(1);
|
|
382
652
|
return;
|
|
383
653
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
654
|
+
let provider;
|
|
655
|
+
try {
|
|
656
|
+
provider = await pickProvider({
|
|
657
|
+
flag: opts.auth,
|
|
658
|
+
isTTY: !opts.nonInteractive && isTTY,
|
|
659
|
+
available: ["github", "email"],
|
|
660
|
+
stdin: opts.stdin,
|
|
661
|
+
stdout
|
|
662
|
+
});
|
|
663
|
+
} catch (err) {
|
|
664
|
+
if (err instanceof IdentityFlowError) {
|
|
665
|
+
stderr(`${err.message}
|
|
387
666
|
`);
|
|
388
|
-
|
|
389
|
-
|
|
667
|
+
exit(1);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
throw err;
|
|
390
671
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
672
|
+
let challengeRes;
|
|
673
|
+
try {
|
|
674
|
+
challengeRes = await fetchFn(`${baseUrl}/join/${token}/challenge`, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
677
|
+
body: JSON.stringify({ provider })
|
|
678
|
+
});
|
|
679
|
+
} catch (err) {
|
|
680
|
+
stderr(
|
|
681
|
+
`cleargate: cannot reach ${baseUrl} (${err instanceof Error ? err.message : String(err)}).
|
|
682
|
+
`
|
|
683
|
+
);
|
|
684
|
+
exit(2);
|
|
395
685
|
return;
|
|
396
686
|
}
|
|
397
|
-
if (!
|
|
398
|
-
|
|
687
|
+
if (!challengeRes.ok) {
|
|
688
|
+
const body = await challengeRes.json().catch(() => ({}));
|
|
689
|
+
const { message, exitCode } = mapProviderError(
|
|
690
|
+
challengeRes.status,
|
|
691
|
+
body.error ?? "",
|
|
692
|
+
parseRetryAfter(challengeRes)
|
|
693
|
+
);
|
|
694
|
+
stderr(`${message}
|
|
399
695
|
`);
|
|
400
|
-
exit(
|
|
696
|
+
exit(exitCode);
|
|
401
697
|
return;
|
|
402
698
|
}
|
|
403
|
-
let
|
|
699
|
+
let challengeBody;
|
|
404
700
|
try {
|
|
405
|
-
|
|
701
|
+
challengeBody = await challengeRes.json();
|
|
406
702
|
} catch {
|
|
407
703
|
stderr("cleargate: server returned non-JSON response.\n");
|
|
408
704
|
exit(7);
|
|
409
705
|
return;
|
|
410
706
|
}
|
|
411
|
-
const
|
|
707
|
+
const challengeId = challengeBody.challenge_id;
|
|
708
|
+
const clientHints = challengeBody.client_hints;
|
|
709
|
+
let completeRawBody;
|
|
710
|
+
if (provider === "github") {
|
|
711
|
+
const deviceCode = clientHints["device_code"];
|
|
712
|
+
const userCode = clientHints["user_code"];
|
|
713
|
+
const verificationUri = clientHints["verification_uri"];
|
|
714
|
+
const expiresIn = typeof clientHints["expires_in"] === "number" ? clientHints["expires_in"] : 900;
|
|
715
|
+
const interval = typeof clientHints["interval"] === "number" ? clientHints["interval"] : 5;
|
|
716
|
+
stdout(`Open the following URL in your browser and enter the code:
|
|
717
|
+
`);
|
|
718
|
+
stdout(` URL: ${verificationUri}
|
|
719
|
+
`);
|
|
720
|
+
stdout(` Code: ${userCode}
|
|
721
|
+
`);
|
|
722
|
+
stdout(` (Code expires in ${Math.floor(expiresIn / 60)} minutes)
|
|
723
|
+
`);
|
|
724
|
+
stdout("Waiting for authorization...\n");
|
|
725
|
+
let accessToken;
|
|
726
|
+
try {
|
|
727
|
+
const result = await startDeviceFlow({
|
|
728
|
+
deviceCode,
|
|
729
|
+
interval,
|
|
730
|
+
expiresIn,
|
|
731
|
+
fetchPoll: async (dc) => {
|
|
732
|
+
const res = await fetchFn(GITHUB_DEVICE_FLOW_URL, {
|
|
733
|
+
method: "POST",
|
|
734
|
+
headers: {
|
|
735
|
+
Accept: "application/json",
|
|
736
|
+
"Content-Type": "application/json"
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify({
|
|
739
|
+
device_code: dc,
|
|
740
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
return {
|
|
744
|
+
status: res.status,
|
|
745
|
+
json: () => res.json()
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
...opts.sleepFn !== void 0 ? { sleepFn: opts.sleepFn } : {},
|
|
749
|
+
...opts.intervalOverrideMs !== void 0 ? { intervalOverrideMs: opts.intervalOverrideMs } : {}
|
|
750
|
+
});
|
|
751
|
+
accessToken = result.accessToken;
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err instanceof DeviceFlowError) {
|
|
754
|
+
switch (err.code) {
|
|
755
|
+
case "access_denied":
|
|
756
|
+
stderr("cleargate: access denied \u2014 you declined authorization in the browser.\n");
|
|
757
|
+
exit(5);
|
|
758
|
+
return;
|
|
759
|
+
case "expired_token":
|
|
760
|
+
stderr("cleargate: device code expired \u2014 please re-run `cleargate join <url>`.\n");
|
|
761
|
+
exit(5);
|
|
762
|
+
return;
|
|
763
|
+
case "unreachable":
|
|
764
|
+
stderr("cleargate: cannot reach GitHub. Check your connection and retry.\n");
|
|
765
|
+
exit(2);
|
|
766
|
+
return;
|
|
767
|
+
default:
|
|
768
|
+
stderr(`cleargate: GitHub device flow error: ${err.code}
|
|
769
|
+
`);
|
|
770
|
+
exit(6);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
stderr("cleargate: unexpected error during GitHub device flow\n");
|
|
775
|
+
exit(6);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
let completeRes;
|
|
779
|
+
try {
|
|
780
|
+
completeRes = await fetchFn(`${baseUrl}/join/${token}/complete`, {
|
|
781
|
+
method: "POST",
|
|
782
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
783
|
+
body: JSON.stringify({ challenge_id: challengeId, proof: { access_token: accessToken } })
|
|
784
|
+
});
|
|
785
|
+
} catch (err) {
|
|
786
|
+
stderr(`cleargate: cannot reach ${baseUrl} (${err instanceof Error ? err.message : String(err)}).
|
|
787
|
+
`);
|
|
788
|
+
exit(2);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (!completeRes.ok) {
|
|
792
|
+
const body = await completeRes.json().catch(() => ({}));
|
|
793
|
+
const { message, exitCode } = mapProviderError(
|
|
794
|
+
completeRes.status,
|
|
795
|
+
body.error ?? "",
|
|
796
|
+
parseRetryAfter(completeRes)
|
|
797
|
+
);
|
|
798
|
+
stderr(`${message}
|
|
799
|
+
`);
|
|
800
|
+
exit(exitCode);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
completeRawBody = await completeRes.json();
|
|
805
|
+
} catch {
|
|
806
|
+
stderr("cleargate: server returned non-JSON response.\n");
|
|
807
|
+
exit(7);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
const sentTo = typeof clientHints["sent_to"] === "string" ? clientHints["sent_to"] : "(unknown)";
|
|
812
|
+
const maxRetries = 3;
|
|
813
|
+
stdout(`We sent a 6-digit code to ${sentTo}.
|
|
814
|
+
`);
|
|
815
|
+
if (opts.code !== void 0) {
|
|
816
|
+
let completeRes;
|
|
817
|
+
try {
|
|
818
|
+
completeRes = await fetchFn(`${baseUrl}/join/${token}/complete`, {
|
|
819
|
+
method: "POST",
|
|
820
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
821
|
+
body: JSON.stringify({ challenge_id: challengeId, proof: { code: opts.code } })
|
|
822
|
+
});
|
|
823
|
+
} catch (err) {
|
|
824
|
+
stderr(`cleargate: cannot reach ${baseUrl} (${err instanceof Error ? err.message : String(err)}).
|
|
825
|
+
`);
|
|
826
|
+
exit(2);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (!completeRes.ok) {
|
|
830
|
+
const body = await completeRes.json().catch(() => ({}));
|
|
831
|
+
const { message, exitCode } = mapProviderError(
|
|
832
|
+
completeRes.status,
|
|
833
|
+
body.error ?? "",
|
|
834
|
+
parseRetryAfter(completeRes)
|
|
835
|
+
);
|
|
836
|
+
stderr(`${message}
|
|
837
|
+
`);
|
|
838
|
+
exit(exitCode);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
completeRawBody = await completeRes.json();
|
|
843
|
+
} catch {
|
|
844
|
+
stderr("cleargate: server returned non-JSON response.\n");
|
|
845
|
+
exit(7);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
let readNextLine2 = function() {
|
|
850
|
+
if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift());
|
|
851
|
+
if (rlClosed) return Promise.resolve("");
|
|
852
|
+
return new Promise((resolve14) => {
|
|
853
|
+
lineWaiters.push(resolve14);
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
var readNextLine = readNextLine2;
|
|
857
|
+
const inputStream = opts.stdin ?? process.stdin;
|
|
858
|
+
const rl = readline2.createInterface({
|
|
859
|
+
input: inputStream,
|
|
860
|
+
output: void 0,
|
|
861
|
+
terminal: false
|
|
862
|
+
});
|
|
863
|
+
const lineQueue = [];
|
|
864
|
+
const lineWaiters = [];
|
|
865
|
+
let rlClosed = false;
|
|
866
|
+
rl.on("line", (line) => {
|
|
867
|
+
const waiter = lineWaiters.shift();
|
|
868
|
+
if (waiter) {
|
|
869
|
+
waiter(line);
|
|
870
|
+
} else {
|
|
871
|
+
lineQueue.push(line);
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
rl.once("close", () => {
|
|
875
|
+
rlClosed = true;
|
|
876
|
+
for (const waiter of lineWaiters.splice(0)) {
|
|
877
|
+
waiter("");
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
let succeeded = false;
|
|
881
|
+
try {
|
|
882
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
883
|
+
stdout("Enter code: ");
|
|
884
|
+
const otpCode = (await readNextLine2()).trim();
|
|
885
|
+
let completeRes;
|
|
886
|
+
try {
|
|
887
|
+
completeRes = await fetchFn(`${baseUrl}/join/${token}/complete`, {
|
|
888
|
+
method: "POST",
|
|
889
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
890
|
+
body: JSON.stringify({ challenge_id: challengeId, proof: { code: otpCode } })
|
|
891
|
+
});
|
|
892
|
+
} catch (err) {
|
|
893
|
+
stderr(`cleargate: cannot reach ${baseUrl} (${err instanceof Error ? err.message : String(err)}).
|
|
894
|
+
`);
|
|
895
|
+
exit(2);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (completeRes.ok) {
|
|
899
|
+
try {
|
|
900
|
+
completeRawBody = await completeRes.json();
|
|
901
|
+
} catch {
|
|
902
|
+
stderr("cleargate: server returned non-JSON response.\n");
|
|
903
|
+
exit(7);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
succeeded = true;
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
const body = await completeRes.json().catch(() => ({}));
|
|
910
|
+
const errorCode = body.error ?? "";
|
|
911
|
+
if (completeRes.status === 410 || errorCode === "challenge_expired") {
|
|
912
|
+
const { message, exitCode } = mapProviderError(completeRes.status, errorCode, parseRetryAfter(completeRes));
|
|
913
|
+
stderr(`${message}
|
|
914
|
+
`);
|
|
915
|
+
exit(exitCode);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (completeRes.status === 403 || completeRes.status >= 400 && completeRes.status < 500 && errorCode !== "provider_error") {
|
|
919
|
+
const { message, exitCode } = mapProviderError(completeRes.status, errorCode, parseRetryAfter(completeRes));
|
|
920
|
+
stderr(`${message}
|
|
921
|
+
`);
|
|
922
|
+
exit(exitCode);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (attempt < maxRetries) {
|
|
926
|
+
stderr(`cleargate: code didn't match. ${maxRetries - attempt} attempt${maxRetries - attempt === 1 ? "" : "s"} remaining.
|
|
927
|
+
`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} finally {
|
|
931
|
+
rl.close();
|
|
932
|
+
}
|
|
933
|
+
if (!succeeded) {
|
|
934
|
+
stderr(`cleargate: code didn't match after ${maxRetries} tries. Run \`cleargate join <url>\` again to get a new code.
|
|
935
|
+
`);
|
|
936
|
+
exit(12);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const b = completeRawBody;
|
|
412
942
|
if (typeof b.refresh_token !== "string" || typeof b.project_name !== "string") {
|
|
413
943
|
stderr("cleargate: server returned unexpected response shape.\n");
|
|
414
944
|
exit(7);
|
|
@@ -431,6 +961,12 @@ async function joinHandler(opts) {
|
|
|
431
961
|
exit(99);
|
|
432
962
|
}
|
|
433
963
|
}
|
|
964
|
+
function parseRetryAfter(res) {
|
|
965
|
+
const hdr = res.headers?.get?.("retry-after");
|
|
966
|
+
if (!hdr) return void 0;
|
|
967
|
+
const n = parseInt(hdr, 10);
|
|
968
|
+
return isNaN(n) ? void 0 : n;
|
|
969
|
+
}
|
|
434
970
|
|
|
435
971
|
// src/commands/stamp.ts
|
|
436
972
|
import * as fs4 from "fs";
|
|
@@ -1617,13 +2153,13 @@ async function readDriftState(projectRoot) {
|
|
|
1617
2153
|
}
|
|
1618
2154
|
|
|
1619
2155
|
// src/lib/prompts.ts
|
|
1620
|
-
import * as
|
|
2156
|
+
import * as readline3 from "readline";
|
|
1621
2157
|
async function promptYesNo(question, defaultYes, opts) {
|
|
1622
2158
|
const stdoutFn = opts?.stdout ?? ((s) => process.stdout.write(s));
|
|
1623
2159
|
stdoutFn(question + "\n");
|
|
1624
2160
|
const inputStream = opts?.stdin ?? process.stdin;
|
|
1625
2161
|
return new Promise((resolve14) => {
|
|
1626
|
-
const rl =
|
|
2162
|
+
const rl = readline3.createInterface({
|
|
1627
2163
|
input: inputStream,
|
|
1628
2164
|
output: void 0,
|
|
1629
2165
|
// we handle output ourselves
|
|
@@ -1654,7 +2190,7 @@ async function promptEmail(question, defaultValue, opts) {
|
|
|
1654
2190
|
stdoutFn(question + "\n");
|
|
1655
2191
|
const inputStream = opts?.stdin ?? process.stdin;
|
|
1656
2192
|
return new Promise((resolve14) => {
|
|
1657
|
-
const rl =
|
|
2193
|
+
const rl = readline3.createInterface({
|
|
1658
2194
|
input: inputStream,
|
|
1659
2195
|
output: void 0,
|
|
1660
2196
|
// we handle output ourselves
|
|
@@ -3004,7 +3540,7 @@ ${row}
|
|
|
3004
3540
|
// src/commands/wiki-audit-status.ts
|
|
3005
3541
|
import * as fs19 from "fs";
|
|
3006
3542
|
import * as path20 from "path";
|
|
3007
|
-
import * as
|
|
3543
|
+
import * as readline4 from "readline";
|
|
3008
3544
|
var TERMINAL = /* @__PURE__ */ new Set(["Completed", "Done", "Abandoned", "Closed", "Resolved"]);
|
|
3009
3545
|
async function wikiAuditStatusHandler(opts = {}) {
|
|
3010
3546
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -3143,7 +3679,7 @@ async function wikiAuditStatusHandler(opts = {}) {
|
|
|
3143
3679
|
answer = await opts.promptReader();
|
|
3144
3680
|
} else {
|
|
3145
3681
|
answer = await new Promise((resolve14) => {
|
|
3146
|
-
const rl =
|
|
3682
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
|
|
3147
3683
|
rl.question(`apply ${fixable.length} changes? [y/N] `, (ans) => {
|
|
3148
3684
|
rl.close();
|
|
3149
3685
|
resolve14(ans);
|
|
@@ -7814,9 +8350,6 @@ import * as fs33 from "fs";
|
|
|
7814
8350
|
import * as path43 from "path";
|
|
7815
8351
|
import * as os5 from "os";
|
|
7816
8352
|
var DEFAULT_MCP_URL = "http://localhost:3000";
|
|
7817
|
-
function sleep(ms) {
|
|
7818
|
-
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
7819
|
-
}
|
|
7820
8353
|
function resolveMcpUrl(mcpUrlFlag, env) {
|
|
7821
8354
|
return (mcpUrlFlag ?? (env ?? process.env)["CLEARGATE_MCP_URL"] ?? DEFAULT_MCP_URL).replace(/\/$/, "");
|
|
7822
8355
|
}
|
|
@@ -7837,7 +8370,6 @@ async function adminLoginHandler(opts = {}) {
|
|
|
7837
8370
|
const stdout = opts.stdout ?? ((msg) => process.stdout.write(msg + "\n"));
|
|
7838
8371
|
const stderr = opts.stderr ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
7839
8372
|
const exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
7840
|
-
const sleepFn = opts.sleepFn ?? sleep;
|
|
7841
8373
|
const mcpBase = resolveMcpUrl(opts.mcpUrl, opts.env);
|
|
7842
8374
|
let startData;
|
|
7843
8375
|
try {
|
|
@@ -7864,79 +8396,95 @@ async function adminLoginHandler(opts = {}) {
|
|
|
7864
8396
|
stdout(` Code: ${startData.user_code}`);
|
|
7865
8397
|
stdout(` (Code expires in ${Math.floor(startData.expires_in / 60)} minutes)`);
|
|
7866
8398
|
stdout("Waiting for authorization...");
|
|
7867
|
-
let
|
|
7868
|
-
const
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
body
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
stderr(`cleargate: error: network error while polling (${err instanceof Error ? err.message : String(err)})`);
|
|
7882
|
-
return exitFn(3);
|
|
7883
|
-
}
|
|
7884
|
-
if (pollRes.status === 403) {
|
|
7885
|
-
const body = await pollRes.json().catch(() => ({}));
|
|
7886
|
-
if (body.error === "access_denied") {
|
|
7887
|
-
stderr("cleargate: error: access denied \u2014 you declined authorization in the browser.");
|
|
7888
|
-
return exitFn(5);
|
|
7889
|
-
}
|
|
7890
|
-
stderr("cleargate: error: your GitHub account is not authorized as an admin user.");
|
|
7891
|
-
return exitFn(4);
|
|
7892
|
-
}
|
|
7893
|
-
if (pollRes.status === 410) {
|
|
7894
|
-
stderr("cleargate: error: device code expired \u2014 please run `cleargate admin login` again.");
|
|
7895
|
-
return exitFn(5);
|
|
7896
|
-
}
|
|
7897
|
-
if (!pollRes.ok) {
|
|
7898
|
-
const body = await pollRes.json().catch(() => ({}));
|
|
7899
|
-
stderr(`cleargate: error: unexpected server response ${pollRes.status}: ${body.error ?? "unknown"}`);
|
|
7900
|
-
return exitFn(6);
|
|
7901
|
-
}
|
|
7902
|
-
const pollBody = await pollRes.json();
|
|
7903
|
-
if (pollBody.pending) {
|
|
7904
|
-
const shouldApplyBump = opts.sleepFn !== void 0 || opts.intervalOverrideMs === void 0;
|
|
7905
|
-
if (shouldApplyBump && pollBody.retry_after !== void 0) {
|
|
7906
|
-
const bumped = pollBody.retry_after * 1e3;
|
|
7907
|
-
if (bumped > currentInterval) {
|
|
7908
|
-
currentInterval = bumped;
|
|
8399
|
+
let capturedSuccessBody = null;
|
|
8400
|
+
const fetchPollCapture = async (deviceCode) => {
|
|
8401
|
+
const res = await fetchFn(`${mcpBase}/admin-api/v1/auth/device/poll`, {
|
|
8402
|
+
method: "POST",
|
|
8403
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
8404
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
8405
|
+
});
|
|
8406
|
+
const originalJson = res.json.bind(res);
|
|
8407
|
+
return {
|
|
8408
|
+
status: res.status,
|
|
8409
|
+
json: async () => {
|
|
8410
|
+
const body = await originalJson();
|
|
8411
|
+
if (body["pending"] === false) {
|
|
8412
|
+
capturedSuccessBody = body;
|
|
7909
8413
|
}
|
|
8414
|
+
return body;
|
|
8415
|
+
}
|
|
8416
|
+
};
|
|
8417
|
+
};
|
|
8418
|
+
try {
|
|
8419
|
+
await startDeviceFlow({
|
|
8420
|
+
deviceCode: startData.device_code,
|
|
8421
|
+
interval: startData.interval,
|
|
8422
|
+
expiresIn: startData.expires_in,
|
|
8423
|
+
fetchPoll: fetchPollCapture,
|
|
8424
|
+
// Only pass sleepFn if the caller explicitly injected one (test seam).
|
|
8425
|
+
// When sleepFn is omitted, startDeviceFlow uses its own defaultSleep.
|
|
8426
|
+
// This preserves the original bump-suppression logic:
|
|
8427
|
+
// shouldApplyBump = (sleepFn provided) || (intervalOverrideMs not set).
|
|
8428
|
+
...opts.sleepFn !== void 0 ? { sleepFn: opts.sleepFn } : {},
|
|
8429
|
+
...opts.intervalOverrideMs !== void 0 ? { intervalOverrideMs: opts.intervalOverrideMs } : {},
|
|
8430
|
+
deadlineGraceMs: 1e4
|
|
8431
|
+
});
|
|
8432
|
+
} catch (err) {
|
|
8433
|
+
if (err instanceof DeviceFlowError) {
|
|
8434
|
+
switch (err.code) {
|
|
8435
|
+
case "access_denied":
|
|
8436
|
+
stderr("cleargate: error: access denied \u2014 you declined authorization in the browser.");
|
|
8437
|
+
return exitFn(5);
|
|
8438
|
+
case "not_admin":
|
|
8439
|
+
stderr("cleargate: error: your GitHub account is not authorized as an admin user.");
|
|
8440
|
+
return exitFn(4);
|
|
8441
|
+
case "expired_token":
|
|
8442
|
+
stderr("cleargate: error: device code expired \u2014 please run `cleargate admin login` again.");
|
|
8443
|
+
return exitFn(5);
|
|
8444
|
+
case "timeout":
|
|
8445
|
+
stderr("cleargate: error: timed out waiting for authorization. Please try again.");
|
|
8446
|
+
return exitFn(5);
|
|
8447
|
+
case "unreachable":
|
|
8448
|
+
stderr(`cleargate: error: network error while polling`);
|
|
8449
|
+
return exitFn(3);
|
|
8450
|
+
default:
|
|
8451
|
+
stderr(`cleargate: error: unexpected server response`);
|
|
8452
|
+
return exitFn(6);
|
|
7910
8453
|
}
|
|
7911
|
-
continue;
|
|
7912
8454
|
}
|
|
7913
|
-
|
|
7914
|
-
|
|
8455
|
+
stderr(`cleargate: error: unexpected error during device flow`);
|
|
8456
|
+
return exitFn(6);
|
|
7915
8457
|
}
|
|
7916
|
-
if (!
|
|
8458
|
+
if (!capturedSuccessBody) {
|
|
7917
8459
|
stderr("cleargate: error: timed out waiting for authorization. Please try again.");
|
|
7918
8460
|
return exitFn(5);
|
|
7919
8461
|
}
|
|
8462
|
+
const successBody = capturedSuccessBody;
|
|
7920
8463
|
const authFilePath = resolveAuthFilePath(opts);
|
|
7921
8464
|
try {
|
|
7922
|
-
writeAdminAuth(authFilePath,
|
|
8465
|
+
writeAdminAuth(authFilePath, successBody.admin_token);
|
|
7923
8466
|
} catch (err) {
|
|
7924
8467
|
stderr(`cleargate: error: failed to write ${authFilePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7925
8468
|
return exitFn(99);
|
|
7926
8469
|
}
|
|
7927
|
-
stdout(`Logged in successfully. Token expires ${
|
|
8470
|
+
stdout(`Logged in successfully. Token expires ${successBody.expires_at}.`);
|
|
7928
8471
|
stdout(`Credentials saved to ${authFilePath} (chmod 600).`);
|
|
7929
8472
|
}
|
|
7930
8473
|
|
|
7931
8474
|
// src/cli.ts
|
|
7932
8475
|
var program = new Command();
|
|
7933
8476
|
program.name("cleargate").description("ClearGate CLI \u2014 connects AI agent teams to the ClearGate MCP server").version(package_default.version, "-V, --version").option("--profile <name>", "configuration profile to use", "default").option("--mcp-url <url>", "MCP server URL (overrides config file and env)").showHelpAfterError("(use `cleargate --help`)");
|
|
7934
|
-
program.command("join <invite-url>").description("join a ClearGate workspace using an invite URL").action(async (inviteUrl, _opts, command) => {
|
|
8477
|
+
program.command("join <invite-url>").description("join a ClearGate workspace using an invite URL").option("--auth <provider>", "identity provider: github | email").option("--non-interactive", "fail instead of prompting (CI mode)").option("--code <code>", "OTP code for non-interactive email auth").action(async (inviteUrl, _opts, command) => {
|
|
7935
8478
|
const globals = command.parent.opts();
|
|
8479
|
+
const cmdOpts = command.opts();
|
|
7936
8480
|
await joinHandler({
|
|
7937
8481
|
inviteUrl,
|
|
7938
8482
|
profile: globals.profile,
|
|
7939
|
-
mcpUrlFlag: globals.mcpUrl
|
|
8483
|
+
mcpUrlFlag: globals.mcpUrl,
|
|
8484
|
+
// FLASHCARD #cli #commander #optional-key: only set keys when defined
|
|
8485
|
+
...cmdOpts.auth !== void 0 ? { auth: cmdOpts.auth } : {},
|
|
8486
|
+
...cmdOpts.nonInteractive === true ? { nonInteractive: true } : {},
|
|
8487
|
+
...cmdOpts.code !== void 0 ? { code: cmdOpts.code } : {}
|
|
7940
8488
|
});
|
|
7941
8489
|
});
|
|
7942
8490
|
program.command("init").description("initialise a repo with ClearGate scaffold (CLAUDE.md block, hook config, agents, templates)").option("--force", "overwrite existing files that differ from the bundled payload").option("--yes", "non-interactive: accept all defaults without prompting").action(async (opts) => {
|