@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 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 as randomUUID2 } from "crypto";
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 = randomUUID2();
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 randomUUID3 } from "crypto";
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 = randomUUID3();
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 execFile8 } from "child_process";
781
- import { promisify as promisify8 } from "util";
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 exec8(PROVIDER_BINARIES[name], ["--version"], {
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 exec8, DETECTION_TIMEOUT_MS, PROVIDER_BINARIES;
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
- exec8 = promisify8(execFile8);
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 execFile4 } from "child_process";
1052
- import { promisify as promisify4 } from "util";
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 execFile3 } from "child_process";
1771
- import { readFile, writeFile, readdir, mkdir, rename } from "fs/promises";
1772
- import { basename, dirname as dirname2, isAbsolute, join as join2, parse as parsePath, resolve } from "path";
1773
- import { promisify as promisify3 } from "util";
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/helpers/errors.ts
1777
- var UnsupportedOperationError = class extends Error {
1778
- /** The name of the operation that is not supported. */
1779
- operation;
1780
- constructor(operation, message) {
1781
- const msg = message ?? `Operation not supported: ${operation}`;
1782
- super(msg);
1783
- this.name = "UnsupportedOperationError";
1784
- this.operation = operation;
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
- var exec3 = promisify3(execFile3);
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 join2(cwd, DEFAULT_DIR);
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 join2(resolveDir(opts), filename);
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: join2(dir, filename),
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 false;
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 readFile(filePath, "utf-8");
2050
+ const content = await readFile2(filePath, "utf-8");
1845
2051
  const filename = basename(filePath);
1846
- const dir2 = dirname2(filePath);
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 = join2(dir, filename);
1862
- const content = await readFile(filePath, "utf-8");
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 readFile(filePath, "utf-8");
2084
+ const content = await readFile2(filePath, "utf-8");
1870
2085
  const filename = basename(filePath);
1871
- const dir = dirname2(filePath);
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 = resolveFilePath(issueId, opts);
1876
- await writeFile(filePath, body, "utf-8");
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 = resolveFilePath(issueId, opts);
2094
+ const filePath = await resolveNumericFilePath(issueId, opts);
1880
2095
  const filename = basename(filePath);
1881
- const archiveDir = join2(dirname2(filePath), "archive");
1882
- await mkdir(archiveDir, { recursive: true });
1883
- await rename(filePath, join2(archiveDir, filename));
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 mkdir(dir, { recursive: true });
1888
- const filename = `${slugify(title)}.md`;
1889
- const filePath = join2(dir, filename);
1890
- await writeFile(filePath, body, "utf-8");
1891
- return toIssueDetails(filename, body, dir);
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(_opts) {
1894
- return "main";
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 exec3("git", ["config", "user.name"], { cwd: opts.cwd, shell: process.platform === "win32" });
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(_branchName, _opts) {
1911
- throw new UnsupportedOperationError("createAndSwitchBranch");
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(_branchName, _opts) {
1914
- throw new UnsupportedOperationError("switchBranch");
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(_message, _opts) {
1920
- throw new UnsupportedOperationError("commitAllChanges");
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
- throw new UnsupportedOperationError("createPullRequest");
2180
+ return "";
1924
2181
  }
1925
2182
  };
1926
2183
 
1927
2184
  // src/datasources/index.ts
1928
- var exec4 = promisify4(execFile4);
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 exec4("git", ["remote", "get-url", "origin"], {
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 join3 } from "path";
2116
- import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
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 execFile5 } from "child_process";
2119
- import { promisify as promisify5 } from "util";
2120
- var exec5 = promisify5(execFile5);
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(join3(tmpdir(), "dispatch-"));
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 filename = `${item.number}-${slug}.md`;
2150
- const filepath = join3(tempDir, filename);
2151
- await writeFile2(filepath, item.body, "utf-8");
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 exec5(
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 exec5(
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 exec5(
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 exec5("git", ["reset", "--soft", mergeBase], { cwd, shell: process.platform === "win32" });
2195
- await exec5("git", ["commit", "-m", message], { cwd, shell: process.platform === "win32" });
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 join4, basename as basename3 } from "path";
2288
- import { execFile as execFile6 } from "child_process";
2289
- import { promisify as promisify6 } from "util";
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 exec6 = promisify6(execFile6);
2550
+ var exec7 = promisify7(execFile7);
2293
2551
  var WORKTREE_DIR = ".dispatch/worktrees";
2294
- async function git2(args, cwd) {
2295
- const { stdout } = await exec6("git", args, { cwd, shell: process.platform === "win32" });
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 = join4(repoRoot, WORKTREE_DIR, name);
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 git2(args, repoRoot);
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 git2(["worktree", "add", worktreePath, branchName], repoRoot);
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 = join4(repoRoot, WORKTREE_DIR, name);
2583
+ const worktreePath = join5(repoRoot, WORKTREE_DIR, name);
2326
2584
  try {
2327
- await git2(["worktree", "remove", worktreePath], repoRoot);
2585
+ await git3(["worktree", "remove", worktreePath], repoRoot);
2328
2586
  } catch {
2329
2587
  try {
2330
- await git2(["worktree", "remove", "--force", worktreePath], repoRoot);
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 git2(["worktree", "prune"], repoRoot);
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 = randomUUID();
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 chalk2 from "chalk";
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 ${chalk2.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2618
+ `This operation will process ${chalk3.bold(String(count))} specs, which exceeds the safety threshold of ${threshold}.`
2361
2619
  );
2362
- const answer = await input({
2363
- message: `Type ${chalk2.bold('"yes"')} to proceed:`
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 execFile7 } from "child_process";
2370
- import { promisify as promisify7 } from "util";
2371
- var exec7 = promisify7(execFile7);
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 exec7("git", ["--version"], { shell: process.platform === "win32" });
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 exec7("gh", ["--version"], { shell: process.platform === "win32" });
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 exec7("az", ["--version"], { shell: process.platform === "win32" });
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 readFile2, writeFile as writeFile3 } from "fs/promises";
2421
- import { join as join5 } from "path";
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 = join5(repoRoot, ".gitignore");
2681
+ const gitignorePath = join6(repoRoot, ".gitignore");
2424
2682
  let contents = "";
2425
2683
  try {
2426
- contents = await readFile2(gitignorePath, "utf8");
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 writeFile3(gitignorePath, `${contents}${separator}${entry}
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 !== "md") {
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.1"}`);
5541
+ console.log(`dispatch v${"1.4.2"}`);
5479
5542
  process.exit(0);
5480
5543
  }
5481
5544
  const orchestrator = await boot9({ cwd: args.cwd });