@pruddiman/dispatch 1.4.0 → 1.4.2
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/cli.js +720 -579
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -756,11 +756,11 @@ var init_codex = __esm({
|
|
|
756
756
|
});
|
|
757
757
|
|
|
758
758
|
// src/providers/detect.ts
|
|
759
|
-
import { execFile as
|
|
760
|
-
import { promisify as
|
|
759
|
+
import { execFile as execFile3 } from "child_process";
|
|
760
|
+
import { promisify as promisify3 } from "util";
|
|
761
761
|
async function checkProviderInstalled(name) {
|
|
762
762
|
try {
|
|
763
|
-
await
|
|
763
|
+
await exec3(PROVIDER_BINARIES[name], ["--version"], {
|
|
764
764
|
shell: process.platform === "win32",
|
|
765
765
|
timeout: DETECTION_TIMEOUT_MS
|
|
766
766
|
});
|
|
@@ -769,11 +769,11 @@ async function checkProviderInstalled(name) {
|
|
|
769
769
|
return false;
|
|
770
770
|
}
|
|
771
771
|
}
|
|
772
|
-
var
|
|
772
|
+
var exec3, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
|
|
773
773
|
var init_detect = __esm({
|
|
774
774
|
"src/providers/detect.ts"() {
|
|
775
775
|
"use strict";
|
|
776
|
-
|
|
776
|
+
exec3 = promisify3(execFile3);
|
|
777
777
|
DETECTION_TIMEOUT_MS = 5e3;
|
|
778
778
|
PROVIDER_BINARIES = {
|
|
779
779
|
opencode: "opencode",
|
|
@@ -828,6 +828,27 @@ var init_providers = __esm({
|
|
|
828
828
|
}
|
|
829
829
|
});
|
|
830
830
|
|
|
831
|
+
// src/helpers/cleanup.ts
|
|
832
|
+
function registerCleanup(fn) {
|
|
833
|
+
cleanups.push(fn);
|
|
834
|
+
}
|
|
835
|
+
async function runCleanup() {
|
|
836
|
+
const fns = cleanups.splice(0);
|
|
837
|
+
for (const fn of fns) {
|
|
838
|
+
try {
|
|
839
|
+
await fn();
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
var cleanups;
|
|
845
|
+
var init_cleanup = __esm({
|
|
846
|
+
"src/helpers/cleanup.ts"() {
|
|
847
|
+
"use strict";
|
|
848
|
+
cleanups = [];
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
831
852
|
// src/helpers/environment.ts
|
|
832
853
|
function getEnvironmentInfo() {
|
|
833
854
|
const platform = process.platform;
|
|
@@ -857,27 +878,6 @@ var init_environment = __esm({
|
|
|
857
878
|
}
|
|
858
879
|
});
|
|
859
880
|
|
|
860
|
-
// src/helpers/cleanup.ts
|
|
861
|
-
function registerCleanup(fn) {
|
|
862
|
-
cleanups.push(fn);
|
|
863
|
-
}
|
|
864
|
-
async function runCleanup() {
|
|
865
|
-
const fns = cleanups.splice(0);
|
|
866
|
-
for (const fn of fns) {
|
|
867
|
-
try {
|
|
868
|
-
await fn();
|
|
869
|
-
} catch {
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
var cleanups;
|
|
874
|
-
var init_cleanup = __esm({
|
|
875
|
-
"src/helpers/cleanup.ts"() {
|
|
876
|
-
"use strict";
|
|
877
|
-
cleanups = [];
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
|
|
881
881
|
// src/orchestrator/fix-tests-pipeline.ts
|
|
882
882
|
var fix_tests_pipeline_exports = {};
|
|
883
883
|
__export(fix_tests_pipeline_exports, {
|
|
@@ -1048,8 +1048,8 @@ import { Command, Option, CommanderError } from "commander";
|
|
|
1048
1048
|
import { cpus, freemem } from "os";
|
|
1049
1049
|
|
|
1050
1050
|
// src/datasources/index.ts
|
|
1051
|
-
import { execFile as
|
|
1052
|
-
import { promisify as
|
|
1051
|
+
import { execFile as execFile5 } from "child_process";
|
|
1052
|
+
import { promisify as promisify5 } from "util";
|
|
1053
1053
|
|
|
1054
1054
|
// src/datasources/github.ts
|
|
1055
1055
|
import { execFile } from "child_process";
|
|
@@ -1442,7 +1442,6 @@ var datasource2 = {
|
|
|
1442
1442
|
"json"
|
|
1443
1443
|
];
|
|
1444
1444
|
if (opts.org) batchArgs.push("--org", opts.org);
|
|
1445
|
-
if (opts.project) batchArgs.push("--project", opts.project);
|
|
1446
1445
|
const { stdout: batchStdout } = await exec2("az", batchArgs, {
|
|
1447
1446
|
cwd: opts.cwd || process.cwd(),
|
|
1448
1447
|
shell: process.platform === "win32"
|
|
@@ -1487,9 +1486,6 @@ var datasource2 = {
|
|
|
1487
1486
|
if (opts.org) {
|
|
1488
1487
|
args.push("--org", opts.org);
|
|
1489
1488
|
}
|
|
1490
|
-
if (opts.project) {
|
|
1491
|
-
args.push("--project", opts.project);
|
|
1492
|
-
}
|
|
1493
1489
|
const { stdout } = await exec2("az", args, {
|
|
1494
1490
|
cwd: opts.cwd || process.cwd(),
|
|
1495
1491
|
shell: process.platform === "win32"
|
|
@@ -1516,7 +1512,6 @@ var datasource2 = {
|
|
|
1516
1512
|
body
|
|
1517
1513
|
];
|
|
1518
1514
|
if (opts.org) args.push("--org", opts.org);
|
|
1519
|
-
if (opts.project) args.push("--project", opts.project);
|
|
1520
1515
|
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1521
1516
|
},
|
|
1522
1517
|
async close(issueId, opts = {}) {
|
|
@@ -1532,7 +1527,6 @@ var datasource2 = {
|
|
|
1532
1527
|
"json"
|
|
1533
1528
|
];
|
|
1534
1529
|
if (opts.org) showArgs.push("--org", opts.org);
|
|
1535
|
-
if (opts.project) showArgs.push("--project", opts.project);
|
|
1536
1530
|
const { stdout } = await exec2("az", showArgs, {
|
|
1537
1531
|
cwd: opts.cwd || process.cwd(),
|
|
1538
1532
|
shell: process.platform === "win32"
|
|
@@ -1555,7 +1549,6 @@ var datasource2 = {
|
|
|
1555
1549
|
state
|
|
1556
1550
|
];
|
|
1557
1551
|
if (opts.org) args.push("--org", opts.org);
|
|
1558
|
-
if (opts.project) args.push("--project", opts.project);
|
|
1559
1552
|
await exec2("az", args, { cwd: opts.cwd || process.cwd(), shell: process.platform === "win32" });
|
|
1560
1553
|
},
|
|
1561
1554
|
async create(title, body, opts = {}) {
|
|
@@ -1754,9 +1747,6 @@ async function fetchComments(workItemId, opts) {
|
|
|
1754
1747
|
if (opts.org) {
|
|
1755
1748
|
args.push("--org", opts.org);
|
|
1756
1749
|
}
|
|
1757
|
-
if (opts.project) {
|
|
1758
|
-
args.push("--project", opts.project);
|
|
1759
|
-
}
|
|
1760
1750
|
const { stdout } = await exec2("az", args, {
|
|
1761
1751
|
cwd: opts.cwd || process.cwd(),
|
|
1762
1752
|
shell: process.platform === "win32"
|
|
@@ -1777,30 +1767,224 @@ async function fetchComments(workItemId, opts) {
|
|
|
1777
1767
|
}
|
|
1778
1768
|
|
|
1779
1769
|
// src/datasources/md.ts
|
|
1780
|
-
import { execFile as
|
|
1781
|
-
import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
|
|
1782
|
-
import { basename, dirname as
|
|
1783
|
-
import { promisify as
|
|
1770
|
+
import { execFile as execFile4 } from "child_process";
|
|
1771
|
+
import { readFile as readFile2, writeFile as writeFile2, readdir, mkdir as mkdir2, rename } from "fs/promises";
|
|
1772
|
+
import { basename, dirname as dirname3, isAbsolute, join as join3, parse as parsePath, resolve } from "path";
|
|
1773
|
+
import { promisify as promisify4 } from "util";
|
|
1784
1774
|
import { glob } from "glob";
|
|
1785
1775
|
|
|
1786
|
-
// src/
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1776
|
+
// src/config.ts
|
|
1777
|
+
init_providers();
|
|
1778
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
1779
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
1780
|
+
|
|
1781
|
+
// src/config-prompts.ts
|
|
1782
|
+
init_logger();
|
|
1783
|
+
import { select, confirm, input } from "@inquirer/prompts";
|
|
1784
|
+
import chalk2 from "chalk";
|
|
1785
|
+
init_providers();
|
|
1786
|
+
async function runInteractiveConfigWizard(configDir) {
|
|
1787
|
+
console.log();
|
|
1788
|
+
log.info(chalk2.bold("Dispatch Configuration Wizard"));
|
|
1789
|
+
console.log();
|
|
1790
|
+
const existing = await loadConfig(configDir);
|
|
1791
|
+
const hasExisting = Object.keys(existing).length > 0;
|
|
1792
|
+
if (hasExisting) {
|
|
1793
|
+
log.dim("Current configuration:");
|
|
1794
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
1795
|
+
if (value !== void 0) {
|
|
1796
|
+
log.dim(` ${key} = ${value}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
console.log();
|
|
1800
|
+
const reconfigure = await confirm({
|
|
1801
|
+
message: "Do you want to reconfigure?",
|
|
1802
|
+
default: true
|
|
1803
|
+
});
|
|
1804
|
+
if (!reconfigure) {
|
|
1805
|
+
log.dim("Configuration unchanged.");
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
console.log();
|
|
1809
|
+
}
|
|
1810
|
+
const installStatuses = await Promise.all(
|
|
1811
|
+
PROVIDER_NAMES.map((name) => checkProviderInstalled(name))
|
|
1812
|
+
);
|
|
1813
|
+
const provider = await select({
|
|
1814
|
+
message: "Select a provider:",
|
|
1815
|
+
choices: PROVIDER_NAMES.map((name, i) => ({
|
|
1816
|
+
name: `${installStatuses[i] ? chalk2.green("\u25CF") : chalk2.red("\u25CF")} ${name}`,
|
|
1817
|
+
value: name
|
|
1818
|
+
})),
|
|
1819
|
+
default: existing.provider
|
|
1820
|
+
});
|
|
1821
|
+
let selectedModel = existing.model;
|
|
1822
|
+
try {
|
|
1823
|
+
log.dim("Fetching available models...");
|
|
1824
|
+
const models = await listProviderModels(provider);
|
|
1825
|
+
if (models.length > 0) {
|
|
1826
|
+
const modelChoice = await select({
|
|
1827
|
+
message: "Select a model:",
|
|
1828
|
+
choices: [
|
|
1829
|
+
{ name: "default (provider decides)", value: "" },
|
|
1830
|
+
...models.map((m) => ({ name: m, value: m }))
|
|
1831
|
+
],
|
|
1832
|
+
default: existing.model ?? ""
|
|
1833
|
+
});
|
|
1834
|
+
selectedModel = modelChoice || void 0;
|
|
1835
|
+
} else {
|
|
1836
|
+
log.dim("No models returned by provider \u2014 skipping model selection.");
|
|
1837
|
+
selectedModel = existing.model;
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
log.dim("Could not list models (provider may not be running) \u2014 skipping model selection.");
|
|
1841
|
+
selectedModel = existing.model;
|
|
1842
|
+
}
|
|
1843
|
+
const detectedSource = await detectDatasource(process.cwd());
|
|
1844
|
+
const datasourceDefault = existing.source ?? "auto";
|
|
1845
|
+
if (detectedSource) {
|
|
1846
|
+
log.info(
|
|
1847
|
+
`Detected datasource ${chalk2.cyan(detectedSource)} from git remote`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
const selectedSource = await select({
|
|
1851
|
+
message: "Select a datasource:",
|
|
1852
|
+
choices: [
|
|
1853
|
+
{
|
|
1854
|
+
name: "auto",
|
|
1855
|
+
value: "auto",
|
|
1856
|
+
description: "detect from git remote at runtime"
|
|
1857
|
+
},
|
|
1858
|
+
...DATASOURCE_NAMES.map((name) => ({ name, value: name }))
|
|
1859
|
+
],
|
|
1860
|
+
default: datasourceDefault
|
|
1861
|
+
});
|
|
1862
|
+
const source = selectedSource === "auto" ? void 0 : selectedSource;
|
|
1863
|
+
let org;
|
|
1864
|
+
let project;
|
|
1865
|
+
let workItemType;
|
|
1866
|
+
let iteration;
|
|
1867
|
+
let area;
|
|
1868
|
+
const effectiveSource = source ?? detectedSource;
|
|
1869
|
+
if (effectiveSource === "azdevops") {
|
|
1870
|
+
let defaultOrg = existing.org ?? "";
|
|
1871
|
+
let defaultProject = existing.project ?? "";
|
|
1872
|
+
try {
|
|
1873
|
+
const remoteUrl = await getGitRemoteUrl(process.cwd());
|
|
1874
|
+
if (remoteUrl) {
|
|
1875
|
+
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
1876
|
+
if (parsed) {
|
|
1877
|
+
if (!defaultOrg) defaultOrg = parsed.orgUrl;
|
|
1878
|
+
if (!defaultProject) defaultProject = parsed.project;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
console.log();
|
|
1884
|
+
log.info(chalk2.bold("Azure DevOps settings") + chalk2.dim(" (leave empty to skip):"));
|
|
1885
|
+
const orgInput = await input({
|
|
1886
|
+
message: "Organization URL:",
|
|
1887
|
+
default: defaultOrg || void 0
|
|
1888
|
+
});
|
|
1889
|
+
if (orgInput.trim()) org = orgInput.trim();
|
|
1890
|
+
const projectInput = await input({
|
|
1891
|
+
message: "Project name:",
|
|
1892
|
+
default: defaultProject || void 0
|
|
1893
|
+
});
|
|
1894
|
+
if (projectInput.trim()) project = projectInput.trim();
|
|
1895
|
+
const workItemTypeInput = await input({
|
|
1896
|
+
message: "Work item type (e.g. User Story, Bug):",
|
|
1897
|
+
default: existing.workItemType ?? void 0
|
|
1898
|
+
});
|
|
1899
|
+
if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
|
|
1900
|
+
const iterationInput = await input({
|
|
1901
|
+
message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
|
|
1902
|
+
default: existing.iteration ?? void 0
|
|
1903
|
+
});
|
|
1904
|
+
if (iterationInput.trim()) iteration = iterationInput.trim();
|
|
1905
|
+
const areaInput = await input({
|
|
1906
|
+
message: "Area path (e.g. MyProject\\Team A):",
|
|
1907
|
+
default: existing.area ?? void 0
|
|
1908
|
+
});
|
|
1909
|
+
if (areaInput.trim()) area = areaInput.trim();
|
|
1910
|
+
}
|
|
1911
|
+
const newConfig = {
|
|
1912
|
+
provider,
|
|
1913
|
+
source
|
|
1914
|
+
};
|
|
1915
|
+
if (selectedModel !== void 0) {
|
|
1916
|
+
newConfig.model = selectedModel;
|
|
1917
|
+
}
|
|
1918
|
+
if (org !== void 0) newConfig.org = org;
|
|
1919
|
+
if (project !== void 0) newConfig.project = project;
|
|
1920
|
+
if (workItemType !== void 0) newConfig.workItemType = workItemType;
|
|
1921
|
+
if (iteration !== void 0) newConfig.iteration = iteration;
|
|
1922
|
+
if (area !== void 0) newConfig.area = area;
|
|
1923
|
+
console.log();
|
|
1924
|
+
log.info(chalk2.bold("Configuration summary:"));
|
|
1925
|
+
for (const [key, value] of Object.entries(newConfig)) {
|
|
1926
|
+
if (value !== void 0) {
|
|
1927
|
+
console.log(` ${chalk2.cyan(key)} = ${value}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (selectedSource === "auto") {
|
|
1931
|
+
console.log(
|
|
1932
|
+
` ${chalk2.cyan("source")} = auto (detect from git remote at runtime)`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
console.log();
|
|
1936
|
+
const shouldSave = await confirm({
|
|
1937
|
+
message: "Save this configuration?",
|
|
1938
|
+
default: true
|
|
1939
|
+
});
|
|
1940
|
+
if (shouldSave) {
|
|
1941
|
+
await saveConfig(newConfig, configDir);
|
|
1942
|
+
log.success("Configuration saved.");
|
|
1943
|
+
} else {
|
|
1944
|
+
log.dim("Configuration not saved.");
|
|
1795
1945
|
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/config.ts
|
|
1949
|
+
var CONFIG_BOUNDS = {
|
|
1950
|
+
testTimeout: { min: 1, max: 120 },
|
|
1951
|
+
planTimeout: { min: 1, max: 120 },
|
|
1952
|
+
concurrency: { min: 1, max: 64 }
|
|
1796
1953
|
};
|
|
1954
|
+
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
1955
|
+
function getConfigPath(configDir) {
|
|
1956
|
+
const dir = configDir ?? join2(process.cwd(), ".dispatch");
|
|
1957
|
+
return join2(dir, "config.json");
|
|
1958
|
+
}
|
|
1959
|
+
async function loadConfig(configDir) {
|
|
1960
|
+
const configPath = getConfigPath(configDir);
|
|
1961
|
+
try {
|
|
1962
|
+
const raw = await readFile(configPath, "utf-8");
|
|
1963
|
+
return JSON.parse(raw);
|
|
1964
|
+
} catch {
|
|
1965
|
+
return {};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function saveConfig(config, configDir) {
|
|
1969
|
+
const configPath = getConfigPath(configDir);
|
|
1970
|
+
await mkdir(dirname2(configPath), { recursive: true });
|
|
1971
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1972
|
+
}
|
|
1973
|
+
async function handleConfigCommand(_argv, configDir) {
|
|
1974
|
+
await runInteractiveConfigWizard(configDir);
|
|
1975
|
+
}
|
|
1797
1976
|
|
|
1798
1977
|
// src/datasources/md.ts
|
|
1799
|
-
|
|
1978
|
+
init_logger();
|
|
1979
|
+
var exec4 = promisify4(execFile4);
|
|
1980
|
+
async function git2(args, cwd) {
|
|
1981
|
+
const { stdout } = await exec4("git", args, { cwd, shell: process.platform === "win32" });
|
|
1982
|
+
return stdout;
|
|
1983
|
+
}
|
|
1800
1984
|
var DEFAULT_DIR = ".dispatch/specs";
|
|
1801
1985
|
function resolveDir(opts) {
|
|
1802
1986
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1803
|
-
return
|
|
1987
|
+
return join3(cwd, DEFAULT_DIR);
|
|
1804
1988
|
}
|
|
1805
1989
|
function resolveFilePath(issueId, opts) {
|
|
1806
1990
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
@@ -1809,7 +1993,18 @@ function resolveFilePath(issueId, opts) {
|
|
|
1809
1993
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1810
1994
|
return resolve(cwd, filename);
|
|
1811
1995
|
}
|
|
1812
|
-
return
|
|
1996
|
+
return join3(resolveDir(opts), filename);
|
|
1997
|
+
}
|
|
1998
|
+
async function resolveNumericFilePath(issueId, opts) {
|
|
1999
|
+
if (/^\d+$/.test(issueId)) {
|
|
2000
|
+
const dir = resolveDir(opts);
|
|
2001
|
+
const entries = await readdir(dir);
|
|
2002
|
+
const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
|
|
2003
|
+
if (match) {
|
|
2004
|
+
return join3(dir, match);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return resolveFilePath(issueId, opts);
|
|
1813
2008
|
}
|
|
1814
2009
|
function extractTitle(content, filename) {
|
|
1815
2010
|
const match = content.match(/^#\s+(.+)$/m);
|
|
@@ -1828,13 +2023,14 @@ function extractTitle(content, filename) {
|
|
|
1828
2023
|
return parsePath(filename).name;
|
|
1829
2024
|
}
|
|
1830
2025
|
function toIssueDetails(filename, content, dir) {
|
|
2026
|
+
const idMatch = /^(\d+)-/.exec(filename);
|
|
1831
2027
|
return {
|
|
1832
|
-
number: filename,
|
|
2028
|
+
number: idMatch ? idMatch[1] : filename,
|
|
1833
2029
|
title: extractTitle(content, filename),
|
|
1834
2030
|
body: content,
|
|
1835
2031
|
labels: [],
|
|
1836
2032
|
state: "open",
|
|
1837
|
-
url:
|
|
2033
|
+
url: join3(dir, filename),
|
|
1838
2034
|
comments: [],
|
|
1839
2035
|
acceptanceCriteria: ""
|
|
1840
2036
|
};
|
|
@@ -1842,7 +2038,7 @@ function toIssueDetails(filename, content, dir) {
|
|
|
1842
2038
|
var datasource3 = {
|
|
1843
2039
|
name: "md",
|
|
1844
2040
|
supportsGit() {
|
|
1845
|
-
return
|
|
2041
|
+
return true;
|
|
1846
2042
|
},
|
|
1847
2043
|
async list(opts) {
|
|
1848
2044
|
if (opts?.pattern) {
|
|
@@ -1851,9 +2047,9 @@ var datasource3 = {
|
|
|
1851
2047
|
const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
|
|
1852
2048
|
const results2 = [];
|
|
1853
2049
|
for (const filePath of mdFiles2) {
|
|
1854
|
-
const content = await
|
|
2050
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1855
2051
|
const filename = basename(filePath);
|
|
1856
|
-
const dir2 =
|
|
2052
|
+
const dir2 = dirname3(filePath);
|
|
1857
2053
|
results2.push(toIssueDetails(filename, content, dir2));
|
|
1858
2054
|
}
|
|
1859
2055
|
return results2;
|
|
@@ -1868,44 +2064,81 @@ var datasource3 = {
|
|
|
1868
2064
|
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
1869
2065
|
const results = [];
|
|
1870
2066
|
for (const filename of mdFiles) {
|
|
1871
|
-
const filePath =
|
|
1872
|
-
const content = await
|
|
2067
|
+
const filePath = join3(dir, filename);
|
|
2068
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1873
2069
|
results.push(toIssueDetails(filename, content, dir));
|
|
1874
2070
|
}
|
|
1875
2071
|
return results;
|
|
1876
2072
|
},
|
|
1877
2073
|
async fetch(issueId, opts) {
|
|
2074
|
+
if (/^\d+$/.test(issueId)) {
|
|
2075
|
+
const dir2 = resolveDir(opts);
|
|
2076
|
+
const entries = await readdir(dir2);
|
|
2077
|
+
const match = entries.find((f) => f.startsWith(`${issueId}-`) && f.endsWith(".md"));
|
|
2078
|
+
if (match) {
|
|
2079
|
+
const content2 = await readFile2(join3(dir2, match), "utf-8");
|
|
2080
|
+
return toIssueDetails(match, content2, dir2);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
1878
2083
|
const filePath = resolveFilePath(issueId, opts);
|
|
1879
|
-
const content = await
|
|
2084
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1880
2085
|
const filename = basename(filePath);
|
|
1881
|
-
const dir =
|
|
2086
|
+
const dir = dirname3(filePath);
|
|
1882
2087
|
return toIssueDetails(filename, content, dir);
|
|
1883
2088
|
},
|
|
1884
2089
|
async update(issueId, _title, body, opts) {
|
|
1885
|
-
const filePath =
|
|
1886
|
-
await
|
|
2090
|
+
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
2091
|
+
await writeFile2(filePath, body, "utf-8");
|
|
1887
2092
|
},
|
|
1888
2093
|
async close(issueId, opts) {
|
|
1889
|
-
const filePath =
|
|
2094
|
+
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
1890
2095
|
const filename = basename(filePath);
|
|
1891
|
-
const archiveDir =
|
|
1892
|
-
await
|
|
1893
|
-
await rename(filePath,
|
|
2096
|
+
const archiveDir = join3(dirname3(filePath), "archive");
|
|
2097
|
+
await mkdir2(archiveDir, { recursive: true });
|
|
2098
|
+
await rename(filePath, join3(archiveDir, filename));
|
|
1894
2099
|
},
|
|
1895
2100
|
async create(title, body, opts) {
|
|
2101
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
2102
|
+
const configDir = join3(cwd, ".dispatch");
|
|
2103
|
+
const config = await loadConfig(configDir);
|
|
2104
|
+
const id = config.nextIssueId ?? 1;
|
|
1896
2105
|
const dir = resolveDir(opts);
|
|
1897
|
-
await
|
|
1898
|
-
const filename = `${slugify(title)}.md`;
|
|
1899
|
-
const filePath =
|
|
1900
|
-
await
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2106
|
+
await mkdir2(dir, { recursive: true });
|
|
2107
|
+
const filename = `${id}-${slugify(title)}.md`;
|
|
2108
|
+
const filePath = join3(dir, filename);
|
|
2109
|
+
await writeFile2(filePath, body, "utf-8");
|
|
2110
|
+
config.nextIssueId = id + 1;
|
|
2111
|
+
await saveConfig(config, configDir);
|
|
2112
|
+
return {
|
|
2113
|
+
...toIssueDetails(filename, body, dir),
|
|
2114
|
+
number: String(id)
|
|
2115
|
+
};
|
|
2116
|
+
},
|
|
2117
|
+
async getDefaultBranch(opts) {
|
|
2118
|
+
const PREFIX = "refs/remotes/origin/";
|
|
2119
|
+
try {
|
|
2120
|
+
const ref = await git2(["symbolic-ref", "refs/remotes/origin/HEAD"], opts.cwd);
|
|
2121
|
+
const trimmed = ref.trim();
|
|
2122
|
+
const branch = trimmed.startsWith(PREFIX) ? trimmed.slice(PREFIX.length) : trimmed;
|
|
2123
|
+
if (!isValidBranchName(branch)) {
|
|
2124
|
+
throw new InvalidBranchNameError(branch, "from symbolic-ref output");
|
|
2125
|
+
}
|
|
2126
|
+
return branch;
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
if (err instanceof InvalidBranchNameError) {
|
|
2129
|
+
throw err;
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
await git2(["rev-parse", "--verify", "main"], opts.cwd);
|
|
2133
|
+
return "main";
|
|
2134
|
+
} catch {
|
|
2135
|
+
return "master";
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
1905
2138
|
},
|
|
1906
2139
|
async getUsername(opts) {
|
|
1907
2140
|
try {
|
|
1908
|
-
const { stdout } = await
|
|
2141
|
+
const { stdout } = await exec4("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1909
2142
|
const name = stdout.trim();
|
|
1910
2143
|
if (!name) return "local";
|
|
1911
2144
|
return slugify(name);
|
|
@@ -1917,25 +2150,39 @@ var datasource3 = {
|
|
|
1917
2150
|
const slug = slugify(title, 50);
|
|
1918
2151
|
return `${username}/dispatch/${issueNumber}-${slug}`;
|
|
1919
2152
|
},
|
|
1920
|
-
async createAndSwitchBranch(
|
|
1921
|
-
|
|
2153
|
+
async createAndSwitchBranch(branchName, opts) {
|
|
2154
|
+
try {
|
|
2155
|
+
await git2(["checkout", "-b", branchName], opts.cwd);
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
const message = log.extractMessage(err);
|
|
2158
|
+
if (message.includes("already exists")) {
|
|
2159
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
2160
|
+
} else {
|
|
2161
|
+
throw err;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
1922
2164
|
},
|
|
1923
|
-
async switchBranch(
|
|
1924
|
-
|
|
2165
|
+
async switchBranch(branchName, opts) {
|
|
2166
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
1925
2167
|
},
|
|
1926
2168
|
async pushBranch(_branchName, _opts) {
|
|
1927
|
-
throw new UnsupportedOperationError("pushBranch");
|
|
1928
2169
|
},
|
|
1929
|
-
async commitAllChanges(
|
|
1930
|
-
|
|
2170
|
+
async commitAllChanges(message, opts) {
|
|
2171
|
+
const cwd = opts.cwd;
|
|
2172
|
+
await git2(["add", "-A"], cwd);
|
|
2173
|
+
const status = await git2(["diff", "--cached", "--stat"], cwd);
|
|
2174
|
+
if (!status.trim()) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
await git2(["commit", "-m", message], cwd);
|
|
1931
2178
|
},
|
|
1932
2179
|
async createPullRequest(_branchName, _issueNumber, _title, _body, _opts) {
|
|
1933
|
-
|
|
2180
|
+
return "";
|
|
1934
2181
|
}
|
|
1935
2182
|
};
|
|
1936
2183
|
|
|
1937
2184
|
// src/datasources/index.ts
|
|
1938
|
-
var
|
|
2185
|
+
var exec5 = promisify5(execFile5);
|
|
1939
2186
|
var DATASOURCES = {
|
|
1940
2187
|
github: datasource,
|
|
1941
2188
|
azdevops: datasource2,
|
|
@@ -1953,7 +2200,7 @@ function getDatasource(name) {
|
|
|
1953
2200
|
}
|
|
1954
2201
|
async function getGitRemoteUrl(cwd) {
|
|
1955
2202
|
try {
|
|
1956
|
-
const { stdout } = await
|
|
2203
|
+
const { stdout } = await exec5("git", ["remote", "get-url", "origin"], {
|
|
1957
2204
|
cwd,
|
|
1958
2205
|
shell: process.platform === "win32"
|
|
1959
2206
|
});
|
|
@@ -2120,29 +2367,266 @@ async function resolveSource(issues, issueSource, cwd) {
|
|
|
2120
2367
|
return null;
|
|
2121
2368
|
}
|
|
2122
2369
|
|
|
2370
|
+
// src/orchestrator/datasource-helpers.ts
|
|
2371
|
+
init_logger();
|
|
2372
|
+
import { basename as basename2, join as join4 } from "path";
|
|
2373
|
+
import { mkdtemp, writeFile as writeFile3 } from "fs/promises";
|
|
2374
|
+
import { tmpdir } from "os";
|
|
2375
|
+
import { execFile as execFile6 } from "child_process";
|
|
2376
|
+
import { promisify as promisify6 } from "util";
|
|
2377
|
+
var exec6 = promisify6(execFile6);
|
|
2378
|
+
function parseIssueFilename(filePath) {
|
|
2379
|
+
const filename = basename2(filePath);
|
|
2380
|
+
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
2381
|
+
if (!match) return null;
|
|
2382
|
+
return { issueId: match[1], slug: match[2] };
|
|
2383
|
+
}
|
|
2384
|
+
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
2385
|
+
const ids = issueIds.flatMap(
|
|
2386
|
+
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
2387
|
+
);
|
|
2388
|
+
const items = [];
|
|
2389
|
+
for (const id of ids) {
|
|
2390
|
+
try {
|
|
2391
|
+
const item = await datasource4.fetch(id, fetchOpts);
|
|
2392
|
+
items.push(item);
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
|
|
2395
|
+
log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return items;
|
|
2399
|
+
}
|
|
2400
|
+
async function writeItemsToTempDir(items) {
|
|
2401
|
+
const tempDir = await mkdtemp(join4(tmpdir(), "dispatch-"));
|
|
2402
|
+
const files = [];
|
|
2403
|
+
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
2404
|
+
for (const item of items) {
|
|
2405
|
+
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
2406
|
+
const id = item.number.includes("/") || item.number.includes("\\") ? basename2(item.number, ".md") : item.number;
|
|
2407
|
+
const filename = `${id}-${slug}.md`;
|
|
2408
|
+
const filepath = join4(tempDir, filename);
|
|
2409
|
+
await writeFile3(filepath, item.body, "utf-8");
|
|
2410
|
+
files.push(filepath);
|
|
2411
|
+
issueDetailsByFile.set(filepath, item);
|
|
2412
|
+
}
|
|
2413
|
+
files.sort((a, b) => {
|
|
2414
|
+
const numA = parseInt(basename2(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2415
|
+
const numB = parseInt(basename2(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
2416
|
+
if (numA !== numB) return numA - numB;
|
|
2417
|
+
return a.localeCompare(b);
|
|
2418
|
+
});
|
|
2419
|
+
return { files, issueDetailsByFile };
|
|
2420
|
+
}
|
|
2421
|
+
async function getCommitSummaries(defaultBranch, cwd) {
|
|
2422
|
+
try {
|
|
2423
|
+
const { stdout } = await exec6(
|
|
2424
|
+
"git",
|
|
2425
|
+
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
2426
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2427
|
+
);
|
|
2428
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
2429
|
+
} catch {
|
|
2430
|
+
return [];
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async function getBranchDiff(defaultBranch, cwd) {
|
|
2434
|
+
try {
|
|
2435
|
+
const { stdout } = await exec6(
|
|
2436
|
+
"git",
|
|
2437
|
+
["diff", `${defaultBranch}..HEAD`],
|
|
2438
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
2439
|
+
);
|
|
2440
|
+
return stdout;
|
|
2441
|
+
} catch {
|
|
2442
|
+
return "";
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
2446
|
+
const { stdout } = await exec6(
|
|
2447
|
+
"git",
|
|
2448
|
+
["merge-base", defaultBranch, "HEAD"],
|
|
2449
|
+
{ cwd, shell: process.platform === "win32" }
|
|
2450
|
+
);
|
|
2451
|
+
const mergeBase = stdout.trim();
|
|
2452
|
+
await exec6("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
2453
|
+
await exec6("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
2454
|
+
}
|
|
2455
|
+
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
2456
|
+
const sections = [];
|
|
2457
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2458
|
+
if (commits.length > 0) {
|
|
2459
|
+
sections.push("## Summary\n");
|
|
2460
|
+
for (const commit of commits) {
|
|
2461
|
+
sections.push(`- ${commit}`);
|
|
2462
|
+
}
|
|
2463
|
+
sections.push("");
|
|
2464
|
+
}
|
|
2465
|
+
const taskResults = new Map(
|
|
2466
|
+
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
2467
|
+
);
|
|
2468
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2469
|
+
const failedTasks = tasks.filter((t) => {
|
|
2470
|
+
const r = taskResults.get(t);
|
|
2471
|
+
return r && !r.success;
|
|
2472
|
+
});
|
|
2473
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2474
|
+
sections.push("## Tasks\n");
|
|
2475
|
+
for (const task of completedTasks) {
|
|
2476
|
+
sections.push(`- [x] ${task.text}`);
|
|
2477
|
+
}
|
|
2478
|
+
for (const task of failedTasks) {
|
|
2479
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2480
|
+
}
|
|
2481
|
+
sections.push("");
|
|
2482
|
+
}
|
|
2483
|
+
if (details.labels.length > 0) {
|
|
2484
|
+
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
2485
|
+
`);
|
|
2486
|
+
}
|
|
2487
|
+
if (datasourceName === "github") {
|
|
2488
|
+
sections.push(`Closes #${details.number}`);
|
|
2489
|
+
} else if (datasourceName === "azdevops") {
|
|
2490
|
+
sections.push(`Resolves AB#${details.number}`);
|
|
2491
|
+
}
|
|
2492
|
+
return sections.join("\n");
|
|
2493
|
+
}
|
|
2494
|
+
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
2495
|
+
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
2496
|
+
if (commits.length === 0) {
|
|
2497
|
+
return issueTitle;
|
|
2498
|
+
}
|
|
2499
|
+
if (commits.length === 1) {
|
|
2500
|
+
return commits[0];
|
|
2501
|
+
}
|
|
2502
|
+
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
2503
|
+
}
|
|
2504
|
+
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
2505
|
+
if (issues.length === 1) {
|
|
2506
|
+
return issues[0].title;
|
|
2507
|
+
}
|
|
2508
|
+
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
2509
|
+
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
2510
|
+
}
|
|
2511
|
+
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
2512
|
+
const sections = [];
|
|
2513
|
+
sections.push("## Issues\n");
|
|
2514
|
+
for (const issue of issues) {
|
|
2515
|
+
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
2516
|
+
}
|
|
2517
|
+
sections.push("");
|
|
2518
|
+
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
2519
|
+
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
2520
|
+
const failedTasks = tasks.filter((t) => {
|
|
2521
|
+
const r = taskResults.get(t);
|
|
2522
|
+
return r && !r.success;
|
|
2523
|
+
});
|
|
2524
|
+
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
2525
|
+
sections.push("## Tasks\n");
|
|
2526
|
+
for (const task of completedTasks) {
|
|
2527
|
+
sections.push(`- [x] ${task.text}`);
|
|
2528
|
+
}
|
|
2529
|
+
for (const task of failedTasks) {
|
|
2530
|
+
sections.push(`- [ ] ${task.text}`);
|
|
2531
|
+
}
|
|
2532
|
+
sections.push("");
|
|
2533
|
+
}
|
|
2534
|
+
for (const issue of issues) {
|
|
2535
|
+
if (datasourceName === "github") {
|
|
2536
|
+
sections.push(`Closes #${issue.number}`);
|
|
2537
|
+
} else if (datasourceName === "azdevops") {
|
|
2538
|
+
sections.push(`Resolves AB#${issue.number}`);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
return sections.join("\n");
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/helpers/worktree.ts
|
|
2545
|
+
import { join as join5, basename as basename3 } from "path";
|
|
2546
|
+
import { execFile as execFile7 } from "child_process";
|
|
2547
|
+
import { promisify as promisify7 } from "util";
|
|
2548
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2549
|
+
init_logger();
|
|
2550
|
+
var exec7 = promisify7(execFile7);
|
|
2551
|
+
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
2552
|
+
async function git3(args, cwd) {
|
|
2553
|
+
const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
|
|
2554
|
+
return stdout;
|
|
2555
|
+
}
|
|
2556
|
+
function worktreeName(issueFilename) {
|
|
2557
|
+
const base = basename3(issueFilename);
|
|
2558
|
+
const withoutExt = base.replace(/\.md$/i, "");
|
|
2559
|
+
const match = withoutExt.match(/^(\d+)/);
|
|
2560
|
+
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
2561
|
+
}
|
|
2562
|
+
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
2563
|
+
const name = worktreeName(issueFilename);
|
|
2564
|
+
const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
|
|
2565
|
+
try {
|
|
2566
|
+
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
2567
|
+
if (startPoint) args.push(startPoint);
|
|
2568
|
+
await git3(args, repoRoot);
|
|
2569
|
+
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
const message = log.extractMessage(err);
|
|
2572
|
+
if (message.includes("already exists")) {
|
|
2573
|
+
await git3(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
2574
|
+
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
2575
|
+
} else {
|
|
2576
|
+
throw err;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return worktreePath;
|
|
2580
|
+
}
|
|
2581
|
+
async function removeWorktree(repoRoot, issueFilename) {
|
|
2582
|
+
const name = worktreeName(issueFilename);
|
|
2583
|
+
const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
|
|
2584
|
+
try {
|
|
2585
|
+
await git3(["worktree", "remove", worktreePath], repoRoot);
|
|
2586
|
+
} catch {
|
|
2587
|
+
try {
|
|
2588
|
+
await git3(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
try {
|
|
2595
|
+
await git3(["worktree", "prune"], repoRoot);
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
function generateFeatureBranchName() {
|
|
2601
|
+
const uuid = randomUUID3();
|
|
2602
|
+
const octet = uuid.split("-")[0];
|
|
2603
|
+
return `dispatch/feature-${octet}`;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2123
2606
|
// src/orchestrator/runner.ts
|
|
2607
|
+
init_cleanup();
|
|
2124
2608
|
init_logger();
|
|
2125
2609
|
|
|
2126
2610
|
// src/helpers/confirm-large-batch.ts
|
|
2127
2611
|
init_logger();
|
|
2128
|
-
import { input } from "@inquirer/prompts";
|
|
2129
|
-
import
|
|
2612
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
2613
|
+
import chalk3 from "chalk";
|
|
2130
2614
|
var LARGE_BATCH_THRESHOLD = 100;
|
|
2131
2615
|
async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
2132
2616
|
if (count <= threshold) return true;
|
|
2133
2617
|
log.warn(
|
|
2134
|
-
`This operation will process ${
|
|
2618
|
+
`This operation will process ${chalk3.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
|
|
2135
2619
|
);
|
|
2136
|
-
const answer = await
|
|
2137
|
-
message: `Type ${
|
|
2620
|
+
const answer = await input2({
|
|
2621
|
+
message: `Type ${chalk3.bold('"yes"')} to proceed:`
|
|
2138
2622
|
});
|
|
2139
2623
|
return answer.trim().toLowerCase() === "yes";
|
|
2140
2624
|
}
|
|
2141
2625
|
|
|
2142
2626
|
// src/helpers/prereqs.ts
|
|
2143
|
-
import { execFile as
|
|
2144
|
-
import { promisify as
|
|
2145
|
-
var
|
|
2627
|
+
import { execFile as execFile8 } from "child_process";
|
|
2628
|
+
import { promisify as promisify8 } from "util";
|
|
2629
|
+
var exec8 = promisify8(execFile8);
|
|
2146
2630
|
var MIN_NODE_VERSION = "20.12.0";
|
|
2147
2631
|
function parseSemver(version) {
|
|
2148
2632
|
const [major, minor, patch] = version.split(".").map(Number);
|
|
@@ -2158,7 +2642,7 @@ function semverGte(current, minimum) {
|
|
|
2158
2642
|
async function checkPrereqs(context) {
|
|
2159
2643
|
const failures = [];
|
|
2160
2644
|
try {
|
|
2161
|
-
await
|
|
2645
|
+
await exec8("git", ["--version"], { shell: process.platform === "win32" });
|
|
2162
2646
|
} catch {
|
|
2163
2647
|
failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
|
|
2164
2648
|
}
|
|
@@ -2170,7 +2654,7 @@ async function checkPrereqs(context) {
|
|
|
2170
2654
|
}
|
|
2171
2655
|
if (context?.datasource === "github") {
|
|
2172
2656
|
try {
|
|
2173
|
-
await
|
|
2657
|
+
await exec8("gh", ["--version"], { shell: process.platform === "win32" });
|
|
2174
2658
|
} catch {
|
|
2175
2659
|
failures.push(
|
|
2176
2660
|
"gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
|
|
@@ -2179,7 +2663,7 @@ async function checkPrereqs(context) {
|
|
|
2179
2663
|
}
|
|
2180
2664
|
if (context?.datasource === "azdevops") {
|
|
2181
2665
|
try {
|
|
2182
|
-
await
|
|
2666
|
+
await exec8("az", ["--version"], { shell: process.platform === "win32" });
|
|
2183
2667
|
} catch {
|
|
2184
2668
|
failures.push(
|
|
2185
2669
|
"az (Azure CLI) is required for the azdevops datasource but was not found on PATH. Install it from https://learn.microsoft.com/en-us/cli/azure/"
|
|
@@ -2191,13 +2675,13 @@ async function checkPrereqs(context) {
|
|
|
2191
2675
|
|
|
2192
2676
|
// src/helpers/gitignore.ts
|
|
2193
2677
|
init_logger();
|
|
2194
|
-
import { readFile as
|
|
2195
|
-
import { join as
|
|
2678
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
2679
|
+
import { join as join6 } from "path";
|
|
2196
2680
|
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
2197
|
-
const gitignorePath =
|
|
2681
|
+
const gitignorePath = join6(repoRoot, ".gitignore");
|
|
2198
2682
|
let contents = "";
|
|
2199
2683
|
try {
|
|
2200
|
-
contents = await
|
|
2684
|
+
contents = await readFile3(gitignorePath, "utf8");
|
|
2201
2685
|
} catch (err) {
|
|
2202
2686
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
2203
2687
|
} else {
|
|
@@ -2213,7 +2697,7 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2213
2697
|
}
|
|
2214
2698
|
try {
|
|
2215
2699
|
const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
2216
|
-
await
|
|
2700
|
+
await writeFile4(gitignorePath, `${contents}${separator}${entry}
|
|
2217
2701
|
`, "utf8");
|
|
2218
2702
|
log.debug(`Added '${entry}' to .gitignore`);
|
|
2219
2703
|
} catch (err) {
|
|
@@ -2223,212 +2707,9 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2223
2707
|
|
|
2224
2708
|
// src/orchestrator/cli-config.ts
|
|
2225
2709
|
init_logger();
|
|
2226
|
-
import { join as
|
|
2710
|
+
import { join as join7 } from "path";
|
|
2227
2711
|
import { access } from "fs/promises";
|
|
2228
|
-
import { constants } from "fs";
|
|
2229
|
-
|
|
2230
|
-
// src/config.ts
|
|
2231
|
-
init_providers();
|
|
2232
|
-
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
2233
|
-
import { join as join4, dirname as dirname3 } from "path";
|
|
2234
|
-
|
|
2235
|
-
// src/config-prompts.ts
|
|
2236
|
-
init_logger();
|
|
2237
|
-
import { select, confirm, input as input2 } from "@inquirer/prompts";
|
|
2238
|
-
import chalk3 from "chalk";
|
|
2239
|
-
init_providers();
|
|
2240
|
-
async function runInteractiveConfigWizard(configDir) {
|
|
2241
|
-
console.log();
|
|
2242
|
-
log.info(chalk3.bold("Dispatch Configuration Wizard"));
|
|
2243
|
-
console.log();
|
|
2244
|
-
const existing = await loadConfig(configDir);
|
|
2245
|
-
const hasExisting = Object.keys(existing).length > 0;
|
|
2246
|
-
if (hasExisting) {
|
|
2247
|
-
log.dim("Current configuration:");
|
|
2248
|
-
for (const [key, value] of Object.entries(existing)) {
|
|
2249
|
-
if (value !== void 0) {
|
|
2250
|
-
log.dim(` ${key} = ${value}`);
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
console.log();
|
|
2254
|
-
const reconfigure = await confirm({
|
|
2255
|
-
message: "Do you want to reconfigure?",
|
|
2256
|
-
default: true
|
|
2257
|
-
});
|
|
2258
|
-
if (!reconfigure) {
|
|
2259
|
-
log.dim("Configuration unchanged.");
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
console.log();
|
|
2263
|
-
}
|
|
2264
|
-
const installStatuses = await Promise.all(
|
|
2265
|
-
PROVIDER_NAMES.map((name) => checkProviderInstalled(name))
|
|
2266
|
-
);
|
|
2267
|
-
const provider = await select({
|
|
2268
|
-
message: "Select a provider:",
|
|
2269
|
-
choices: PROVIDER_NAMES.map((name, i) => ({
|
|
2270
|
-
name: `${installStatuses[i] ? chalk3.green("\u25CF") : chalk3.red("\u25CF")} ${name}`,
|
|
2271
|
-
value: name
|
|
2272
|
-
})),
|
|
2273
|
-
default: existing.provider
|
|
2274
|
-
});
|
|
2275
|
-
let selectedModel = existing.model;
|
|
2276
|
-
try {
|
|
2277
|
-
log.dim("Fetching available models...");
|
|
2278
|
-
const models = await listProviderModels(provider);
|
|
2279
|
-
if (models.length > 0) {
|
|
2280
|
-
const modelChoice = await select({
|
|
2281
|
-
message: "Select a model:",
|
|
2282
|
-
choices: [
|
|
2283
|
-
{ name: "default (provider decides)", value: "" },
|
|
2284
|
-
...models.map((m) => ({ name: m, value: m }))
|
|
2285
|
-
],
|
|
2286
|
-
default: existing.model ?? ""
|
|
2287
|
-
});
|
|
2288
|
-
selectedModel = modelChoice || void 0;
|
|
2289
|
-
} else {
|
|
2290
|
-
log.dim("No models returned by provider \u2014 skipping model selection.");
|
|
2291
|
-
selectedModel = existing.model;
|
|
2292
|
-
}
|
|
2293
|
-
} catch {
|
|
2294
|
-
log.dim("Could not list models (provider may not be running) \u2014 skipping model selection.");
|
|
2295
|
-
selectedModel = existing.model;
|
|
2296
|
-
}
|
|
2297
|
-
const detectedSource = await detectDatasource(process.cwd());
|
|
2298
|
-
const datasourceDefault = existing.source ?? "auto";
|
|
2299
|
-
if (detectedSource) {
|
|
2300
|
-
log.info(
|
|
2301
|
-
`Detected datasource ${chalk3.cyan(detectedSource)} from git remote`
|
|
2302
|
-
);
|
|
2303
|
-
}
|
|
2304
|
-
const selectedSource = await select({
|
|
2305
|
-
message: "Select a datasource:",
|
|
2306
|
-
choices: [
|
|
2307
|
-
{
|
|
2308
|
-
name: "auto",
|
|
2309
|
-
value: "auto",
|
|
2310
|
-
description: "detect from git remote at runtime"
|
|
2311
|
-
},
|
|
2312
|
-
...DATASOURCE_NAMES.map((name) => ({ name, value: name }))
|
|
2313
|
-
],
|
|
2314
|
-
default: datasourceDefault
|
|
2315
|
-
});
|
|
2316
|
-
const source = selectedSource === "auto" ? void 0 : selectedSource;
|
|
2317
|
-
let org;
|
|
2318
|
-
let project;
|
|
2319
|
-
let workItemType;
|
|
2320
|
-
let iteration;
|
|
2321
|
-
let area;
|
|
2322
|
-
const effectiveSource = source ?? detectedSource;
|
|
2323
|
-
if (effectiveSource === "azdevops") {
|
|
2324
|
-
let defaultOrg = existing.org ?? "";
|
|
2325
|
-
let defaultProject = existing.project ?? "";
|
|
2326
|
-
try {
|
|
2327
|
-
const remoteUrl = await getGitRemoteUrl(process.cwd());
|
|
2328
|
-
if (remoteUrl) {
|
|
2329
|
-
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
2330
|
-
if (parsed) {
|
|
2331
|
-
if (!defaultOrg) defaultOrg = parsed.orgUrl;
|
|
2332
|
-
if (!defaultProject) defaultProject = parsed.project;
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
} catch {
|
|
2336
|
-
}
|
|
2337
|
-
console.log();
|
|
2338
|
-
log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
|
|
2339
|
-
const orgInput = await input2({
|
|
2340
|
-
message: "Organization URL:",
|
|
2341
|
-
default: defaultOrg || void 0
|
|
2342
|
-
});
|
|
2343
|
-
if (orgInput.trim()) org = orgInput.trim();
|
|
2344
|
-
const projectInput = await input2({
|
|
2345
|
-
message: "Project name:",
|
|
2346
|
-
default: defaultProject || void 0
|
|
2347
|
-
});
|
|
2348
|
-
if (projectInput.trim()) project = projectInput.trim();
|
|
2349
|
-
const workItemTypeInput = await input2({
|
|
2350
|
-
message: "Work item type (e.g. User Story, Bug):",
|
|
2351
|
-
default: existing.workItemType ?? void 0
|
|
2352
|
-
});
|
|
2353
|
-
if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
|
|
2354
|
-
const iterationInput = await input2({
|
|
2355
|
-
message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
|
|
2356
|
-
default: existing.iteration ?? void 0
|
|
2357
|
-
});
|
|
2358
|
-
if (iterationInput.trim()) iteration = iterationInput.trim();
|
|
2359
|
-
const areaInput = await input2({
|
|
2360
|
-
message: "Area path (e.g. MyProject\\Team A):",
|
|
2361
|
-
default: existing.area ?? void 0
|
|
2362
|
-
});
|
|
2363
|
-
if (areaInput.trim()) area = areaInput.trim();
|
|
2364
|
-
}
|
|
2365
|
-
const newConfig = {
|
|
2366
|
-
provider,
|
|
2367
|
-
source
|
|
2368
|
-
};
|
|
2369
|
-
if (selectedModel !== void 0) {
|
|
2370
|
-
newConfig.model = selectedModel;
|
|
2371
|
-
}
|
|
2372
|
-
if (org !== void 0) newConfig.org = org;
|
|
2373
|
-
if (project !== void 0) newConfig.project = project;
|
|
2374
|
-
if (workItemType !== void 0) newConfig.workItemType = workItemType;
|
|
2375
|
-
if (iteration !== void 0) newConfig.iteration = iteration;
|
|
2376
|
-
if (area !== void 0) newConfig.area = area;
|
|
2377
|
-
console.log();
|
|
2378
|
-
log.info(chalk3.bold("Configuration summary:"));
|
|
2379
|
-
for (const [key, value] of Object.entries(newConfig)) {
|
|
2380
|
-
if (value !== void 0) {
|
|
2381
|
-
console.log(` ${chalk3.cyan(key)} = ${value}`);
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
if (selectedSource === "auto") {
|
|
2385
|
-
console.log(
|
|
2386
|
-
` ${chalk3.cyan("source")} = auto (detect from git remote at runtime)`
|
|
2387
|
-
);
|
|
2388
|
-
}
|
|
2389
|
-
console.log();
|
|
2390
|
-
const shouldSave = await confirm({
|
|
2391
|
-
message: "Save this configuration?",
|
|
2392
|
-
default: true
|
|
2393
|
-
});
|
|
2394
|
-
if (shouldSave) {
|
|
2395
|
-
await saveConfig(newConfig, configDir);
|
|
2396
|
-
log.success("Configuration saved.");
|
|
2397
|
-
} else {
|
|
2398
|
-
log.dim("Configuration not saved.");
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
// src/config.ts
|
|
2403
|
-
var CONFIG_BOUNDS = {
|
|
2404
|
-
testTimeout: { min: 1, max: 120 },
|
|
2405
|
-
planTimeout: { min: 1, max: 120 },
|
|
2406
|
-
concurrency: { min: 1, max: 64 }
|
|
2407
|
-
};
|
|
2408
|
-
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
2409
|
-
function getConfigPath(configDir) {
|
|
2410
|
-
const dir = configDir ?? join4(process.cwd(), ".dispatch");
|
|
2411
|
-
return join4(dir, "config.json");
|
|
2412
|
-
}
|
|
2413
|
-
async function loadConfig(configDir) {
|
|
2414
|
-
const configPath = getConfigPath(configDir);
|
|
2415
|
-
try {
|
|
2416
|
-
const raw = await readFile3(configPath, "utf-8");
|
|
2417
|
-
return JSON.parse(raw);
|
|
2418
|
-
} catch {
|
|
2419
|
-
return {};
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
async function saveConfig(config, configDir) {
|
|
2423
|
-
const configPath = getConfigPath(configDir);
|
|
2424
|
-
await mkdir2(dirname3(configPath), { recursive: true });
|
|
2425
|
-
await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2426
|
-
}
|
|
2427
|
-
async function handleConfigCommand(_argv, configDir) {
|
|
2428
|
-
await runInteractiveConfigWizard(configDir);
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
// src/orchestrator/cli-config.ts
|
|
2712
|
+
import { constants } from "fs";
|
|
2432
2713
|
var CONFIG_TO_CLI = {
|
|
2433
2714
|
provider: "provider",
|
|
2434
2715
|
model: "model",
|
|
@@ -2447,7 +2728,7 @@ function setCliField(target, key, value) {
|
|
|
2447
2728
|
}
|
|
2448
2729
|
async function resolveCliConfig(args) {
|
|
2449
2730
|
const { explicitFlags } = args;
|
|
2450
|
-
const configDir =
|
|
2731
|
+
const configDir = join7(args.cwd, ".dispatch");
|
|
2451
2732
|
const config = await loadConfig(configDir);
|
|
2452
2733
|
const merged = { ...args };
|
|
2453
2734
|
for (const configKey of CONFIG_KEYS) {
|
|
@@ -2475,7 +2756,7 @@ async function resolveCliConfig(args) {
|
|
|
2475
2756
|
}
|
|
2476
2757
|
}
|
|
2477
2758
|
const sourceConfigured = explicitFlags.has("issueSource") || config.source !== void 0;
|
|
2478
|
-
const needsSource = !merged.fixTests && !merged.spec && !merged.respec;
|
|
2759
|
+
const needsSource = !(merged.fixTests && merged.issueIds.length === 0) && !merged.spec && !merged.respec;
|
|
2479
2760
|
if (needsSource && !sourceConfigured) {
|
|
2480
2761
|
const detected = await detectDatasource(merged.cwd);
|
|
2481
2762
|
if (detected) {
|
|
@@ -2494,15 +2775,15 @@ async function resolveCliConfig(args) {
|
|
|
2494
2775
|
}
|
|
2495
2776
|
|
|
2496
2777
|
// src/orchestrator/spec-pipeline.ts
|
|
2497
|
-
import { join as
|
|
2778
|
+
import { join as join9 } from "path";
|
|
2498
2779
|
import { mkdir as mkdir4, readFile as readFile5, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
2499
2780
|
import { glob as glob2 } from "glob";
|
|
2500
2781
|
init_providers();
|
|
2501
2782
|
|
|
2502
2783
|
// src/agents/spec.ts
|
|
2503
|
-
import { mkdir as mkdir3, readFile as readFile4, writeFile as
|
|
2504
|
-
import { join as
|
|
2505
|
-
import { randomUUID as
|
|
2784
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile5, unlink } from "fs/promises";
|
|
2785
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
2786
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2506
2787
|
init_logger();
|
|
2507
2788
|
init_file_logger();
|
|
2508
2789
|
init_environment();
|
|
@@ -2527,10 +2808,10 @@ async function boot5(opts) {
|
|
|
2527
2808
|
durationMs: Date.now() - startTime
|
|
2528
2809
|
};
|
|
2529
2810
|
}
|
|
2530
|
-
const tmpDir =
|
|
2811
|
+
const tmpDir = join8(resolvedCwd, ".dispatch", "tmp");
|
|
2531
2812
|
await mkdir3(tmpDir, { recursive: true });
|
|
2532
|
-
const tmpFilename = `spec-${
|
|
2533
|
-
const tmpPath =
|
|
2813
|
+
const tmpFilename = `spec-${randomUUID4()}.md`;
|
|
2814
|
+
const tmpPath = join8(tmpDir, tmpFilename);
|
|
2534
2815
|
let prompt;
|
|
2535
2816
|
if (issue) {
|
|
2536
2817
|
prompt = buildSpecPrompt(issue, workingDir, tmpPath);
|
|
@@ -2577,7 +2858,7 @@ async function boot5(opts) {
|
|
|
2577
2858
|
if (!validation.valid) {
|
|
2578
2859
|
log.warn(`Spec validation warning for ${outputPath}: ${validation.reason}`);
|
|
2579
2860
|
}
|
|
2580
|
-
await
|
|
2861
|
+
await writeFile5(resolvedOutput, cleanedContent, "utf-8");
|
|
2581
2862
|
log.debug(`Wrote cleaned spec to ${resolvedOutput}`);
|
|
2582
2863
|
try {
|
|
2583
2864
|
await unlink(tmpPath);
|
|
@@ -2932,7 +3213,7 @@ function buildInlineTextItem(issues, outputDir) {
|
|
|
2932
3213
|
const title = text.length > 80 ? text.slice(0, 80).trimEnd() + "\u2026" : text;
|
|
2933
3214
|
const slug = slugify(text, MAX_SLUG_LENGTH);
|
|
2934
3215
|
const filename = `${slug}.md`;
|
|
2935
|
-
const filepath =
|
|
3216
|
+
const filepath = join9(outputDir, filename);
|
|
2936
3217
|
const details = {
|
|
2937
3218
|
number: filepath,
|
|
2938
3219
|
title,
|
|
@@ -2994,7 +3275,7 @@ function previewDryRun(validItems, items, isTrackerMode, isInlineText, outputDir
|
|
|
2994
3275
|
let filepath;
|
|
2995
3276
|
if (isTrackerMode) {
|
|
2996
3277
|
const slug = slugify(details.title, 60);
|
|
2997
|
-
filepath =
|
|
3278
|
+
filepath = join9(outputDir, `${id}-${slug}.md`);
|
|
2998
3279
|
} else {
|
|
2999
3280
|
filepath = id;
|
|
3000
3281
|
}
|
|
@@ -3057,7 +3338,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3057
3338
|
if (isTrackerMode) {
|
|
3058
3339
|
const slug = slugify(details.title, MAX_SLUG_LENGTH);
|
|
3059
3340
|
const filename = `${id}-${slug}.md`;
|
|
3060
|
-
filepath =
|
|
3341
|
+
filepath = join9(outputDir, filename);
|
|
3061
3342
|
} else if (isInlineText) {
|
|
3062
3343
|
filepath = id;
|
|
3063
3344
|
} else {
|
|
@@ -3086,7 +3367,7 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3086
3367
|
const h1Title = extractTitle(result.data.content, filepath);
|
|
3087
3368
|
const h1Slug = slugify(h1Title, MAX_SLUG_LENGTH);
|
|
3088
3369
|
const finalFilename = isTrackerMode ? `${id}-${h1Slug}.md` : `${h1Slug}.md`;
|
|
3089
|
-
const finalFilepath =
|
|
3370
|
+
const finalFilepath = join9(outputDir, finalFilename);
|
|
3090
3371
|
if (finalFilepath !== filepath) {
|
|
3091
3372
|
await rename2(filepath, finalFilepath);
|
|
3092
3373
|
filepath = finalFilepath;
|
|
@@ -3105,7 +3386,15 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3105
3386
|
log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
|
|
3106
3387
|
identifier = id;
|
|
3107
3388
|
issueNumbers.push(id);
|
|
3108
|
-
} else if (datasource4.name
|
|
3389
|
+
} else if (datasource4.name === "md") {
|
|
3390
|
+
const parsed = parseIssueFilename(filepath);
|
|
3391
|
+
if (parsed) {
|
|
3392
|
+
await datasource4.update(parsed.issueId, details.title, result.data.content, fetchOpts);
|
|
3393
|
+
log.success(`Updated spec #${parsed.issueId} in-place`);
|
|
3394
|
+
identifier = parsed.issueId;
|
|
3395
|
+
issueNumbers.push(parsed.issueId);
|
|
3396
|
+
}
|
|
3397
|
+
} else {
|
|
3109
3398
|
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
3110
3399
|
log.success(`Created issue #${created.number} from ${filepath}`);
|
|
3111
3400
|
await unlink2(filepath);
|
|
@@ -3191,7 +3480,7 @@ async function runSpecPipeline(opts) {
|
|
|
3191
3480
|
model,
|
|
3192
3481
|
serverUrl,
|
|
3193
3482
|
cwd: specCwd,
|
|
3194
|
-
outputDir =
|
|
3483
|
+
outputDir = join9(specCwd, ".dispatch", "specs"),
|
|
3195
3484
|
org,
|
|
3196
3485
|
project,
|
|
3197
3486
|
workItemType,
|
|
@@ -3272,7 +3561,7 @@ import { readFile as readFile7 } from "fs/promises";
|
|
|
3272
3561
|
import { glob as glob3 } from "glob";
|
|
3273
3562
|
|
|
3274
3563
|
// src/parser.ts
|
|
3275
|
-
import { readFile as readFile6, writeFile as
|
|
3564
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
3276
3565
|
var UNCHECKED_RE = /^(\s*[-*]\s)\[ \]\s+(.+)$/;
|
|
3277
3566
|
var CHECKED_SUB = "$1[x] $2";
|
|
3278
3567
|
var MODE_PREFIX_RE = /^\(([PSI])\)\s+/;
|
|
@@ -3340,7 +3629,7 @@ async function markTaskComplete(task) {
|
|
|
3340
3629
|
);
|
|
3341
3630
|
}
|
|
3342
3631
|
lines[lineIndex] = updated;
|
|
3343
|
-
await
|
|
3632
|
+
await writeFile6(task.file, lines.join(eol), "utf-8");
|
|
3344
3633
|
}
|
|
3345
3634
|
function groupTasksByMode(tasks) {
|
|
3346
3635
|
if (tasks.length === 0) return [];
|
|
@@ -3611,9 +3900,9 @@ ${err.stack}` : ""}`);
|
|
|
3611
3900
|
init_logger();
|
|
3612
3901
|
init_file_logger();
|
|
3613
3902
|
init_environment();
|
|
3614
|
-
import { mkdir as mkdir5, writeFile as
|
|
3615
|
-
import { join as
|
|
3616
|
-
import { randomUUID as
|
|
3903
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
3904
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
3905
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3617
3906
|
async function boot8(opts) {
|
|
3618
3907
|
const { provider } = opts;
|
|
3619
3908
|
if (!provider) {
|
|
@@ -3626,10 +3915,10 @@ async function boot8(opts) {
|
|
|
3626
3915
|
async generate(genOpts) {
|
|
3627
3916
|
try {
|
|
3628
3917
|
const resolvedCwd = resolve3(genOpts.cwd);
|
|
3629
|
-
const tmpDir =
|
|
3918
|
+
const tmpDir = join10(resolvedCwd, ".dispatch", "tmp");
|
|
3630
3919
|
await mkdir5(tmpDir, { recursive: true });
|
|
3631
|
-
const tmpFilename = `commit-${
|
|
3632
|
-
const tmpPath =
|
|
3920
|
+
const tmpFilename = `commit-${randomUUID5()}.md`;
|
|
3921
|
+
const tmpPath = join10(tmpDir, tmpFilename);
|
|
3633
3922
|
const prompt = buildCommitPrompt(genOpts);
|
|
3634
3923
|
fileLoggerStorage.getStore()?.prompt("commit", prompt);
|
|
3635
3924
|
const sessionId = await provider.createSession();
|
|
@@ -3657,7 +3946,7 @@ async function boot8(opts) {
|
|
|
3657
3946
|
};
|
|
3658
3947
|
}
|
|
3659
3948
|
const outputContent = formatOutputFile(parsed);
|
|
3660
|
-
await
|
|
3949
|
+
await writeFile7(tmpPath, outputContent, "utf-8");
|
|
3661
3950
|
log.debug(`Wrote commit agent output to ${tmpPath}`);
|
|
3662
3951
|
fileLoggerStorage.getStore()?.agentEvent("commit", "completed", `message: ${parsed.commitMessage.slice(0, 80)}`);
|
|
3663
3952
|
return {
|
|
@@ -3809,68 +4098,6 @@ function formatOutputFile(parsed) {
|
|
|
3809
4098
|
init_logger();
|
|
3810
4099
|
init_cleanup();
|
|
3811
4100
|
|
|
3812
|
-
// src/helpers/worktree.ts
|
|
3813
|
-
import { join as join9, basename as basename2 } from "path";
|
|
3814
|
-
import { execFile as execFile7 } from "child_process";
|
|
3815
|
-
import { promisify as promisify7 } from "util";
|
|
3816
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
3817
|
-
init_logger();
|
|
3818
|
-
var exec7 = promisify7(execFile7);
|
|
3819
|
-
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
3820
|
-
async function git2(args, cwd) {
|
|
3821
|
-
const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
|
|
3822
|
-
return stdout;
|
|
3823
|
-
}
|
|
3824
|
-
function worktreeName(issueFilename) {
|
|
3825
|
-
const base = basename2(issueFilename);
|
|
3826
|
-
const withoutExt = base.replace(/\.md$/i, "");
|
|
3827
|
-
const match = withoutExt.match(/^(\d+)/);
|
|
3828
|
-
return match ? `issue-${match[1]}` : slugify(withoutExt);
|
|
3829
|
-
}
|
|
3830
|
-
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
3831
|
-
const name = worktreeName(issueFilename);
|
|
3832
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3833
|
-
try {
|
|
3834
|
-
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
3835
|
-
if (startPoint) args.push(startPoint);
|
|
3836
|
-
await git2(args, repoRoot);
|
|
3837
|
-
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
3838
|
-
} catch (err) {
|
|
3839
|
-
const message = log.extractMessage(err);
|
|
3840
|
-
if (message.includes("already exists")) {
|
|
3841
|
-
await git2(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
3842
|
-
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
3843
|
-
} else {
|
|
3844
|
-
throw err;
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
return worktreePath;
|
|
3848
|
-
}
|
|
3849
|
-
async function removeWorktree(repoRoot, issueFilename) {
|
|
3850
|
-
const name = worktreeName(issueFilename);
|
|
3851
|
-
const worktreePath = join9(repoRoot, WORKTREE_DIR, name);
|
|
3852
|
-
try {
|
|
3853
|
-
await git2(["worktree", "remove", worktreePath], repoRoot);
|
|
3854
|
-
} catch {
|
|
3855
|
-
try {
|
|
3856
|
-
await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
3857
|
-
} catch (err) {
|
|
3858
|
-
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
3859
|
-
return;
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
try {
|
|
3863
|
-
await git2(["worktree", "prune"], repoRoot);
|
|
3864
|
-
} catch (err) {
|
|
3865
|
-
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
function generateFeatureBranchName() {
|
|
3869
|
-
const uuid = randomUUID5();
|
|
3870
|
-
const octet = uuid.split("-")[0];
|
|
3871
|
-
return `dispatch/feature-${octet}`;
|
|
3872
|
-
}
|
|
3873
|
-
|
|
3874
4101
|
// src/tui.ts
|
|
3875
4102
|
import chalk6 from "chalk";
|
|
3876
4103
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -4127,181 +4354,6 @@ function createTui() {
|
|
|
4127
4354
|
|
|
4128
4355
|
// src/orchestrator/dispatch-pipeline.ts
|
|
4129
4356
|
init_providers();
|
|
4130
|
-
|
|
4131
|
-
// src/orchestrator/datasource-helpers.ts
|
|
4132
|
-
init_logger();
|
|
4133
|
-
import { basename as basename3, join as join10 } from "path";
|
|
4134
|
-
import { mkdtemp, writeFile as writeFile7 } from "fs/promises";
|
|
4135
|
-
import { tmpdir } from "os";
|
|
4136
|
-
import { execFile as execFile8 } from "child_process";
|
|
4137
|
-
import { promisify as promisify8 } from "util";
|
|
4138
|
-
var exec8 = promisify8(execFile8);
|
|
4139
|
-
function parseIssueFilename(filePath) {
|
|
4140
|
-
const filename = basename3(filePath);
|
|
4141
|
-
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
4142
|
-
if (!match) return null;
|
|
4143
|
-
return { issueId: match[1], slug: match[2] };
|
|
4144
|
-
}
|
|
4145
|
-
async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
4146
|
-
const ids = issueIds.flatMap(
|
|
4147
|
-
(id) => id.split(",").map((s) => s.trim()).filter(Boolean)
|
|
4148
|
-
);
|
|
4149
|
-
const items = [];
|
|
4150
|
-
for (const id of ids) {
|
|
4151
|
-
try {
|
|
4152
|
-
const item = await datasource4.fetch(id, fetchOpts);
|
|
4153
|
-
items.push(item);
|
|
4154
|
-
} catch (err) {
|
|
4155
|
-
const prefix = id.includes("/") || id.includes("\\") || id.endsWith(".md") ? "" : "#";
|
|
4156
|
-
log.warn(`Could not fetch issue ${prefix}${id}: ${log.formatErrorChain(err)}`);
|
|
4157
|
-
}
|
|
4158
|
-
}
|
|
4159
|
-
return items;
|
|
4160
|
-
}
|
|
4161
|
-
async function writeItemsToTempDir(items) {
|
|
4162
|
-
const tempDir = await mkdtemp(join10(tmpdir(), "dispatch-"));
|
|
4163
|
-
const files = [];
|
|
4164
|
-
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
4165
|
-
for (const item of items) {
|
|
4166
|
-
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
4167
|
-
const filename = `${item.number}-${slug}.md`;
|
|
4168
|
-
const filepath = join10(tempDir, filename);
|
|
4169
|
-
await writeFile7(filepath, item.body, "utf-8");
|
|
4170
|
-
files.push(filepath);
|
|
4171
|
-
issueDetailsByFile.set(filepath, item);
|
|
4172
|
-
}
|
|
4173
|
-
files.sort((a, b) => {
|
|
4174
|
-
const numA = parseInt(basename3(a).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4175
|
-
const numB = parseInt(basename3(b).match(/^(\d+)/)?.[1] ?? "0", 10);
|
|
4176
|
-
if (numA !== numB) return numA - numB;
|
|
4177
|
-
return a.localeCompare(b);
|
|
4178
|
-
});
|
|
4179
|
-
return { files, issueDetailsByFile };
|
|
4180
|
-
}
|
|
4181
|
-
async function getCommitSummaries(defaultBranch, cwd) {
|
|
4182
|
-
try {
|
|
4183
|
-
const { stdout } = await exec8(
|
|
4184
|
-
"git",
|
|
4185
|
-
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
4186
|
-
{ cwd, shell: process.platform === "win32" }
|
|
4187
|
-
);
|
|
4188
|
-
return stdout.trim().split("\n").filter(Boolean);
|
|
4189
|
-
} catch {
|
|
4190
|
-
return [];
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
async function getBranchDiff(defaultBranch, cwd) {
|
|
4194
|
-
try {
|
|
4195
|
-
const { stdout } = await exec8(
|
|
4196
|
-
"git",
|
|
4197
|
-
["diff", `${defaultBranch}..HEAD`],
|
|
4198
|
-
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
4199
|
-
);
|
|
4200
|
-
return stdout;
|
|
4201
|
-
} catch {
|
|
4202
|
-
return "";
|
|
4203
|
-
}
|
|
4204
|
-
}
|
|
4205
|
-
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
4206
|
-
const { stdout } = await exec8(
|
|
4207
|
-
"git",
|
|
4208
|
-
["merge-base", defaultBranch, "HEAD"],
|
|
4209
|
-
{ cwd, shell: process.platform === "win32" }
|
|
4210
|
-
);
|
|
4211
|
-
const mergeBase = stdout.trim();
|
|
4212
|
-
await exec8("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
4213
|
-
await exec8("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
4214
|
-
}
|
|
4215
|
-
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
4216
|
-
const sections = [];
|
|
4217
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4218
|
-
if (commits.length > 0) {
|
|
4219
|
-
sections.push("## Summary\n");
|
|
4220
|
-
for (const commit of commits) {
|
|
4221
|
-
sections.push(`- ${commit}`);
|
|
4222
|
-
}
|
|
4223
|
-
sections.push("");
|
|
4224
|
-
}
|
|
4225
|
-
const taskResults = new Map(
|
|
4226
|
-
results.filter((r) => tasks.includes(r.task)).map((r) => [r.task, r])
|
|
4227
|
-
);
|
|
4228
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4229
|
-
const failedTasks = tasks.filter((t) => {
|
|
4230
|
-
const r = taskResults.get(t);
|
|
4231
|
-
return r && !r.success;
|
|
4232
|
-
});
|
|
4233
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4234
|
-
sections.push("## Tasks\n");
|
|
4235
|
-
for (const task of completedTasks) {
|
|
4236
|
-
sections.push(`- [x] ${task.text}`);
|
|
4237
|
-
}
|
|
4238
|
-
for (const task of failedTasks) {
|
|
4239
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4240
|
-
}
|
|
4241
|
-
sections.push("");
|
|
4242
|
-
}
|
|
4243
|
-
if (details.labels.length > 0) {
|
|
4244
|
-
sections.push(`**Labels:** ${details.labels.join(", ")}
|
|
4245
|
-
`);
|
|
4246
|
-
}
|
|
4247
|
-
if (datasourceName === "github") {
|
|
4248
|
-
sections.push(`Closes #${details.number}`);
|
|
4249
|
-
} else if (datasourceName === "azdevops") {
|
|
4250
|
-
sections.push(`Resolves AB#${details.number}`);
|
|
4251
|
-
}
|
|
4252
|
-
return sections.join("\n");
|
|
4253
|
-
}
|
|
4254
|
-
async function buildPrTitle(issueTitle, defaultBranch, cwd) {
|
|
4255
|
-
const commits = await getCommitSummaries(defaultBranch, cwd);
|
|
4256
|
-
if (commits.length === 0) {
|
|
4257
|
-
return issueTitle;
|
|
4258
|
-
}
|
|
4259
|
-
if (commits.length === 1) {
|
|
4260
|
-
return commits[0];
|
|
4261
|
-
}
|
|
4262
|
-
return `${commits[commits.length - 1]} (+${commits.length - 1} more)`;
|
|
4263
|
-
}
|
|
4264
|
-
function buildFeaturePrTitle(featureBranchName, issues) {
|
|
4265
|
-
if (issues.length === 1) {
|
|
4266
|
-
return issues[0].title;
|
|
4267
|
-
}
|
|
4268
|
-
const issueRefs = issues.map((d) => `#${d.number}`).join(", ");
|
|
4269
|
-
return `feat: ${featureBranchName} (${issueRefs})`;
|
|
4270
|
-
}
|
|
4271
|
-
function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
4272
|
-
const sections = [];
|
|
4273
|
-
sections.push("## Issues\n");
|
|
4274
|
-
for (const issue of issues) {
|
|
4275
|
-
sections.push(`- #${issue.number}: ${issue.title}`);
|
|
4276
|
-
}
|
|
4277
|
-
sections.push("");
|
|
4278
|
-
const taskResults = new Map(results.map((r) => [r.task, r]));
|
|
4279
|
-
const completedTasks = tasks.filter((t) => taskResults.get(t)?.success);
|
|
4280
|
-
const failedTasks = tasks.filter((t) => {
|
|
4281
|
-
const r = taskResults.get(t);
|
|
4282
|
-
return r && !r.success;
|
|
4283
|
-
});
|
|
4284
|
-
if (completedTasks.length > 0 || failedTasks.length > 0) {
|
|
4285
|
-
sections.push("## Tasks\n");
|
|
4286
|
-
for (const task of completedTasks) {
|
|
4287
|
-
sections.push(`- [x] ${task.text}`);
|
|
4288
|
-
}
|
|
4289
|
-
for (const task of failedTasks) {
|
|
4290
|
-
sections.push(`- [ ] ${task.text}`);
|
|
4291
|
-
}
|
|
4292
|
-
sections.push("");
|
|
4293
|
-
}
|
|
4294
|
-
for (const issue of issues) {
|
|
4295
|
-
if (datasourceName === "github") {
|
|
4296
|
-
sections.push(`Closes #${issue.number}`);
|
|
4297
|
-
} else if (datasourceName === "azdevops") {
|
|
4298
|
-
sections.push(`Resolves AB#${issue.number}`);
|
|
4299
|
-
}
|
|
4300
|
-
}
|
|
4301
|
-
return sections.join("\n");
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
// src/orchestrator/dispatch-pipeline.ts
|
|
4305
4357
|
init_timeout();
|
|
4306
4358
|
import chalk7 from "chalk";
|
|
4307
4359
|
init_file_logger();
|
|
@@ -5017,6 +5069,58 @@ async function dryRunMode(issueIds, cwd, source, org, project, workItemType, ite
|
|
|
5017
5069
|
}
|
|
5018
5070
|
|
|
5019
5071
|
// src/orchestrator/runner.ts
|
|
5072
|
+
async function runMultiIssueFixTests(opts) {
|
|
5073
|
+
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5074
|
+
const datasource4 = getDatasource(opts.source);
|
|
5075
|
+
const fetchOpts = { cwd: opts.cwd, org: opts.org, project: opts.project };
|
|
5076
|
+
const items = await fetchItemsById(opts.issueIds, datasource4, fetchOpts);
|
|
5077
|
+
if (items.length === 0) {
|
|
5078
|
+
log.warn("No issues found for the given IDs");
|
|
5079
|
+
return { mode: "fix-tests", success: false, error: "No issues found" };
|
|
5080
|
+
}
|
|
5081
|
+
let username = "";
|
|
5082
|
+
try {
|
|
5083
|
+
username = await datasource4.getUsername({ cwd: opts.cwd });
|
|
5084
|
+
} catch (err) {
|
|
5085
|
+
log.warn(`Could not resolve git username for branch naming: ${log.formatErrorChain(err)}`);
|
|
5086
|
+
}
|
|
5087
|
+
log.info(`Running fix-tests for ${items.length} issue(s) in worktrees`);
|
|
5088
|
+
const issueResults = [];
|
|
5089
|
+
for (const item of items) {
|
|
5090
|
+
const branchName = datasource4.buildBranchName(item.number, item.title, username);
|
|
5091
|
+
const issueFilename = `${item.number}-fix-tests.md`;
|
|
5092
|
+
let worktreePath;
|
|
5093
|
+
try {
|
|
5094
|
+
worktreePath = await createWorktree(opts.cwd, issueFilename, branchName);
|
|
5095
|
+
registerCleanup(async () => {
|
|
5096
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5097
|
+
});
|
|
5098
|
+
log.info(`Created worktree for issue #${item.number} at ${worktreePath}`);
|
|
5099
|
+
const result = await runFixTestsPipeline2({
|
|
5100
|
+
cwd: worktreePath,
|
|
5101
|
+
provider: opts.provider,
|
|
5102
|
+
serverUrl: opts.serverUrl,
|
|
5103
|
+
verbose: opts.verbose,
|
|
5104
|
+
testTimeout: opts.testTimeout
|
|
5105
|
+
});
|
|
5106
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: result.success, error: result.error });
|
|
5107
|
+
} catch (err) {
|
|
5108
|
+
const message = log.extractMessage(err);
|
|
5109
|
+
log.error(`Fix-tests failed for issue #${item.number}: ${message}`);
|
|
5110
|
+
issueResults.push({ issueId: item.number, branch: branchName, success: false, error: message });
|
|
5111
|
+
} finally {
|
|
5112
|
+
if (worktreePath) {
|
|
5113
|
+
try {
|
|
5114
|
+
await removeWorktree(opts.cwd, issueFilename);
|
|
5115
|
+
} catch (err) {
|
|
5116
|
+
log.warn(`Could not remove worktree for issue #${item.number}: ${log.formatErrorChain(err)}`);
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
const allSuccess = issueResults.length > 0 && issueResults.every((r) => r.success);
|
|
5122
|
+
return { mode: "fix-tests", success: allSuccess, issueResults };
|
|
5123
|
+
}
|
|
5020
5124
|
async function boot9(opts) {
|
|
5021
5125
|
const { cwd } = opts;
|
|
5022
5126
|
const runner = {
|
|
@@ -5029,7 +5133,25 @@ async function boot9(opts) {
|
|
|
5029
5133
|
}
|
|
5030
5134
|
if (opts2.mode === "fix-tests") {
|
|
5031
5135
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5032
|
-
|
|
5136
|
+
if (!opts2.issueIds || opts2.issueIds.length === 0) {
|
|
5137
|
+
return runFixTestsPipeline2({ cwd, provider: opts2.provider ?? "opencode", serverUrl: opts2.serverUrl, verbose: opts2.verbose ?? false, testTimeout: opts2.testTimeout });
|
|
5138
|
+
}
|
|
5139
|
+
const source = opts2.source;
|
|
5140
|
+
if (!source) {
|
|
5141
|
+
log.error("No datasource configured for multi-issue fix-tests.");
|
|
5142
|
+
return { mode: "fix-tests", success: false, error: "No datasource configured" };
|
|
5143
|
+
}
|
|
5144
|
+
return runMultiIssueFixTests({
|
|
5145
|
+
cwd,
|
|
5146
|
+
issueIds: opts2.issueIds,
|
|
5147
|
+
source,
|
|
5148
|
+
provider: opts2.provider ?? "opencode",
|
|
5149
|
+
serverUrl: opts2.serverUrl,
|
|
5150
|
+
verbose: opts2.verbose ?? false,
|
|
5151
|
+
testTimeout: opts2.testTimeout,
|
|
5152
|
+
org: opts2.org,
|
|
5153
|
+
project: opts2.project
|
|
5154
|
+
});
|
|
5033
5155
|
}
|
|
5034
5156
|
const { mode: _, ...rest } = opts2;
|
|
5035
5157
|
return runner.orchestrate(rest);
|
|
@@ -5058,13 +5180,27 @@ async function boot9(opts) {
|
|
|
5058
5180
|
log.error("--feature and --no-branch are mutually exclusive");
|
|
5059
5181
|
process.exit(1);
|
|
5060
5182
|
}
|
|
5061
|
-
if (m.fixTests && m.issueIds.length > 0) {
|
|
5062
|
-
log.error("--fix-tests cannot be combined with issue IDs");
|
|
5063
|
-
process.exit(1);
|
|
5064
|
-
}
|
|
5065
5183
|
if (m.fixTests) {
|
|
5066
5184
|
const { runFixTestsPipeline: runFixTestsPipeline2 } = await Promise.resolve().then(() => (init_fix_tests_pipeline(), fix_tests_pipeline_exports));
|
|
5067
|
-
|
|
5185
|
+
if (m.issueIds.length === 0) {
|
|
5186
|
+
return runFixTestsPipeline2({ cwd: m.cwd, provider: m.provider, serverUrl: m.serverUrl, verbose: m.verbose, testTimeout: m.testTimeout });
|
|
5187
|
+
}
|
|
5188
|
+
const source = m.issueSource;
|
|
5189
|
+
if (!source) {
|
|
5190
|
+
log.error("No datasource configured. Use --source or run 'dispatch config' to set up defaults.");
|
|
5191
|
+
process.exit(1);
|
|
5192
|
+
}
|
|
5193
|
+
return runMultiIssueFixTests({
|
|
5194
|
+
cwd: m.cwd,
|
|
5195
|
+
issueIds: m.issueIds,
|
|
5196
|
+
source,
|
|
5197
|
+
provider: m.provider,
|
|
5198
|
+
serverUrl: m.serverUrl,
|
|
5199
|
+
verbose: m.verbose,
|
|
5200
|
+
testTimeout: m.testTimeout,
|
|
5201
|
+
org: m.org,
|
|
5202
|
+
project: m.project
|
|
5203
|
+
});
|
|
5068
5204
|
}
|
|
5069
5205
|
if (m.spec) {
|
|
5070
5206
|
return this.generateSpecs({
|
|
@@ -5170,6 +5306,7 @@ var HELP = `
|
|
|
5170
5306
|
dispatch --respec <glob> Regenerate specs matching a glob pattern
|
|
5171
5307
|
dispatch --spec "description" Generate a spec from an inline text description
|
|
5172
5308
|
dispatch --fix-tests Run tests and fix failures via AI agent
|
|
5309
|
+
dispatch --fix-tests <ids> Fix tests on specific issue branches (in worktrees)
|
|
5173
5310
|
|
|
5174
5311
|
Dispatch options:
|
|
5175
5312
|
--dry-run List tasks without dispatching (also works with --spec)
|
|
@@ -5224,6 +5361,10 @@ var HELP = `
|
|
|
5224
5361
|
dispatch --spec "feature A should do x" --provider copilot
|
|
5225
5362
|
dispatch --feature
|
|
5226
5363
|
dispatch --feature my-feature
|
|
5364
|
+
dispatch --fix-tests
|
|
5365
|
+
dispatch --fix-tests 14
|
|
5366
|
+
dispatch --fix-tests 14 15 16
|
|
5367
|
+
dispatch --fix-tests 14,15,16
|
|
5227
5368
|
dispatch config
|
|
5228
5369
|
`.trimStart();
|
|
5229
5370
|
function parseArgs(argv) {
|
|
@@ -5233,7 +5374,7 @@ function parseArgs(argv) {
|
|
|
5233
5374
|
},
|
|
5234
5375
|
writeErr: () => {
|
|
5235
5376
|
}
|
|
5236
|
-
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature [name]", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
5377
|
+
}).helpOption(false).argument("[issueIds...]").option("-h, --help", "Show help").option("-v, --version", "Show version").option("--dry-run", "List tasks without dispatching").option("--no-plan", "Skip the planner agent").option("--no-branch", "Skip branch creation").option("--no-worktree", "Skip git worktree isolation").option("--feature [name]", "Group issues into a single feature branch").option("--force", "Ignore prior run state").option("--verbose", "Show detailed debug output").option("--fix-tests", "Run tests and fix failures (optionally pass issue IDs to target specific branches)").option("--spec <values...>", "Spec mode: issue numbers, glob, or text").option("--respec [values...]", "Regenerate specs").addOption(
|
|
5237
5378
|
new Option("--provider <name>", "Agent backend").choices(PROVIDER_NAMES)
|
|
5238
5379
|
).addOption(
|
|
5239
5380
|
new Option("--source <name>", "Issue source").choices(
|
|
@@ -5397,7 +5538,7 @@ async function main() {
|
|
|
5397
5538
|
process.exit(0);
|
|
5398
5539
|
}
|
|
5399
5540
|
if (args.version) {
|
|
5400
|
-
console.log(`dispatch v${"1.4.
|
|
5541
|
+
console.log(`dispatch v${"1.4.2"}`);
|
|
5401
5542
|
process.exit(0);
|
|
5402
5543
|
}
|
|
5403
5544
|
const orchestrator = await boot9({ cwd: args.cwd });
|