@pruddiman/dispatch 1.4.1 → 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 +394 -331
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -210,27 +210,6 @@ var init_logger = __esm({
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
-
// src/helpers/cleanup.ts
|
|
214
|
-
function registerCleanup(fn) {
|
|
215
|
-
cleanups.push(fn);
|
|
216
|
-
}
|
|
217
|
-
async function runCleanup() {
|
|
218
|
-
const fns = cleanups.splice(0);
|
|
219
|
-
for (const fn of fns) {
|
|
220
|
-
try {
|
|
221
|
-
await fn();
|
|
222
|
-
} catch {
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
var cleanups;
|
|
227
|
-
var init_cleanup = __esm({
|
|
228
|
-
"src/helpers/cleanup.ts"() {
|
|
229
|
-
"use strict";
|
|
230
|
-
cleanups = [];
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
213
|
// src/helpers/guards.ts
|
|
235
214
|
function hasProperty(value, key) {
|
|
236
215
|
return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key);
|
|
@@ -613,7 +592,7 @@ var init_copilot = __esm({
|
|
|
613
592
|
});
|
|
614
593
|
|
|
615
594
|
// src/providers/claude.ts
|
|
616
|
-
import { randomUUID
|
|
595
|
+
import { randomUUID } from "crypto";
|
|
617
596
|
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
|
|
618
597
|
async function listModels3(_opts) {
|
|
619
598
|
return [
|
|
@@ -636,7 +615,7 @@ async function boot3(opts) {
|
|
|
636
615
|
try {
|
|
637
616
|
const sessionOpts = { model, permissionMode: "acceptEdits", ...cwd ? { cwd } : {} };
|
|
638
617
|
const session = unstable_v2_createSession(sessionOpts);
|
|
639
|
-
const sessionId =
|
|
618
|
+
const sessionId = randomUUID();
|
|
640
619
|
sessions.set(sessionId, session);
|
|
641
620
|
log.debug(`Session created: ${sessionId}`);
|
|
642
621
|
return sessionId;
|
|
@@ -688,7 +667,7 @@ var init_claude = __esm({
|
|
|
688
667
|
});
|
|
689
668
|
|
|
690
669
|
// src/providers/codex.ts
|
|
691
|
-
import { randomUUID as
|
|
670
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
692
671
|
async function loadAgentLoop() {
|
|
693
672
|
return import("@openai/codex");
|
|
694
673
|
}
|
|
@@ -710,7 +689,7 @@ async function boot4(opts) {
|
|
|
710
689
|
async createSession() {
|
|
711
690
|
log.debug("Creating Codex session...");
|
|
712
691
|
try {
|
|
713
|
-
const sessionId =
|
|
692
|
+
const sessionId = randomUUID2();
|
|
714
693
|
const agent = new AgentLoop({
|
|
715
694
|
model,
|
|
716
695
|
config: { model, instructions: "" },
|
|
@@ -777,11 +756,11 @@ var init_codex = __esm({
|
|
|
777
756
|
});
|
|
778
757
|
|
|
779
758
|
// src/providers/detect.ts
|
|
780
|
-
import { execFile as
|
|
781
|
-
import { promisify as
|
|
759
|
+
import { execFile as execFile3 } from "child_process";
|
|
760
|
+
import { promisify as promisify3 } from "util";
|
|
782
761
|
async function checkProviderInstalled(name) {
|
|
783
762
|
try {
|
|
784
|
-
await
|
|
763
|
+
await exec3(PROVIDER_BINARIES[name], ["--version"], {
|
|
785
764
|
shell: process.platform === "win32",
|
|
786
765
|
timeout: DETECTION_TIMEOUT_MS
|
|
787
766
|
});
|
|
@@ -790,11 +769,11 @@ async function checkProviderInstalled(name) {
|
|
|
790
769
|
return false;
|
|
791
770
|
}
|
|
792
771
|
}
|
|
793
|
-
var
|
|
772
|
+
var exec3, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
|
|
794
773
|
var init_detect = __esm({
|
|
795
774
|
"src/providers/detect.ts"() {
|
|
796
775
|
"use strict";
|
|
797
|
-
|
|
776
|
+
exec3 = promisify3(execFile3);
|
|
798
777
|
DETECTION_TIMEOUT_MS = 5e3;
|
|
799
778
|
PROVIDER_BINARIES = {
|
|
800
779
|
opencode: "opencode",
|
|
@@ -849,6 +828,27 @@ var init_providers = __esm({
|
|
|
849
828
|
}
|
|
850
829
|
});
|
|
851
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
|
+
|
|
852
852
|
// src/helpers/environment.ts
|
|
853
853
|
function getEnvironmentInfo() {
|
|
854
854
|
const platform = process.platform;
|
|
@@ -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";
|
|
@@ -1767,30 +1767,224 @@ async function fetchComments(workItemId, opts) {
|
|
|
1767
1767
|
}
|
|
1768
1768
|
|
|
1769
1769
|
// src/datasources/md.ts
|
|
1770
|
-
import { execFile as
|
|
1771
|
-
import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
|
|
1772
|
-
import { basename, dirname as
|
|
1773
|
-
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";
|
|
1774
1774
|
import { glob } from "glob";
|
|
1775
1775
|
|
|
1776
|
-
// src/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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;
|
|
1785
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.");
|
|
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 }
|
|
1786
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
|
+
}
|
|
1787
1976
|
|
|
1788
1977
|
// src/datasources/md.ts
|
|
1789
|
-
|
|
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
|
+
}
|
|
1790
1984
|
var DEFAULT_DIR = ".dispatch/specs";
|
|
1791
1985
|
function resolveDir(opts) {
|
|
1792
1986
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1793
|
-
return
|
|
1987
|
+
return join3(cwd, DEFAULT_DIR);
|
|
1794
1988
|
}
|
|
1795
1989
|
function resolveFilePath(issueId, opts) {
|
|
1796
1990
|
const filename = issueId.endsWith(".md") ? issueId : `${issueId}.md`;
|
|
@@ -1799,7 +1993,18 @@ function resolveFilePath(issueId, opts) {
|
|
|
1799
1993
|
const cwd = opts?.cwd ?? process.cwd();
|
|
1800
1994
|
return resolve(cwd, filename);
|
|
1801
1995
|
}
|
|
1802
|
-
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);
|
|
1803
2008
|
}
|
|
1804
2009
|
function extractTitle(content, filename) {
|
|
1805
2010
|
const match = content.match(/^#\s+(.+)$/m);
|
|
@@ -1818,13 +2023,14 @@ function extractTitle(content, filename) {
|
|
|
1818
2023
|
return parsePath(filename).name;
|
|
1819
2024
|
}
|
|
1820
2025
|
function toIssueDetails(filename, content, dir) {
|
|
2026
|
+
const idMatch = /^(\d+)-/.exec(filename);
|
|
1821
2027
|
return {
|
|
1822
|
-
number: filename,
|
|
2028
|
+
number: idMatch ? idMatch[1] : filename,
|
|
1823
2029
|
title: extractTitle(content, filename),
|
|
1824
2030
|
body: content,
|
|
1825
2031
|
labels: [],
|
|
1826
2032
|
state: "open",
|
|
1827
|
-
url:
|
|
2033
|
+
url: join3(dir, filename),
|
|
1828
2034
|
comments: [],
|
|
1829
2035
|
acceptanceCriteria: ""
|
|
1830
2036
|
};
|
|
@@ -1832,7 +2038,7 @@ function toIssueDetails(filename, content, dir) {
|
|
|
1832
2038
|
var datasource3 = {
|
|
1833
2039
|
name: "md",
|
|
1834
2040
|
supportsGit() {
|
|
1835
|
-
return
|
|
2041
|
+
return true;
|
|
1836
2042
|
},
|
|
1837
2043
|
async list(opts) {
|
|
1838
2044
|
if (opts?.pattern) {
|
|
@@ -1841,9 +2047,9 @@ var datasource3 = {
|
|
|
1841
2047
|
const mdFiles2 = files.filter((f) => f.endsWith(".md")).sort();
|
|
1842
2048
|
const results2 = [];
|
|
1843
2049
|
for (const filePath of mdFiles2) {
|
|
1844
|
-
const content = await
|
|
2050
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1845
2051
|
const filename = basename(filePath);
|
|
1846
|
-
const dir2 =
|
|
2052
|
+
const dir2 = dirname3(filePath);
|
|
1847
2053
|
results2.push(toIssueDetails(filename, content, dir2));
|
|
1848
2054
|
}
|
|
1849
2055
|
return results2;
|
|
@@ -1858,44 +2064,81 @@ var datasource3 = {
|
|
|
1858
2064
|
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
1859
2065
|
const results = [];
|
|
1860
2066
|
for (const filename of mdFiles) {
|
|
1861
|
-
const filePath =
|
|
1862
|
-
const content = await
|
|
2067
|
+
const filePath = join3(dir, filename);
|
|
2068
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1863
2069
|
results.push(toIssueDetails(filename, content, dir));
|
|
1864
2070
|
}
|
|
1865
2071
|
return results;
|
|
1866
2072
|
},
|
|
1867
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
|
+
}
|
|
1868
2083
|
const filePath = resolveFilePath(issueId, opts);
|
|
1869
|
-
const content = await
|
|
2084
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1870
2085
|
const filename = basename(filePath);
|
|
1871
|
-
const dir =
|
|
2086
|
+
const dir = dirname3(filePath);
|
|
1872
2087
|
return toIssueDetails(filename, content, dir);
|
|
1873
2088
|
},
|
|
1874
2089
|
async update(issueId, _title, body, opts) {
|
|
1875
|
-
const filePath =
|
|
1876
|
-
await
|
|
2090
|
+
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
2091
|
+
await writeFile2(filePath, body, "utf-8");
|
|
1877
2092
|
},
|
|
1878
2093
|
async close(issueId, opts) {
|
|
1879
|
-
const filePath =
|
|
2094
|
+
const filePath = await resolveNumericFilePath(issueId, opts);
|
|
1880
2095
|
const filename = basename(filePath);
|
|
1881
|
-
const archiveDir =
|
|
1882
|
-
await
|
|
1883
|
-
await rename(filePath,
|
|
2096
|
+
const archiveDir = join3(dirname3(filePath), "archive");
|
|
2097
|
+
await mkdir2(archiveDir, { recursive: true });
|
|
2098
|
+
await rename(filePath, join3(archiveDir, filename));
|
|
1884
2099
|
},
|
|
1885
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;
|
|
1886
2105
|
const dir = resolveDir(opts);
|
|
1887
|
-
await
|
|
1888
|
-
const filename = `${slugify(title)}.md`;
|
|
1889
|
-
const filePath =
|
|
1890
|
-
await
|
|
1891
|
-
|
|
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
|
+
};
|
|
1892
2116
|
},
|
|
1893
|
-
async getDefaultBranch(
|
|
1894
|
-
|
|
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
|
+
}
|
|
1895
2138
|
},
|
|
1896
2139
|
async getUsername(opts) {
|
|
1897
2140
|
try {
|
|
1898
|
-
const { stdout } = await
|
|
2141
|
+
const { stdout } = await exec4("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
|
|
1899
2142
|
const name = stdout.trim();
|
|
1900
2143
|
if (!name) return "local";
|
|
1901
2144
|
return slugify(name);
|
|
@@ -1907,25 +2150,39 @@ var datasource3 = {
|
|
|
1907
2150
|
const slug = slugify(title, 50);
|
|
1908
2151
|
return `${username}/dispatch/${issueNumber}-${slug}`;
|
|
1909
2152
|
},
|
|
1910
|
-
async createAndSwitchBranch(
|
|
1911
|
-
|
|
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
|
+
}
|
|
1912
2164
|
},
|
|
1913
|
-
async switchBranch(
|
|
1914
|
-
|
|
2165
|
+
async switchBranch(branchName, opts) {
|
|
2166
|
+
await git2(["checkout", branchName], opts.cwd);
|
|
1915
2167
|
},
|
|
1916
2168
|
async pushBranch(_branchName, _opts) {
|
|
1917
|
-
throw new UnsupportedOperationError("pushBranch");
|
|
1918
2169
|
},
|
|
1919
|
-
async commitAllChanges(
|
|
1920
|
-
|
|
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);
|
|
1921
2178
|
},
|
|
1922
2179
|
async createPullRequest(_branchName, _issueNumber, _title, _body, _opts) {
|
|
1923
|
-
|
|
2180
|
+
return "";
|
|
1924
2181
|
}
|
|
1925
2182
|
};
|
|
1926
2183
|
|
|
1927
2184
|
// src/datasources/index.ts
|
|
1928
|
-
var
|
|
2185
|
+
var exec5 = promisify5(execFile5);
|
|
1929
2186
|
var DATASOURCES = {
|
|
1930
2187
|
github: datasource,
|
|
1931
2188
|
azdevops: datasource2,
|
|
@@ -1943,7 +2200,7 @@ function getDatasource(name) {
|
|
|
1943
2200
|
}
|
|
1944
2201
|
async function getGitRemoteUrl(cwd) {
|
|
1945
2202
|
try {
|
|
1946
|
-
const { stdout } = await
|
|
2203
|
+
const { stdout } = await exec5("git", ["remote", "get-url", "origin"], {
|
|
1947
2204
|
cwd,
|
|
1948
2205
|
shell: process.platform === "win32"
|
|
1949
2206
|
});
|
|
@@ -2112,12 +2369,12 @@ async function resolveSource(issues, issueSource, cwd) {
|
|
|
2112
2369
|
|
|
2113
2370
|
// src/orchestrator/datasource-helpers.ts
|
|
2114
2371
|
init_logger();
|
|
2115
|
-
import { basename as basename2, join as
|
|
2116
|
-
import { mkdtemp, writeFile as
|
|
2372
|
+
import { basename as basename2, join as join4 } from "path";
|
|
2373
|
+
import { mkdtemp, writeFile as writeFile3 } from "fs/promises";
|
|
2117
2374
|
import { tmpdir } from "os";
|
|
2118
|
-
import { execFile as
|
|
2119
|
-
import { promisify as
|
|
2120
|
-
var
|
|
2375
|
+
import { execFile as execFile6 } from "child_process";
|
|
2376
|
+
import { promisify as promisify6 } from "util";
|
|
2377
|
+
var exec6 = promisify6(execFile6);
|
|
2121
2378
|
function parseIssueFilename(filePath) {
|
|
2122
2379
|
const filename = basename2(filePath);
|
|
2123
2380
|
const match = /^(\d+)-(.+)\.md$/.exec(filename);
|
|
@@ -2141,14 +2398,15 @@ async function fetchItemsById(issueIds, datasource4, fetchOpts) {
|
|
|
2141
2398
|
return items;
|
|
2142
2399
|
}
|
|
2143
2400
|
async function writeItemsToTempDir(items) {
|
|
2144
|
-
const tempDir = await mkdtemp(
|
|
2401
|
+
const tempDir = await mkdtemp(join4(tmpdir(), "dispatch-"));
|
|
2145
2402
|
const files = [];
|
|
2146
2403
|
const issueDetailsByFile = /* @__PURE__ */ new Map();
|
|
2147
2404
|
for (const item of items) {
|
|
2148
2405
|
const slug = slugify(item.title, MAX_SLUG_LENGTH);
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
|
|
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");
|
|
2152
2410
|
files.push(filepath);
|
|
2153
2411
|
issueDetailsByFile.set(filepath, item);
|
|
2154
2412
|
}
|
|
@@ -2162,7 +2420,7 @@ async function writeItemsToTempDir(items) {
|
|
|
2162
2420
|
}
|
|
2163
2421
|
async function getCommitSummaries(defaultBranch, cwd) {
|
|
2164
2422
|
try {
|
|
2165
|
-
const { stdout } = await
|
|
2423
|
+
const { stdout } = await exec6(
|
|
2166
2424
|
"git",
|
|
2167
2425
|
["log", `${defaultBranch}..HEAD`, "--pretty=format:%s"],
|
|
2168
2426
|
{ cwd, shell: process.platform === "win32" }
|
|
@@ -2174,7 +2432,7 @@ async function getCommitSummaries(defaultBranch, cwd) {
|
|
|
2174
2432
|
}
|
|
2175
2433
|
async function getBranchDiff(defaultBranch, cwd) {
|
|
2176
2434
|
try {
|
|
2177
|
-
const { stdout } = await
|
|
2435
|
+
const { stdout } = await exec6(
|
|
2178
2436
|
"git",
|
|
2179
2437
|
["diff", `${defaultBranch}..HEAD`],
|
|
2180
2438
|
{ cwd, maxBuffer: 10 * 1024 * 1024, shell: process.platform === "win32" }
|
|
@@ -2185,14 +2443,14 @@ async function getBranchDiff(defaultBranch, cwd) {
|
|
|
2185
2443
|
}
|
|
2186
2444
|
}
|
|
2187
2445
|
async function squashBranchCommits(defaultBranch, message, cwd) {
|
|
2188
|
-
const { stdout } = await
|
|
2446
|
+
const { stdout } = await exec6(
|
|
2189
2447
|
"git",
|
|
2190
2448
|
["merge-base", defaultBranch, "HEAD"],
|
|
2191
2449
|
{ cwd, shell: process.platform === "win32" }
|
|
2192
2450
|
);
|
|
2193
2451
|
const mergeBase = stdout.trim();
|
|
2194
|
-
await
|
|
2195
|
-
await
|
|
2452
|
+
await exec6("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
|
|
2453
|
+
await exec6("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
|
|
2196
2454
|
}
|
|
2197
2455
|
async function buildPrBody(details, tasks, results, defaultBranch, datasourceName, cwd) {
|
|
2198
2456
|
const sections = [];
|
|
@@ -2284,15 +2542,15 @@ function buildFeaturePrBody(issues, tasks, results, datasourceName) {
|
|
|
2284
2542
|
}
|
|
2285
2543
|
|
|
2286
2544
|
// src/helpers/worktree.ts
|
|
2287
|
-
import { join as
|
|
2288
|
-
import { execFile as
|
|
2289
|
-
import { promisify as
|
|
2290
|
-
import { randomUUID } from "crypto";
|
|
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";
|
|
2291
2549
|
init_logger();
|
|
2292
|
-
var
|
|
2550
|
+
var exec7 = promisify7(execFile7);
|
|
2293
2551
|
var WORKTREE_DIR = ".dispatch/worktrees";
|
|
2294
|
-
async function
|
|
2295
|
-
const { stdout } = await
|
|
2552
|
+
async function git3(args, cwd) {
|
|
2553
|
+
const { stdout } = await exec7("git", args, { cwd, shell: process.platform === "win32" });
|
|
2296
2554
|
return stdout;
|
|
2297
2555
|
}
|
|
2298
2556
|
function worktreeName(issueFilename) {
|
|
@@ -2303,16 +2561,16 @@ function worktreeName(issueFilename) {
|
|
|
2303
2561
|
}
|
|
2304
2562
|
async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
2305
2563
|
const name = worktreeName(issueFilename);
|
|
2306
|
-
const worktreePath =
|
|
2564
|
+
const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
|
|
2307
2565
|
try {
|
|
2308
2566
|
const args = ["worktree", "add", worktreePath, "-b", branchName];
|
|
2309
2567
|
if (startPoint) args.push(startPoint);
|
|
2310
|
-
await
|
|
2568
|
+
await git3(args, repoRoot);
|
|
2311
2569
|
log.debug(`Created worktree at ${worktreePath} on branch ${branchName}`);
|
|
2312
2570
|
} catch (err) {
|
|
2313
2571
|
const message = log.extractMessage(err);
|
|
2314
2572
|
if (message.includes("already exists")) {
|
|
2315
|
-
await
|
|
2573
|
+
await git3(["worktree", "add", worktreePath, branchName], repoRoot);
|
|
2316
2574
|
log.debug(`Created worktree at ${worktreePath} using existing branch ${branchName}`);
|
|
2317
2575
|
} else {
|
|
2318
2576
|
throw err;
|
|
@@ -2322,25 +2580,25 @@ async function createWorktree(repoRoot, issueFilename, branchName, startPoint) {
|
|
|
2322
2580
|
}
|
|
2323
2581
|
async function removeWorktree(repoRoot, issueFilename) {
|
|
2324
2582
|
const name = worktreeName(issueFilename);
|
|
2325
|
-
const worktreePath =
|
|
2583
|
+
const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
|
|
2326
2584
|
try {
|
|
2327
|
-
await
|
|
2585
|
+
await git3(["worktree", "remove", worktreePath], repoRoot);
|
|
2328
2586
|
} catch {
|
|
2329
2587
|
try {
|
|
2330
|
-
await
|
|
2588
|
+
await git3(["worktree", "remove", "--force", worktreePath], repoRoot);
|
|
2331
2589
|
} catch (err) {
|
|
2332
2590
|
log.warn(`Could not remove worktree ${name}: ${log.formatErrorChain(err)}`);
|
|
2333
2591
|
return;
|
|
2334
2592
|
}
|
|
2335
2593
|
}
|
|
2336
2594
|
try {
|
|
2337
|
-
await
|
|
2595
|
+
await git3(["worktree", "prune"], repoRoot);
|
|
2338
2596
|
} catch (err) {
|
|
2339
2597
|
log.warn(`Could not prune worktrees: ${log.formatErrorChain(err)}`);
|
|
2340
2598
|
}
|
|
2341
2599
|
}
|
|
2342
2600
|
function generateFeatureBranchName() {
|
|
2343
|
-
const uuid =
|
|
2601
|
+
const uuid = randomUUID3();
|
|
2344
2602
|
const octet = uuid.split("-")[0];
|
|
2345
2603
|
return `dispatch/feature-${octet}`;
|
|
2346
2604
|
}
|
|
@@ -2351,24 +2609,24 @@ init_logger();
|
|
|
2351
2609
|
|
|
2352
2610
|
// src/helpers/confirm-large-batch.ts
|
|
2353
2611
|
init_logger();
|
|
2354
|
-
import { input } from "@inquirer/prompts";
|
|
2355
|
-
import
|
|
2612
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
2613
|
+
import chalk3 from "chalk";
|
|
2356
2614
|
var LARGE_BATCH_THRESHOLD = 100;
|
|
2357
2615
|
async function confirmLargeBatch(count, threshold = LARGE_BATCH_THRESHOLD) {
|
|
2358
2616
|
if (count <= threshold) return true;
|
|
2359
2617
|
log.warn(
|
|
2360
|
-
`This operation will process ${
|
|
2618
|
+
`This operation will process ${chalk3.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
|
|
2361
2619
|
);
|
|
2362
|
-
const answer = await
|
|
2363
|
-
message: `Type ${
|
|
2620
|
+
const answer = await input2({
|
|
2621
|
+
message: `Type ${chalk3.bold('"yes"')} to proceed:`
|
|
2364
2622
|
});
|
|
2365
2623
|
return answer.trim().toLowerCase() === "yes";
|
|
2366
2624
|
}
|
|
2367
2625
|
|
|
2368
2626
|
// src/helpers/prereqs.ts
|
|
2369
|
-
import { execFile as
|
|
2370
|
-
import { promisify as
|
|
2371
|
-
var
|
|
2627
|
+
import { execFile as execFile8 } from "child_process";
|
|
2628
|
+
import { promisify as promisify8 } from "util";
|
|
2629
|
+
var exec8 = promisify8(execFile8);
|
|
2372
2630
|
var MIN_NODE_VERSION = "20.12.0";
|
|
2373
2631
|
function parseSemver(version) {
|
|
2374
2632
|
const [major, minor, patch] = version.split(".").map(Number);
|
|
@@ -2384,7 +2642,7 @@ function semverGte(current, minimum) {
|
|
|
2384
2642
|
async function checkPrereqs(context) {
|
|
2385
2643
|
const failures = [];
|
|
2386
2644
|
try {
|
|
2387
|
-
await
|
|
2645
|
+
await exec8("git", ["--version"], { shell: process.platform === "win32" });
|
|
2388
2646
|
} catch {
|
|
2389
2647
|
failures.push("git is required but was not found on PATH. Install it from https://git-scm.com");
|
|
2390
2648
|
}
|
|
@@ -2396,7 +2654,7 @@ async function checkPrereqs(context) {
|
|
|
2396
2654
|
}
|
|
2397
2655
|
if (context?.datasource === "github") {
|
|
2398
2656
|
try {
|
|
2399
|
-
await
|
|
2657
|
+
await exec8("gh", ["--version"], { shell: process.platform === "win32" });
|
|
2400
2658
|
} catch {
|
|
2401
2659
|
failures.push(
|
|
2402
2660
|
"gh (GitHub CLI) is required for the github datasource but was not found on PATH. Install it from https://cli.github.com/"
|
|
@@ -2405,7 +2663,7 @@ async function checkPrereqs(context) {
|
|
|
2405
2663
|
}
|
|
2406
2664
|
if (context?.datasource === "azdevops") {
|
|
2407
2665
|
try {
|
|
2408
|
-
await
|
|
2666
|
+
await exec8("az", ["--version"], { shell: process.platform === "win32" });
|
|
2409
2667
|
} catch {
|
|
2410
2668
|
failures.push(
|
|
2411
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/"
|
|
@@ -2417,13 +2675,13 @@ async function checkPrereqs(context) {
|
|
|
2417
2675
|
|
|
2418
2676
|
// src/helpers/gitignore.ts
|
|
2419
2677
|
init_logger();
|
|
2420
|
-
import { readFile as
|
|
2421
|
-
import { join as
|
|
2678
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
2679
|
+
import { join as join6 } from "path";
|
|
2422
2680
|
async function ensureGitignoreEntry(repoRoot, entry) {
|
|
2423
|
-
const gitignorePath =
|
|
2681
|
+
const gitignorePath = join6(repoRoot, ".gitignore");
|
|
2424
2682
|
let contents = "";
|
|
2425
2683
|
try {
|
|
2426
|
-
contents = await
|
|
2684
|
+
contents = await readFile3(gitignorePath, "utf8");
|
|
2427
2685
|
} catch (err) {
|
|
2428
2686
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
2429
2687
|
} else {
|
|
@@ -2439,7 +2697,7 @@ async function ensureGitignoreEntry(repoRoot, entry) {
|
|
|
2439
2697
|
}
|
|
2440
2698
|
try {
|
|
2441
2699
|
const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
2442
|
-
await
|
|
2700
|
+
await writeFile4(gitignorePath, `${contents}${separator}${entry}
|
|
2443
2701
|
`, "utf8");
|
|
2444
2702
|
log.debug(`Added '${entry}' to .gitignore`);
|
|
2445
2703
|
} catch (err) {
|
|
@@ -2452,209 +2710,6 @@ init_logger();
|
|
|
2452
2710
|
import { join as join7 } from "path";
|
|
2453
2711
|
import { access } from "fs/promises";
|
|
2454
2712
|
import { constants } from "fs";
|
|
2455
|
-
|
|
2456
|
-
// src/config.ts
|
|
2457
|
-
init_providers();
|
|
2458
|
-
import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
|
|
2459
|
-
import { join as join6, dirname as dirname3 } from "path";
|
|
2460
|
-
|
|
2461
|
-
// src/config-prompts.ts
|
|
2462
|
-
init_logger();
|
|
2463
|
-
import { select, confirm, input as input2 } from "@inquirer/prompts";
|
|
2464
|
-
import chalk3 from "chalk";
|
|
2465
|
-
init_providers();
|
|
2466
|
-
async function runInteractiveConfigWizard(configDir) {
|
|
2467
|
-
console.log();
|
|
2468
|
-
log.info(chalk3.bold("Dispatch Configuration Wizard"));
|
|
2469
|
-
console.log();
|
|
2470
|
-
const existing = await loadConfig(configDir);
|
|
2471
|
-
const hasExisting = Object.keys(existing).length > 0;
|
|
2472
|
-
if (hasExisting) {
|
|
2473
|
-
log.dim("Current configuration:");
|
|
2474
|
-
for (const [key, value] of Object.entries(existing)) {
|
|
2475
|
-
if (value !== void 0) {
|
|
2476
|
-
log.dim(` ${key} = ${value}`);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
console.log();
|
|
2480
|
-
const reconfigure = await confirm({
|
|
2481
|
-
message: "Do you want to reconfigure?",
|
|
2482
|
-
default: true
|
|
2483
|
-
});
|
|
2484
|
-
if (!reconfigure) {
|
|
2485
|
-
log.dim("Configuration unchanged.");
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
console.log();
|
|
2489
|
-
}
|
|
2490
|
-
const installStatuses = await Promise.all(
|
|
2491
|
-
PROVIDER_NAMES.map((name) => checkProviderInstalled(name))
|
|
2492
|
-
);
|
|
2493
|
-
const provider = await select({
|
|
2494
|
-
message: "Select a provider:",
|
|
2495
|
-
choices: PROVIDER_NAMES.map((name, i) => ({
|
|
2496
|
-
name: `${installStatuses[i] ? chalk3.green("\u25CF") : chalk3.red("\u25CF")} ${name}`,
|
|
2497
|
-
value: name
|
|
2498
|
-
})),
|
|
2499
|
-
default: existing.provider
|
|
2500
|
-
});
|
|
2501
|
-
let selectedModel = existing.model;
|
|
2502
|
-
try {
|
|
2503
|
-
log.dim("Fetching available models...");
|
|
2504
|
-
const models = await listProviderModels(provider);
|
|
2505
|
-
if (models.length > 0) {
|
|
2506
|
-
const modelChoice = await select({
|
|
2507
|
-
message: "Select a model:",
|
|
2508
|
-
choices: [
|
|
2509
|
-
{ name: "default (provider decides)", value: "" },
|
|
2510
|
-
...models.map((m) => ({ name: m, value: m }))
|
|
2511
|
-
],
|
|
2512
|
-
default: existing.model ?? ""
|
|
2513
|
-
});
|
|
2514
|
-
selectedModel = modelChoice || void 0;
|
|
2515
|
-
} else {
|
|
2516
|
-
log.dim("No models returned by provider \u2014 skipping model selection.");
|
|
2517
|
-
selectedModel = existing.model;
|
|
2518
|
-
}
|
|
2519
|
-
} catch {
|
|
2520
|
-
log.dim("Could not list models (provider may not be running) \u2014 skipping model selection.");
|
|
2521
|
-
selectedModel = existing.model;
|
|
2522
|
-
}
|
|
2523
|
-
const detectedSource = await detectDatasource(process.cwd());
|
|
2524
|
-
const datasourceDefault = existing.source ?? "auto";
|
|
2525
|
-
if (detectedSource) {
|
|
2526
|
-
log.info(
|
|
2527
|
-
`Detected datasource ${chalk3.cyan(detectedSource)} from git remote`
|
|
2528
|
-
);
|
|
2529
|
-
}
|
|
2530
|
-
const selectedSource = await select({
|
|
2531
|
-
message: "Select a datasource:",
|
|
2532
|
-
choices: [
|
|
2533
|
-
{
|
|
2534
|
-
name: "auto",
|
|
2535
|
-
value: "auto",
|
|
2536
|
-
description: "detect from git remote at runtime"
|
|
2537
|
-
},
|
|
2538
|
-
...DATASOURCE_NAMES.map((name) => ({ name, value: name }))
|
|
2539
|
-
],
|
|
2540
|
-
default: datasourceDefault
|
|
2541
|
-
});
|
|
2542
|
-
const source = selectedSource === "auto" ? void 0 : selectedSource;
|
|
2543
|
-
let org;
|
|
2544
|
-
let project;
|
|
2545
|
-
let workItemType;
|
|
2546
|
-
let iteration;
|
|
2547
|
-
let area;
|
|
2548
|
-
const effectiveSource = source ?? detectedSource;
|
|
2549
|
-
if (effectiveSource === "azdevops") {
|
|
2550
|
-
let defaultOrg = existing.org ?? "";
|
|
2551
|
-
let defaultProject = existing.project ?? "";
|
|
2552
|
-
try {
|
|
2553
|
-
const remoteUrl = await getGitRemoteUrl(process.cwd());
|
|
2554
|
-
if (remoteUrl) {
|
|
2555
|
-
const parsed = parseAzDevOpsRemoteUrl(remoteUrl);
|
|
2556
|
-
if (parsed) {
|
|
2557
|
-
if (!defaultOrg) defaultOrg = parsed.orgUrl;
|
|
2558
|
-
if (!defaultProject) defaultProject = parsed.project;
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
} catch {
|
|
2562
|
-
}
|
|
2563
|
-
console.log();
|
|
2564
|
-
log.info(chalk3.bold("Azure DevOps settings") + chalk3.dim(" (leave empty to skip):"));
|
|
2565
|
-
const orgInput = await input2({
|
|
2566
|
-
message: "Organization URL:",
|
|
2567
|
-
default: defaultOrg || void 0
|
|
2568
|
-
});
|
|
2569
|
-
if (orgInput.trim()) org = orgInput.trim();
|
|
2570
|
-
const projectInput = await input2({
|
|
2571
|
-
message: "Project name:",
|
|
2572
|
-
default: defaultProject || void 0
|
|
2573
|
-
});
|
|
2574
|
-
if (projectInput.trim()) project = projectInput.trim();
|
|
2575
|
-
const workItemTypeInput = await input2({
|
|
2576
|
-
message: "Work item type (e.g. User Story, Bug):",
|
|
2577
|
-
default: existing.workItemType ?? void 0
|
|
2578
|
-
});
|
|
2579
|
-
if (workItemTypeInput.trim()) workItemType = workItemTypeInput.trim();
|
|
2580
|
-
const iterationInput = await input2({
|
|
2581
|
-
message: "Iteration path (e.g. MyProject\\Sprint 1, or @CurrentIteration):",
|
|
2582
|
-
default: existing.iteration ?? void 0
|
|
2583
|
-
});
|
|
2584
|
-
if (iterationInput.trim()) iteration = iterationInput.trim();
|
|
2585
|
-
const areaInput = await input2({
|
|
2586
|
-
message: "Area path (e.g. MyProject\\Team A):",
|
|
2587
|
-
default: existing.area ?? void 0
|
|
2588
|
-
});
|
|
2589
|
-
if (areaInput.trim()) area = areaInput.trim();
|
|
2590
|
-
}
|
|
2591
|
-
const newConfig = {
|
|
2592
|
-
provider,
|
|
2593
|
-
source
|
|
2594
|
-
};
|
|
2595
|
-
if (selectedModel !== void 0) {
|
|
2596
|
-
newConfig.model = selectedModel;
|
|
2597
|
-
}
|
|
2598
|
-
if (org !== void 0) newConfig.org = org;
|
|
2599
|
-
if (project !== void 0) newConfig.project = project;
|
|
2600
|
-
if (workItemType !== void 0) newConfig.workItemType = workItemType;
|
|
2601
|
-
if (iteration !== void 0) newConfig.iteration = iteration;
|
|
2602
|
-
if (area !== void 0) newConfig.area = area;
|
|
2603
|
-
console.log();
|
|
2604
|
-
log.info(chalk3.bold("Configuration summary:"));
|
|
2605
|
-
for (const [key, value] of Object.entries(newConfig)) {
|
|
2606
|
-
if (value !== void 0) {
|
|
2607
|
-
console.log(` ${chalk3.cyan(key)} = ${value}`);
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
if (selectedSource === "auto") {
|
|
2611
|
-
console.log(
|
|
2612
|
-
` ${chalk3.cyan("source")} = auto (detect from git remote at runtime)`
|
|
2613
|
-
);
|
|
2614
|
-
}
|
|
2615
|
-
console.log();
|
|
2616
|
-
const shouldSave = await confirm({
|
|
2617
|
-
message: "Save this configuration?",
|
|
2618
|
-
default: true
|
|
2619
|
-
});
|
|
2620
|
-
if (shouldSave) {
|
|
2621
|
-
await saveConfig(newConfig, configDir);
|
|
2622
|
-
log.success("Configuration saved.");
|
|
2623
|
-
} else {
|
|
2624
|
-
log.dim("Configuration not saved.");
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
// src/config.ts
|
|
2629
|
-
var CONFIG_BOUNDS = {
|
|
2630
|
-
testTimeout: { min: 1, max: 120 },
|
|
2631
|
-
planTimeout: { min: 1, max: 120 },
|
|
2632
|
-
concurrency: { min: 1, max: 64 }
|
|
2633
|
-
};
|
|
2634
|
-
var CONFIG_KEYS = ["provider", "model", "source", "testTimeout", "planTimeout", "concurrency", "org", "project", "workItemType", "iteration", "area"];
|
|
2635
|
-
function getConfigPath(configDir) {
|
|
2636
|
-
const dir = configDir ?? join6(process.cwd(), ".dispatch");
|
|
2637
|
-
return join6(dir, "config.json");
|
|
2638
|
-
}
|
|
2639
|
-
async function loadConfig(configDir) {
|
|
2640
|
-
const configPath = getConfigPath(configDir);
|
|
2641
|
-
try {
|
|
2642
|
-
const raw = await readFile3(configPath, "utf-8");
|
|
2643
|
-
return JSON.parse(raw);
|
|
2644
|
-
} catch {
|
|
2645
|
-
return {};
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
async function saveConfig(config, configDir) {
|
|
2649
|
-
const configPath = getConfigPath(configDir);
|
|
2650
|
-
await mkdir2(dirname3(configPath), { recursive: true });
|
|
2651
|
-
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2652
|
-
}
|
|
2653
|
-
async function handleConfigCommand(_argv, configDir) {
|
|
2654
|
-
await runInteractiveConfigWizard(configDir);
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
// src/orchestrator/cli-config.ts
|
|
2658
2713
|
var CONFIG_TO_CLI = {
|
|
2659
2714
|
provider: "provider",
|
|
2660
2715
|
model: "model",
|
|
@@ -3331,7 +3386,15 @@ async function generateSpecsBatch(validItems, items, specAgent, instance, isTrac
|
|
|
3331
3386
|
log.success(`Deleted local spec ${filepath} (now tracked as issue #${id})`);
|
|
3332
3387
|
identifier = id;
|
|
3333
3388
|
issueNumbers.push(id);
|
|
3334
|
-
} 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 {
|
|
3335
3398
|
const created = await datasource4.create(details.title, result.data.content, fetchOpts);
|
|
3336
3399
|
log.success(`Created issue #${created.number} from ${filepath}`);
|
|
3337
3400
|
await unlink2(filepath);
|
|
@@ -5475,7 +5538,7 @@ async function main() {
|
|
|
5475
5538
|
process.exit(0);
|
|
5476
5539
|
}
|
|
5477
5540
|
if (args.version) {
|
|
5478
|
-
console.log(`dispatch v${"1.4.
|
|
5541
|
+
console.log(`dispatch v${"1.4.2"}`);
|
|
5479
5542
|
process.exit(0);
|
|
5480
5543
|
}
|
|
5481
5544
|
const orchestrator = await boot9({ cwd: args.cwd });
|