@autoclawd/autoclawd 1.0.3 → 1.0.5
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/index.js +439 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,9 @@ var LinearConfigSchema = z.object({
|
|
|
34
34
|
inProgressStatus: z.string().default("In Progress")
|
|
35
35
|
});
|
|
36
36
|
var GitHubConfigSchema = z.object({
|
|
37
|
-
token: z.string()
|
|
37
|
+
token: z.string(),
|
|
38
|
+
autoMerge: z.boolean().default(false),
|
|
39
|
+
mergeMethod: z.enum(["merge", "squash", "rebase"]).default("squash")
|
|
38
40
|
});
|
|
39
41
|
var WebhookConfigSchema = z.object({
|
|
40
42
|
port: z.number().default(3e3),
|
|
@@ -393,6 +395,10 @@ ${reason}`
|
|
|
393
395
|
|
|
394
396
|
// src/github.ts
|
|
395
397
|
import { Octokit } from "@octokit/rest";
|
|
398
|
+
import { execSync } from "child_process";
|
|
399
|
+
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
400
|
+
import { join as join3 } from "path";
|
|
401
|
+
import { tmpdir } from "os";
|
|
396
402
|
function createOctokit(config) {
|
|
397
403
|
return new Octokit({ auth: config.github.token });
|
|
398
404
|
}
|
|
@@ -557,6 +563,144 @@ async function fetchFailedChecks(octokit, opts) {
|
|
|
557
563
|
}
|
|
558
564
|
return results;
|
|
559
565
|
}
|
|
566
|
+
async function enableAutoMerge(octokit, opts) {
|
|
567
|
+
return retry(async () => {
|
|
568
|
+
const { owner, repo } = parseRepoUrl(opts.repoUrl);
|
|
569
|
+
const { data: pr } = await octokit.rest.pulls.get({
|
|
570
|
+
owner,
|
|
571
|
+
repo,
|
|
572
|
+
pull_number: opts.prNumber
|
|
573
|
+
});
|
|
574
|
+
try {
|
|
575
|
+
await octokit.graphql(`
|
|
576
|
+
mutation EnableAutoMerge($prId: ID!, $mergeMethod: PullRequestMergeMethod!) {
|
|
577
|
+
enablePullRequestAutoMerge(input: {pullRequestId: $prId, mergeMethod: $mergeMethod}) {
|
|
578
|
+
pullRequest { autoMergeRequest { enabledAt } }
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
`, {
|
|
582
|
+
prId: pr.node_id,
|
|
583
|
+
mergeMethod: opts.mergeMethod ?? "SQUASH"
|
|
584
|
+
});
|
|
585
|
+
log.info(`Auto-merge enabled on PR #${opts.prNumber} (${opts.mergeMethod ?? "SQUASH"})`);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
588
|
+
const msgLower = msg.toLowerCase();
|
|
589
|
+
if (msgLower.includes("auto-merge") || msgLower.includes("not allowed") || msgLower.includes("branch protection") || msgLower.includes("protected branch")) {
|
|
590
|
+
log.warn(`Auto-merge not available on PR #${opts.prNumber}: ${msg}`);
|
|
591
|
+
} else {
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}, { label: "GitHub auto-merge", retryIf: isTransientError });
|
|
596
|
+
}
|
|
597
|
+
async function retargetPR(octokit, opts) {
|
|
598
|
+
return retry(async () => {
|
|
599
|
+
const { owner, repo } = parseRepoUrl(opts.repoUrl);
|
|
600
|
+
await octokit.rest.pulls.update({
|
|
601
|
+
owner,
|
|
602
|
+
repo,
|
|
603
|
+
pull_number: opts.prNumber,
|
|
604
|
+
base: opts.newBase
|
|
605
|
+
});
|
|
606
|
+
log.info(`Retargeted PR #${opts.prNumber} to base ${opts.newBase}`);
|
|
607
|
+
}, { label: "GitHub retarget", retryIf: isTransientError });
|
|
608
|
+
}
|
|
609
|
+
function createAskpass(githubToken) {
|
|
610
|
+
const dir = mkdtempSync(join3(tmpdir(), "autoclawd-rebase-"));
|
|
611
|
+
const path = join3(dir, "askpass.sh");
|
|
612
|
+
writeFileSync(path, `#!/bin/sh
|
|
613
|
+
echo "${githubToken}"
|
|
614
|
+
`, { mode: 448 });
|
|
615
|
+
return { dir, path };
|
|
616
|
+
}
|
|
617
|
+
function gitEnv(askpassPath) {
|
|
618
|
+
return { ...process.env, GIT_ASKPASS: askpassPath, GIT_TERMINAL_PROMPT: "0" };
|
|
619
|
+
}
|
|
620
|
+
function detectDefaultBranch(repoUrl, githubToken) {
|
|
621
|
+
const askpass = createAskpass(githubToken);
|
|
622
|
+
const authedUrl = repoUrl.replace("https://", "https://x-access-token@");
|
|
623
|
+
try {
|
|
624
|
+
const output = execSync(
|
|
625
|
+
`git ls-remote --symref -- ${authedUrl} HEAD`,
|
|
626
|
+
{ stdio: "pipe", timeout: 3e4, env: gitEnv(askpass.path), encoding: "utf-8" }
|
|
627
|
+
);
|
|
628
|
+
const match = output.match(/ref:\s+refs\/heads\/(\S+)\s+HEAD/);
|
|
629
|
+
if (match) return match[1];
|
|
630
|
+
} catch {
|
|
631
|
+
} finally {
|
|
632
|
+
rmSync(askpass.dir, { recursive: true, force: true });
|
|
633
|
+
}
|
|
634
|
+
return "main";
|
|
635
|
+
}
|
|
636
|
+
async function rebaseStackedPRs(octokit, opts) {
|
|
637
|
+
const { owner, repo } = parseRepoUrl(opts.repoUrl);
|
|
638
|
+
const defaultBranch = detectDefaultBranch(opts.repoUrl, opts.githubToken);
|
|
639
|
+
const prefix = opts.branchPrefix ?? "autoclawd/";
|
|
640
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
641
|
+
owner,
|
|
642
|
+
repo,
|
|
643
|
+
state: "open",
|
|
644
|
+
per_page: 100
|
|
645
|
+
});
|
|
646
|
+
const stackedPRs = openPRs.filter(
|
|
647
|
+
(pr) => pr.head.ref.startsWith(prefix) && pr.base.ref !== defaultBranch
|
|
648
|
+
);
|
|
649
|
+
if (stackedPRs.length === 0) return 0;
|
|
650
|
+
let rebased = 0;
|
|
651
|
+
for (const pr of stackedPRs) {
|
|
652
|
+
const baseBranch = pr.base.ref;
|
|
653
|
+
const { data: basePRs } = await octokit.rest.pulls.list({
|
|
654
|
+
owner,
|
|
655
|
+
repo,
|
|
656
|
+
head: `${owner}:${baseBranch}`,
|
|
657
|
+
state: "closed"
|
|
658
|
+
});
|
|
659
|
+
const mergedBasePR = basePRs.find((p) => p.merged_at !== null);
|
|
660
|
+
if (!mergedBasePR) {
|
|
661
|
+
log.debug(`PR #${pr.number}: base "${baseBranch}" not yet merged, skipping`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
log.info(`PR #${pr.number} ("${pr.title}"): base "${baseBranch}" was merged, rebasing onto ${defaultBranch}`);
|
|
665
|
+
const askpass = createAskpass(opts.githubToken);
|
|
666
|
+
const workDir = mkdtempSync(join3(tmpdir(), "autoclawd-rebase-"));
|
|
667
|
+
try {
|
|
668
|
+
const authedUrl = opts.repoUrl.replace("https://", "https://x-access-token@");
|
|
669
|
+
const env = gitEnv(askpass.path);
|
|
670
|
+
const run = (cmd) => execSync(cmd, { cwd: workDir, stdio: "pipe", timeout: 12e4, env, encoding: "utf-8" });
|
|
671
|
+
run(`git clone --no-tags -- ${authedUrl} .`);
|
|
672
|
+
run(`git checkout ${pr.head.ref}`);
|
|
673
|
+
run(`git fetch origin ${defaultBranch}`);
|
|
674
|
+
try {
|
|
675
|
+
run(`git rebase --onto origin/${defaultBranch} origin/${baseBranch}`);
|
|
676
|
+
} catch {
|
|
677
|
+
try {
|
|
678
|
+
run("git rebase --abort");
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
log.warn(`PR #${pr.number}: rebase conflict, skipping (manual resolution needed)`);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
run(`git config credential.helper '!f() { echo "password=${opts.githubToken}"; echo "username=x-access-token"; }; f'`);
|
|
685
|
+
run(`git push --force-with-lease origin ${pr.head.ref}`);
|
|
686
|
+
await retargetPR(octokit, {
|
|
687
|
+
repoUrl: opts.repoUrl,
|
|
688
|
+
prNumber: pr.number,
|
|
689
|
+
newBase: defaultBranch
|
|
690
|
+
});
|
|
691
|
+
log.success(`PR #${pr.number}: rebased onto ${defaultBranch} and retargeted`);
|
|
692
|
+
rebased++;
|
|
693
|
+
} catch (err) {
|
|
694
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
695
|
+
const safe = msg.replace(/x-access-token:[^\s@]+/g, "x-access-token:***");
|
|
696
|
+
log.error(`PR #${pr.number}: rebase failed: ${safe}`);
|
|
697
|
+
} finally {
|
|
698
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
699
|
+
rmSync(askpass.dir, { recursive: true, force: true });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return rebased;
|
|
703
|
+
}
|
|
560
704
|
|
|
561
705
|
// src/webhook.ts
|
|
562
706
|
import { createServer } from "http";
|
|
@@ -565,18 +709,123 @@ import { createHmac, timingSafeEqual } from "crypto";
|
|
|
565
709
|
// src/docker.ts
|
|
566
710
|
import Docker from "dockerode";
|
|
567
711
|
import { PassThrough } from "stream";
|
|
568
|
-
import { homedir as homedir3 } from "os";
|
|
569
|
-
import { join as
|
|
712
|
+
import { homedir as homedir3, platform } from "os";
|
|
713
|
+
import { join as join4 } from "path";
|
|
570
714
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
715
|
+
import { execSync as execSync2 } from "child_process";
|
|
571
716
|
var docker = new Docker();
|
|
717
|
+
function sleep2(ms) {
|
|
718
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
719
|
+
}
|
|
720
|
+
async function tryStartDocker() {
|
|
721
|
+
const os = platform();
|
|
722
|
+
if (os === "darwin") {
|
|
723
|
+
if (existsSync3("/Applications/Docker.app")) {
|
|
724
|
+
log.info("Docker not running \u2014 starting Docker Desktop...");
|
|
725
|
+
try {
|
|
726
|
+
execSync2("open -a Docker", { stdio: "pipe" });
|
|
727
|
+
} catch {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
} else if (os === "linux") {
|
|
734
|
+
log.info("Docker not running \u2014 attempting to start...");
|
|
735
|
+
try {
|
|
736
|
+
execSync2("sudo systemctl start docker 2>/dev/null || sudo service docker start 2>/dev/null", {
|
|
737
|
+
stdio: "pipe",
|
|
738
|
+
timeout: 3e4
|
|
739
|
+
});
|
|
740
|
+
} catch {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
for (let i = 0; i < 30; i++) {
|
|
747
|
+
await sleep2(1e3);
|
|
748
|
+
try {
|
|
749
|
+
await docker.ping();
|
|
750
|
+
log.success("Docker started");
|
|
751
|
+
return true;
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
if (i > 0 && i % 5 === 0) {
|
|
755
|
+
log.info(`Waiting for Docker daemon... (${i}s)`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
async function tryInstallDocker() {
|
|
761
|
+
const os = platform();
|
|
762
|
+
if (os === "darwin") {
|
|
763
|
+
try {
|
|
764
|
+
execSync2("which brew", { stdio: "pipe" });
|
|
765
|
+
} catch {
|
|
766
|
+
log.warn("Docker not installed and Homebrew not found \u2014 cannot auto-install");
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
log.info("Docker not installed \u2014 installing via Homebrew (this may take a few minutes)...");
|
|
770
|
+
try {
|
|
771
|
+
execSync2("brew install --cask docker", { stdio: "inherit", timeout: 6e5 });
|
|
772
|
+
log.success("Docker Desktop installed");
|
|
773
|
+
return true;
|
|
774
|
+
} catch {
|
|
775
|
+
log.warn("Docker install via Homebrew failed");
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
} else if (os === "linux") {
|
|
779
|
+
const cmds = [
|
|
780
|
+
{ check: "apt-get", install: "sudo apt-get update -qq && sudo apt-get install -y docker.io && sudo systemctl enable docker" },
|
|
781
|
+
{ check: "dnf", install: "sudo dnf install -y docker && sudo systemctl enable docker" },
|
|
782
|
+
{ check: "yum", install: "sudo yum install -y docker && sudo systemctl enable docker" },
|
|
783
|
+
{ check: "pacman", install: "sudo pacman -Sy --noconfirm docker && sudo systemctl enable docker" }
|
|
784
|
+
];
|
|
785
|
+
for (const { check, install } of cmds) {
|
|
786
|
+
try {
|
|
787
|
+
execSync2(`which ${check}`, { stdio: "pipe" });
|
|
788
|
+
log.info(`Docker not installed \u2014 installing via ${check}...`);
|
|
789
|
+
execSync2(install, { stdio: "inherit", timeout: 3e5 });
|
|
790
|
+
log.success("Docker installed");
|
|
791
|
+
return true;
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
572
799
|
async function checkDockerAvailable() {
|
|
573
800
|
try {
|
|
574
801
|
await docker.ping();
|
|
575
|
-
|
|
802
|
+
return;
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
const started = await tryStartDocker();
|
|
806
|
+
if (started) return;
|
|
807
|
+
let installed = false;
|
|
808
|
+
try {
|
|
809
|
+
execSync2("which docker", { stdio: "pipe" });
|
|
810
|
+
installed = true;
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
if (installed) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
"Docker is installed but the daemon is not running and could not be started automatically.\nStart it manually:\n" + (platform() === "darwin" ? " open -a Docker\n" : " sudo systemctl start docker\n")
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
const didInstall = await tryInstallDocker();
|
|
819
|
+
if (didInstall) {
|
|
820
|
+
const started2 = await tryStartDocker();
|
|
821
|
+
if (started2) return;
|
|
576
822
|
throw new Error(
|
|
577
|
-
"
|
|
823
|
+
"Docker was installed but could not be started.\n" + (platform() === "darwin" ? "Open Docker Desktop from Applications and try again.\n" : "Run: sudo systemctl start docker\n")
|
|
578
824
|
);
|
|
579
825
|
}
|
|
826
|
+
throw new Error(
|
|
827
|
+
"Docker is not installed and could not be auto-installed.\n" + (platform() === "darwin" ? "Install manually: brew install --cask docker\n" : "Install manually: https://docs.docker.com/get-docker/\n")
|
|
828
|
+
);
|
|
580
829
|
}
|
|
581
830
|
async function ensureImage(image) {
|
|
582
831
|
try {
|
|
@@ -766,7 +1015,7 @@ async function copyClaudeCredentials(container) {
|
|
|
766
1015
|
"-c",
|
|
767
1016
|
"mkdir -p /home/autoclawd/.claude && chown autoclawd /home/autoclawd/.claude"
|
|
768
1017
|
]);
|
|
769
|
-
const credFile =
|
|
1018
|
+
const credFile = join4(home, ".claude", ".credentials.json");
|
|
770
1019
|
if (existsSync3(credFile)) {
|
|
771
1020
|
const content = readFileSync2(credFile, "utf-8");
|
|
772
1021
|
const b64 = Buffer.from(content).toString("base64");
|
|
@@ -777,7 +1026,7 @@ async function copyClaudeCredentials(container) {
|
|
|
777
1026
|
]);
|
|
778
1027
|
log.debug("Copied .credentials.json");
|
|
779
1028
|
}
|
|
780
|
-
const settingsFile =
|
|
1029
|
+
const settingsFile = join4(home, ".claude", "settings.json");
|
|
781
1030
|
if (existsSync3(settingsFile)) {
|
|
782
1031
|
const content = readFileSync2(settingsFile, "utf-8");
|
|
783
1032
|
const b64 = Buffer.from(content).toString("base64");
|
|
@@ -969,7 +1218,7 @@ async function runAgentLoop(opts) {
|
|
|
969
1218
|
}
|
|
970
1219
|
const waitMs = estimateResetMs(combined);
|
|
971
1220
|
log.ticket(ticketId, `Rate limited (${rateLimitRetries}/${MAX_RATE_LIMIT_RETRIES}) \u2014 pausing ${Math.round(waitMs / 1e3)}s`);
|
|
972
|
-
await
|
|
1221
|
+
await sleep3(waitMs);
|
|
973
1222
|
i--;
|
|
974
1223
|
continue;
|
|
975
1224
|
}
|
|
@@ -980,7 +1229,7 @@ async function runAgentLoop(opts) {
|
|
|
980
1229
|
log.ticket(ticketId, `Max iterations (${agentConfig.maxIterations}) reached`);
|
|
981
1230
|
return { iterations: agentConfig.maxIterations, success: false, lastOutput };
|
|
982
1231
|
}
|
|
983
|
-
function
|
|
1232
|
+
function sleep3(ms) {
|
|
984
1233
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
985
1234
|
}
|
|
986
1235
|
|
|
@@ -1051,16 +1300,16 @@ async function commitAndPush(container, opts) {
|
|
|
1051
1300
|
}
|
|
1052
1301
|
|
|
1053
1302
|
// src/worker.ts
|
|
1054
|
-
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
1055
|
-
import { join as
|
|
1056
|
-
import { tmpdir } from "os";
|
|
1057
|
-
import { execSync } from "child_process";
|
|
1303
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1304
|
+
import { join as join6 } from "path";
|
|
1305
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1306
|
+
import { execSync as execSync3 } from "child_process";
|
|
1058
1307
|
|
|
1059
1308
|
// src/db.ts
|
|
1060
1309
|
import Database from "better-sqlite3";
|
|
1061
|
-
import { join as
|
|
1310
|
+
import { join as join5 } from "path";
|
|
1062
1311
|
import { mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
1063
|
-
var DB_PATH =
|
|
1312
|
+
var DB_PATH = join5(CONFIG_DIR, "autoclawd.db");
|
|
1064
1313
|
var _db;
|
|
1065
1314
|
function getDb() {
|
|
1066
1315
|
if (_db) return _db;
|
|
@@ -1138,6 +1387,10 @@ function isTicketProcessed(ticketId) {
|
|
|
1138
1387
|
).get(ticketId);
|
|
1139
1388
|
return !!row;
|
|
1140
1389
|
}
|
|
1390
|
+
function removeFromProcessed(ticketId) {
|
|
1391
|
+
const db = getDb();
|
|
1392
|
+
db.prepare("DELETE FROM runs WHERE id = ? AND status = 'failed'").run(ticketId);
|
|
1393
|
+
}
|
|
1141
1394
|
function isTicketActive(ticketId) {
|
|
1142
1395
|
const db = getDb();
|
|
1143
1396
|
const row = db.prepare(
|
|
@@ -1223,75 +1476,75 @@ function assertSafeRef(name, label) {
|
|
|
1223
1476
|
throw new Error(`Invalid ${label}: "${name}"`);
|
|
1224
1477
|
}
|
|
1225
1478
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const dir =
|
|
1228
|
-
const path =
|
|
1229
|
-
|
|
1479
|
+
function createAskpass2(githubToken) {
|
|
1480
|
+
const dir = mkdtempSync2(join6(tmpdir2(), "autoclawd-cred-"));
|
|
1481
|
+
const path = join6(dir, "askpass.sh");
|
|
1482
|
+
writeFileSync2(path, `#!/bin/sh
|
|
1230
1483
|
echo "${githubToken}"
|
|
1231
1484
|
`, { mode: 448 });
|
|
1232
1485
|
return { dir, path };
|
|
1233
1486
|
}
|
|
1234
|
-
function
|
|
1487
|
+
function gitEnv2(askpassPath) {
|
|
1235
1488
|
return { ...process.env, GIT_ASKPASS: askpassPath, GIT_TERMINAL_PROMPT: "0" };
|
|
1236
1489
|
}
|
|
1237
|
-
function
|
|
1238
|
-
const askpass =
|
|
1490
|
+
function detectDefaultBranch2(repoUrl, githubToken) {
|
|
1491
|
+
const askpass = createAskpass2(githubToken);
|
|
1239
1492
|
const authedUrl = repoUrl.replace("https://", "https://x-access-token@");
|
|
1240
1493
|
try {
|
|
1241
|
-
const output =
|
|
1494
|
+
const output = execSync3(
|
|
1242
1495
|
`git ls-remote --symref -- ${authedUrl} HEAD`,
|
|
1243
|
-
{ stdio: "pipe", timeout: 3e4, env:
|
|
1496
|
+
{ stdio: "pipe", timeout: 3e4, env: gitEnv2(askpass.path), encoding: "utf-8" }
|
|
1244
1497
|
);
|
|
1245
1498
|
const match = output.match(/ref:\s+refs\/heads\/(\S+)\s+HEAD/);
|
|
1246
1499
|
if (match) return match[1];
|
|
1247
1500
|
} catch {
|
|
1248
1501
|
} finally {
|
|
1249
|
-
|
|
1502
|
+
rmSync2(askpass.dir, { recursive: true, force: true });
|
|
1250
1503
|
}
|
|
1251
1504
|
return "main";
|
|
1252
1505
|
}
|
|
1253
1506
|
async function pushScaffold(opts) {
|
|
1254
|
-
const scaffoldDir =
|
|
1255
|
-
const askpass =
|
|
1507
|
+
const scaffoldDir = mkdtempSync2(join6(tmpdir2(), "autoclawd-scaffold-"));
|
|
1508
|
+
const askpass = createAskpass2(opts.githubToken);
|
|
1256
1509
|
try {
|
|
1257
1510
|
const authedUrl = opts.repoUrl.replace("https://", "https://x-access-token@");
|
|
1258
|
-
const env =
|
|
1511
|
+
const env = gitEnv2(askpass.path);
|
|
1259
1512
|
const readmeLines = [`# ${opts.ticketTitle}`];
|
|
1260
1513
|
if (opts.ticketDescription) {
|
|
1261
1514
|
readmeLines.push("", opts.ticketDescription);
|
|
1262
1515
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1516
|
+
writeFileSync2(join6(scaffoldDir, "README.md"), readmeLines.join("\n") + "\n");
|
|
1517
|
+
writeFileSync2(join6(scaffoldDir, ".autoclawd.yaml"), "base: main\n");
|
|
1518
|
+
execSync3("git init", { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1519
|
+
execSync3("git checkout -b main", { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1520
|
+
execSync3('git config user.email "autoclawd@users.noreply.github.com"', { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1521
|
+
execSync3('git config user.name "autoclawd"', { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1522
|
+
execSync3("git add .", { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1523
|
+
execSync3('git commit -m "chore: initial scaffold"', { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1524
|
+
execSync3(`git remote add origin ${authedUrl}`, { cwd: scaffoldDir, stdio: "pipe", env });
|
|
1525
|
+
execSync3("git push -u origin main", { cwd: scaffoldDir, stdio: "pipe", timeout: 6e4, env });
|
|
1273
1526
|
} finally {
|
|
1274
|
-
|
|
1275
|
-
|
|
1527
|
+
rmSync2(scaffoldDir, { recursive: true, force: true });
|
|
1528
|
+
rmSync2(askpass.dir, { recursive: true, force: true });
|
|
1276
1529
|
}
|
|
1277
1530
|
}
|
|
1278
1531
|
async function cloneToTemp(repoUrl, baseBranch, githubToken) {
|
|
1279
1532
|
assertSafeRef(baseBranch, "base branch");
|
|
1280
1533
|
return retrySync(() => {
|
|
1281
|
-
const workDir =
|
|
1282
|
-
const askpass =
|
|
1534
|
+
const workDir = mkdtempSync2(join6(tmpdir2(), "autoclawd-"));
|
|
1535
|
+
const askpass = createAskpass2(githubToken);
|
|
1283
1536
|
const authedUrl = repoUrl.replace("https://", "https://x-access-token@");
|
|
1284
1537
|
try {
|
|
1285
|
-
|
|
1538
|
+
execSync3(`git clone --depth=50 -b ${baseBranch} -- ${authedUrl} ${workDir}`, {
|
|
1286
1539
|
stdio: "pipe",
|
|
1287
1540
|
timeout: 12e4,
|
|
1288
|
-
env:
|
|
1541
|
+
env: gitEnv2(askpass.path)
|
|
1289
1542
|
});
|
|
1290
1543
|
} catch (err) {
|
|
1291
|
-
|
|
1544
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
1292
1545
|
throw err;
|
|
1293
1546
|
} finally {
|
|
1294
|
-
|
|
1547
|
+
rmSync2(askpass.dir, { recursive: true, force: true });
|
|
1295
1548
|
}
|
|
1296
1549
|
return workDir;
|
|
1297
1550
|
}, { label: "git clone", retryIf: isTransientError });
|
|
@@ -1428,7 +1681,7 @@ async function executeTicket(opts) {
|
|
|
1428
1681
|
});
|
|
1429
1682
|
log.ticket(ticket.identifier, "Repo created, pushed scaffold");
|
|
1430
1683
|
}
|
|
1431
|
-
const detectedBase = repoCreated ? "main" :
|
|
1684
|
+
const detectedBase = repoCreated ? "main" : detectDefaultBranch2(ticket.repoUrl, config.github.token);
|
|
1432
1685
|
log.ticket(ticket.identifier, `Default branch: ${detectedBase}`);
|
|
1433
1686
|
workDir = await cloneToTemp(ticket.repoUrl, detectedBase, config.github.token);
|
|
1434
1687
|
log.ticket(ticket.identifier, "Cloned repo");
|
|
@@ -1442,10 +1695,24 @@ async function executeTicket(opts) {
|
|
|
1442
1695
|
log.ticket(ticket.identifier, `Stacked on branch: ${ticket.baseBranch}`);
|
|
1443
1696
|
}
|
|
1444
1697
|
if (actualBase !== detectedBase) {
|
|
1445
|
-
|
|
1446
|
-
|
|
1698
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
1699
|
+
try {
|
|
1700
|
+
workDir = await cloneToTemp(ticket.repoUrl, actualBase, config.github.token);
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1703
|
+
if (msg.includes("not found in upstream") || msg.includes("Could not find remote branch")) {
|
|
1704
|
+
log.ticket(ticket.identifier, `Base branch "${actualBase}" not found \u2014 will retry later`);
|
|
1705
|
+
return {
|
|
1706
|
+
ticketId: ticket.identifier,
|
|
1707
|
+
success: false,
|
|
1708
|
+
error: `REQUEUE: base branch "${actualBase}" does not exist yet`,
|
|
1709
|
+
iterations: 0
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
throw err;
|
|
1713
|
+
}
|
|
1447
1714
|
}
|
|
1448
|
-
|
|
1715
|
+
execSync3(`git checkout -B ${branchName} --`, { cwd: workDir, stdio: "pipe" });
|
|
1449
1716
|
container = await createContainer({
|
|
1450
1717
|
dockerConfig: docker2,
|
|
1451
1718
|
workspacePath: workDir,
|
|
@@ -1520,6 +1787,10 @@ async function executeTicket(opts) {
|
|
|
1520
1787
|
body: buildPRBody({ commitCount: gitResult.commitCount }),
|
|
1521
1788
|
draft: hasValidation ? true : false
|
|
1522
1789
|
});
|
|
1790
|
+
if (!hasValidation && config.github.autoMerge) {
|
|
1791
|
+
const method = config.github.mergeMethod.toUpperCase();
|
|
1792
|
+
await enableAutoMerge(octokit, { repoUrl: ticket.repoUrl, prNumber: pr.number, mergeMethod: method });
|
|
1793
|
+
}
|
|
1523
1794
|
if (hasValidation) {
|
|
1524
1795
|
let iterationsUsed = agentResult.iterations;
|
|
1525
1796
|
let lastFailures = [];
|
|
@@ -1575,6 +1846,10 @@ async function executeTicket(opts) {
|
|
|
1575
1846
|
prNumber: pr.number
|
|
1576
1847
|
});
|
|
1577
1848
|
log.ticket(ticket.identifier, "PR marked as ready for review");
|
|
1849
|
+
if (config.github.autoMerge) {
|
|
1850
|
+
const method = config.github.mergeMethod.toUpperCase();
|
|
1851
|
+
await enableAutoMerge(octokit, { repoUrl: ticket.repoUrl, prNumber: pr.number, mergeMethod: method });
|
|
1852
|
+
}
|
|
1578
1853
|
}
|
|
1579
1854
|
}
|
|
1580
1855
|
await completeTicket(linearClient, config, ticket.id, `PR opened: ${pr.url}`);
|
|
@@ -1612,7 +1887,7 @@ async function executeTicket(opts) {
|
|
|
1612
1887
|
}
|
|
1613
1888
|
if (workDir) {
|
|
1614
1889
|
try {
|
|
1615
|
-
|
|
1890
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
1616
1891
|
} catch {
|
|
1617
1892
|
}
|
|
1618
1893
|
}
|
|
@@ -1701,7 +1976,7 @@ async function fixPR(opts) {
|
|
|
1701
1976
|
if (container) await destroyContainer(container);
|
|
1702
1977
|
if (workDir) {
|
|
1703
1978
|
try {
|
|
1704
|
-
|
|
1979
|
+
rmSync2(workDir, { recursive: true, force: true });
|
|
1705
1980
|
} catch {
|
|
1706
1981
|
}
|
|
1707
1982
|
}
|
|
@@ -1884,8 +2159,17 @@ var WebhookServer = class {
|
|
|
1884
2159
|
linearClient: this.linearClient,
|
|
1885
2160
|
octokit: this.octokit
|
|
1886
2161
|
}).then(
|
|
1887
|
-
() => {
|
|
1888
|
-
|
|
2162
|
+
(result) => {
|
|
2163
|
+
if (result.error?.startsWith("REQUEUE:")) {
|
|
2164
|
+
log.ticket(ticket.identifier, "Re-queued (waiting for dependency branch)");
|
|
2165
|
+
finishRun(ticket.id, "failed", result.error);
|
|
2166
|
+
removeFromProcessed(ticket.id);
|
|
2167
|
+
enqueue(ticket);
|
|
2168
|
+
} else if (result.success) {
|
|
2169
|
+
finishRun(ticket.id, "success");
|
|
2170
|
+
} else {
|
|
2171
|
+
finishRun(ticket.id, "failed", result.error);
|
|
2172
|
+
}
|
|
1889
2173
|
},
|
|
1890
2174
|
(err) => {
|
|
1891
2175
|
finishRun(ticket.id, "failed", err.message);
|
|
@@ -1966,6 +2250,8 @@ var Watcher = class {
|
|
|
1966
2250
|
activeInMemory = /* @__PURE__ */ new Set();
|
|
1967
2251
|
timer;
|
|
1968
2252
|
stopped = false;
|
|
2253
|
+
pollCount = 0;
|
|
2254
|
+
rebaseRunning = false;
|
|
1969
2255
|
async start(intervalSeconds, once) {
|
|
1970
2256
|
getDb();
|
|
1971
2257
|
this.resumeQueue();
|
|
@@ -2030,6 +2316,36 @@ var Watcher = class {
|
|
|
2030
2316
|
} catch (err) {
|
|
2031
2317
|
log.error(`Poll failed: ${err instanceof Error ? err.message : err}`);
|
|
2032
2318
|
}
|
|
2319
|
+
this.pollCount++;
|
|
2320
|
+
if (this.pollCount % 5 === 0) {
|
|
2321
|
+
void this.rebaseSweep();
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
async rebaseSweep() {
|
|
2325
|
+
if (this.rebaseRunning) return;
|
|
2326
|
+
this.rebaseRunning = true;
|
|
2327
|
+
try {
|
|
2328
|
+
const repos = this.config.safety.allowedRepos;
|
|
2329
|
+
if (!repos || repos.length === 0) return;
|
|
2330
|
+
for (const allowed of repos) {
|
|
2331
|
+
const repoUrl = allowed.startsWith("https://") ? allowed : `https://github.com/${allowed}`;
|
|
2332
|
+
if (!isRepoAllowed(repoUrl, repos)) continue;
|
|
2333
|
+
try {
|
|
2334
|
+
const count = await rebaseStackedPRs(this.octokit, {
|
|
2335
|
+
repoUrl,
|
|
2336
|
+
githubToken: this.config.github.token,
|
|
2337
|
+
branchPrefix: this.config.safety.branchPrefix
|
|
2338
|
+
});
|
|
2339
|
+
if (count > 0) {
|
|
2340
|
+
log.info(`Rebase sweep: rebased ${count} stacked PR(s) in ${allowed}`);
|
|
2341
|
+
}
|
|
2342
|
+
} catch (err) {
|
|
2343
|
+
log.debug(`Rebase sweep failed for ${allowed}: ${err instanceof Error ? err.message : err}`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
} finally {
|
|
2347
|
+
this.rebaseRunning = false;
|
|
2348
|
+
}
|
|
2033
2349
|
}
|
|
2034
2350
|
isNewTicket(ticket) {
|
|
2035
2351
|
if (this.activeInMemory.has(ticket.id)) return false;
|
|
@@ -2047,8 +2363,17 @@ var Watcher = class {
|
|
|
2047
2363
|
linearClient: this.linearClient,
|
|
2048
2364
|
octokit: this.octokit
|
|
2049
2365
|
}).then(
|
|
2050
|
-
() => {
|
|
2051
|
-
|
|
2366
|
+
(result) => {
|
|
2367
|
+
if (result.error?.startsWith("REQUEUE:")) {
|
|
2368
|
+
log.ticket(ticket.identifier, "Re-queued (waiting for dependency branch)");
|
|
2369
|
+
finishRun(ticket.id, "failed", result.error);
|
|
2370
|
+
removeFromProcessed(ticket.id);
|
|
2371
|
+
enqueue(ticket);
|
|
2372
|
+
} else if (result.success) {
|
|
2373
|
+
finishRun(ticket.id, "success");
|
|
2374
|
+
} else {
|
|
2375
|
+
finishRun(ticket.id, "failed", result.error);
|
|
2376
|
+
}
|
|
2052
2377
|
},
|
|
2053
2378
|
(err) => {
|
|
2054
2379
|
finishRun(ticket.id, "failed", err.message);
|
|
@@ -2082,11 +2407,11 @@ var Watcher = class {
|
|
|
2082
2407
|
|
|
2083
2408
|
// src/history.ts
|
|
2084
2409
|
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
2085
|
-
import { join as
|
|
2410
|
+
import { join as join7 } from "path";
|
|
2086
2411
|
import { homedir as homedir4 } from "os";
|
|
2087
2412
|
import chalk2 from "chalk";
|
|
2088
|
-
var HISTORY_DIR =
|
|
2089
|
-
var HISTORY_FILE =
|
|
2413
|
+
var HISTORY_DIR = join7(homedir4(), ".autoclawd");
|
|
2414
|
+
var HISTORY_FILE = join7(HISTORY_DIR, "history.jsonl");
|
|
2090
2415
|
function runToRecord(run) {
|
|
2091
2416
|
return {
|
|
2092
2417
|
ticketId: run.ticket_id,
|
|
@@ -2176,9 +2501,9 @@ function printHistoryTable(records) {
|
|
|
2176
2501
|
}
|
|
2177
2502
|
|
|
2178
2503
|
// src/deps.ts
|
|
2179
|
-
import { execSync as
|
|
2504
|
+
import { execSync as execSync4 } from "child_process";
|
|
2180
2505
|
import { existsSync as existsSync6 } from "fs";
|
|
2181
|
-
import { join as
|
|
2506
|
+
import { join as join8 } from "path";
|
|
2182
2507
|
import { homedir as homedir5 } from "os";
|
|
2183
2508
|
function detectPackageManager() {
|
|
2184
2509
|
const checks = [
|
|
@@ -2191,7 +2516,7 @@ function detectPackageManager() {
|
|
|
2191
2516
|
];
|
|
2192
2517
|
for (const [name, cmd] of checks) {
|
|
2193
2518
|
try {
|
|
2194
|
-
|
|
2519
|
+
execSync4(`which ${cmd}`, { stdio: "pipe" });
|
|
2195
2520
|
return name;
|
|
2196
2521
|
} catch {
|
|
2197
2522
|
}
|
|
@@ -2264,7 +2589,7 @@ function checkDeps() {
|
|
|
2264
2589
|
},
|
|
2265
2590
|
{
|
|
2266
2591
|
name: "Claude credentials",
|
|
2267
|
-
installed: existsSync6(
|
|
2592
|
+
installed: existsSync6(join8(homedir5(), ".claude", ".credentials.json")),
|
|
2268
2593
|
installable: false,
|
|
2269
2594
|
installCommands: [],
|
|
2270
2595
|
manualInstructions: 'Run "claude" and complete the login flow'
|
|
@@ -2282,7 +2607,7 @@ function installDep(dep) {
|
|
|
2282
2607
|
if (dep.installCommands.length === 0) return false;
|
|
2283
2608
|
for (const cmd of dep.installCommands) {
|
|
2284
2609
|
try {
|
|
2285
|
-
|
|
2610
|
+
execSync4(cmd, { stdio: "inherit", timeout: 3e5 });
|
|
2286
2611
|
} catch {
|
|
2287
2612
|
return false;
|
|
2288
2613
|
}
|
|
@@ -2292,15 +2617,15 @@ function installDep(dep) {
|
|
|
2292
2617
|
function addUserToDockerGroup() {
|
|
2293
2618
|
if (process.platform !== "linux") return;
|
|
2294
2619
|
try {
|
|
2295
|
-
const user =
|
|
2296
|
-
|
|
2620
|
+
const user = execSync4("whoami", { encoding: "utf-8" }).trim();
|
|
2621
|
+
execSync4(`sudo usermod -aG docker ${user}`, { stdio: "pipe" });
|
|
2297
2622
|
log.info(`Added ${user} to docker group \u2014 log out and back in to apply`);
|
|
2298
2623
|
} catch {
|
|
2299
2624
|
}
|
|
2300
2625
|
}
|
|
2301
2626
|
function commandExists(cmd) {
|
|
2302
2627
|
try {
|
|
2303
|
-
|
|
2628
|
+
execSync4(`which ${cmd}`, { stdio: "pipe" });
|
|
2304
2629
|
return true;
|
|
2305
2630
|
} catch {
|
|
2306
2631
|
return false;
|
|
@@ -2308,7 +2633,7 @@ function commandExists(cmd) {
|
|
|
2308
2633
|
}
|
|
2309
2634
|
function isDockerRunning() {
|
|
2310
2635
|
try {
|
|
2311
|
-
|
|
2636
|
+
execSync4("docker info", { stdio: "pipe", timeout: 1e4 });
|
|
2312
2637
|
return true;
|
|
2313
2638
|
} catch {
|
|
2314
2639
|
return false;
|
|
@@ -2316,10 +2641,10 @@ function isDockerRunning() {
|
|
|
2316
2641
|
}
|
|
2317
2642
|
|
|
2318
2643
|
// src/index.ts
|
|
2319
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as
|
|
2320
|
-
import { execSync as
|
|
2644
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
2645
|
+
import { execSync as execSync5, spawn as spawn2 } from "child_process";
|
|
2321
2646
|
import { createInterface } from "readline";
|
|
2322
|
-
import { join as
|
|
2647
|
+
import { join as join9 } from "path";
|
|
2323
2648
|
import { homedir as homedir6 } from "os";
|
|
2324
2649
|
import { LinearClient as LinearClient2 } from "@linear/sdk";
|
|
2325
2650
|
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
@@ -2327,7 +2652,7 @@ import { createRequire } from "module";
|
|
|
2327
2652
|
var require2 = createRequire(import.meta.url);
|
|
2328
2653
|
var pkg = require2("../package.json");
|
|
2329
2654
|
var program = new Command();
|
|
2330
|
-
var WATCHER_PID_FILE =
|
|
2655
|
+
var WATCHER_PID_FILE = join9(homedir6(), ".autoclawd", "watcher.pid");
|
|
2331
2656
|
function readWatcherPid() {
|
|
2332
2657
|
try {
|
|
2333
2658
|
const content = readFileSync4(WATCHER_PID_FILE, "utf-8").trim();
|
|
@@ -2338,8 +2663,8 @@ function readWatcherPid() {
|
|
|
2338
2663
|
}
|
|
2339
2664
|
}
|
|
2340
2665
|
function writeWatcherPid(pid) {
|
|
2341
|
-
mkdirSync3(
|
|
2342
|
-
|
|
2666
|
+
mkdirSync3(join9(homedir6(), ".autoclawd"), { recursive: true });
|
|
2667
|
+
writeFileSync3(WATCHER_PID_FILE, String(pid), { mode: 384 });
|
|
2343
2668
|
}
|
|
2344
2669
|
function removeWatcherPid() {
|
|
2345
2670
|
try {
|
|
@@ -2396,25 +2721,7 @@ program.command("serve").description("Start webhook server with auto-tunnel").op
|
|
|
2396
2721
|
const webhookUrl = `${tunnel.url}/api/v1/linear/webhook`;
|
|
2397
2722
|
log.success(`Tunnel: ${tunnel.url}`);
|
|
2398
2723
|
log.info(`Webhook URL: ${webhookUrl}`);
|
|
2399
|
-
|
|
2400
|
-
try {
|
|
2401
|
-
await linearClient.deleteWebhook(config.webhook.webhookId);
|
|
2402
|
-
} catch {
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
const team = await linearClient.team(config.linear.teamId);
|
|
2406
|
-
const result = await linearClient.createWebhook({
|
|
2407
|
-
url: webhookUrl,
|
|
2408
|
-
resourceTypes: ["Issue"],
|
|
2409
|
-
label: "autoclawd",
|
|
2410
|
-
teamId: team.id
|
|
2411
|
-
});
|
|
2412
|
-
const webhook = await result.webhook;
|
|
2413
|
-
if (webhook) {
|
|
2414
|
-
log.success(`Linear webhook created \u2192 ${webhookUrl}`);
|
|
2415
|
-
config.webhook.webhookId = webhook.id;
|
|
2416
|
-
config.webhook.signingSecret = void 0;
|
|
2417
|
-
}
|
|
2724
|
+
log.info("Configure this URL as a webhook in Linear (Settings \u2192 API \u2192 Webhooks)");
|
|
2418
2725
|
} catch (err) {
|
|
2419
2726
|
log.warn(`Tunnel failed: ${err instanceof Error ? err.message : err}`);
|
|
2420
2727
|
log.info("Falling back to local-only mode. Set up your own reverse proxy.");
|
|
@@ -2669,6 +2976,37 @@ Retrying ticket ${id}...`);
|
|
|
2669
2976
|
process.off("SIGTERM", onSignal);
|
|
2670
2977
|
}
|
|
2671
2978
|
});
|
|
2979
|
+
program.command("rebase [repo]").description("Rebase stacked PRs whose base branch has been merged (e.g. autoclawd rebase owner/repo)").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose logging").action(async (repoArg, opts) => {
|
|
2980
|
+
if (opts.verbose) setLogLevel("debug");
|
|
2981
|
+
enableFileLogging();
|
|
2982
|
+
const config = loadConfig(opts.config);
|
|
2983
|
+
const octokit = createOctokit(config);
|
|
2984
|
+
let repos;
|
|
2985
|
+
if (repoArg) {
|
|
2986
|
+
repos = [repoArg.startsWith("https://") ? repoArg : `https://github.com/${repoArg}`];
|
|
2987
|
+
} else if (config.safety.allowedRepos && config.safety.allowedRepos.length > 0) {
|
|
2988
|
+
repos = config.safety.allowedRepos.map(
|
|
2989
|
+
(r) => r.startsWith("https://") ? r : `https://github.com/${r}`
|
|
2990
|
+
);
|
|
2991
|
+
} else {
|
|
2992
|
+
throw new Error("Provide a repo (autoclawd rebase owner/repo) or configure safety.allowedRepos");
|
|
2993
|
+
}
|
|
2994
|
+
let totalRebased = 0;
|
|
2995
|
+
for (const repoUrl of repos) {
|
|
2996
|
+
log.info(`Checking ${repoUrl} for stacked PRs to rebase...`);
|
|
2997
|
+
const count = await rebaseStackedPRs(octokit, {
|
|
2998
|
+
repoUrl,
|
|
2999
|
+
githubToken: config.github.token,
|
|
3000
|
+
branchPrefix: config.safety.branchPrefix
|
|
3001
|
+
});
|
|
3002
|
+
totalRebased += count;
|
|
3003
|
+
}
|
|
3004
|
+
if (totalRebased === 0) {
|
|
3005
|
+
log.info("No stacked PRs needed rebasing");
|
|
3006
|
+
} else {
|
|
3007
|
+
log.success(`Rebased ${totalRebased} stacked PR(s)`);
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
2672
3010
|
program.command("validate").description("Validate config without running anything").option("-c, --config <path>", "Config file path").action(async (opts) => {
|
|
2673
3011
|
try {
|
|
2674
3012
|
const config = loadConfig(opts.config);
|
|
@@ -2707,7 +3045,7 @@ program.command("history").description("Show run history").option("-n, --limit <
|
|
|
2707
3045
|
}
|
|
2708
3046
|
});
|
|
2709
3047
|
function checkClaudeCredentials() {
|
|
2710
|
-
return existsSync7(
|
|
3048
|
+
return existsSync7(join9(homedir6(), ".claude", ".credentials.json"));
|
|
2711
3049
|
}
|
|
2712
3050
|
async function validateGitHubToken(token) {
|
|
2713
3051
|
try {
|
|
@@ -2810,7 +3148,7 @@ program.command("init").description("Set up autoclawd interactively").action(asy
|
|
|
2810
3148
|
}
|
|
2811
3149
|
let ghToken = "";
|
|
2812
3150
|
try {
|
|
2813
|
-
ghToken =
|
|
3151
|
+
ghToken = execSync5("gh auth token", { encoding: "utf-8" }).trim();
|
|
2814
3152
|
log.success(`GitHub token detected from gh CLI`);
|
|
2815
3153
|
} catch {
|
|
2816
3154
|
ghToken = await ask("GitHub personal access token");
|
|
@@ -2861,7 +3199,7 @@ agent:
|
|
|
2861
3199
|
|
|
2862
3200
|
maxConcurrent: 1
|
|
2863
3201
|
`;
|
|
2864
|
-
|
|
3202
|
+
writeFileSync3(CONFIG_FILE, config, { mode: 384 });
|
|
2865
3203
|
log.success(`Config saved to ${CONFIG_FILE}`);
|
|
2866
3204
|
console.log(`
|
|
2867
3205
|
Setup complete! Next steps:
|
|
@@ -2872,10 +3210,12 @@ maxConcurrent: 1
|
|
|
2872
3210
|
2. Tag tickets with a repo: label
|
|
2873
3211
|
|
|
2874
3212
|
3. Start autoclawd:
|
|
2875
|
-
$ autoclawd
|
|
3213
|
+
$ autoclawd watch # polling mode (recommended)
|
|
3214
|
+
$ autoclawd serve # webhook mode
|
|
2876
3215
|
|
|
2877
|
-
|
|
2878
|
-
|
|
3216
|
+
For webhook mode, configure the webhook URL manually in
|
|
3217
|
+
Linear \u2192 Settings \u2192 API \u2192 Webhooks. autoclawd serve will
|
|
3218
|
+
print the tunnel URL to use.
|
|
2879
3219
|
`);
|
|
2880
3220
|
});
|
|
2881
3221
|
program.parseAsync().catch((err) => {
|