@joshski/dust 0.1.84 → 0.1.86

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/dust.js CHANGED
@@ -319,7 +319,7 @@ async function loadSettings(cwd, fileSystem) {
319
319
  }
320
320
 
321
321
  // lib/version.ts
322
- var DUST_VERSION = "0.1.84";
322
+ var DUST_VERSION = "0.1.86";
323
323
 
324
324
  // lib/session.ts
325
325
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -1740,7 +1740,7 @@ async function defaultExchangeCode(code, fetchFn = fetch) {
1740
1740
  return body.token;
1741
1741
  }
1742
1742
  async function authenticate(authDeps) {
1743
- const exchange = authDeps.exchangeCode ?? defaultExchangeCode;
1743
+ const exchange = authDeps.exchangeCode ?? ((code) => defaultExchangeCode(code, authDeps.fetch));
1744
1744
  return new Promise((resolve, reject) => {
1745
1745
  let timer = null;
1746
1746
  let serverHandle = null;
@@ -1840,6 +1840,264 @@ function openBrowser(url) {
1840
1840
  nodeSpawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
1841
1841
  }
1842
1842
 
1843
+ // lib/bucket/bucket-state.ts
1844
+ function handleServerMessage(state, message) {
1845
+ const effects = [];
1846
+ effects.push({
1847
+ type: "debugLog",
1848
+ message: `ws message: ${message.type}`
1849
+ });
1850
+ switch (message.type) {
1851
+ case "repository-list": {
1852
+ const repos = message.repositories;
1853
+ effects.push({
1854
+ type: "log",
1855
+ message: `Received repository list (${repos.length} repositories):`,
1856
+ stream: "stdout"
1857
+ });
1858
+ if (repos.length === 0) {
1859
+ effects.push({
1860
+ type: "log",
1861
+ message: " (empty)",
1862
+ stream: "stdout"
1863
+ });
1864
+ } else {
1865
+ for (const r of repos) {
1866
+ effects.push({
1867
+ type: "log",
1868
+ message: ` - name=${r.name}`,
1869
+ stream: "stdout"
1870
+ });
1871
+ effects.push({
1872
+ type: "log",
1873
+ message: ` id=${r.id}`,
1874
+ stream: "stdout"
1875
+ });
1876
+ effects.push({
1877
+ type: "log",
1878
+ message: ` gitUrl=${r.gitUrl}`,
1879
+ stream: "stdout"
1880
+ });
1881
+ effects.push({
1882
+ type: "log",
1883
+ message: ` gitSshUrl=${r.gitSshUrl ?? "(none)"}`,
1884
+ stream: "stdout"
1885
+ });
1886
+ effects.push({
1887
+ type: "log",
1888
+ message: ` url=${r.url}`,
1889
+ stream: "stdout"
1890
+ });
1891
+ effects.push({
1892
+ type: "log",
1893
+ message: ` hasTask=${r.hasTask}`,
1894
+ stream: "stdout"
1895
+ });
1896
+ if (r.agentProvider) {
1897
+ effects.push({
1898
+ type: "log",
1899
+ message: ` agentProvider=${r.agentProvider}`,
1900
+ stream: "stdout"
1901
+ });
1902
+ }
1903
+ }
1904
+ }
1905
+ effects.push({
1906
+ type: "syncUI",
1907
+ repositories: repos
1908
+ });
1909
+ effects.push({
1910
+ type: "handleRepositoryList",
1911
+ repositories: repos
1912
+ });
1913
+ break;
1914
+ }
1915
+ case "task-available": {
1916
+ const repoName = message.repository;
1917
+ effects.push({
1918
+ type: "log",
1919
+ message: `Received task-available for ${repoName}`,
1920
+ stream: "stdout"
1921
+ });
1922
+ if (state.repositoryNames.includes(repoName)) {
1923
+ effects.push({
1924
+ type: "signalTaskAvailable",
1925
+ repositoryName: repoName
1926
+ });
1927
+ } else {
1928
+ effects.push({
1929
+ type: "log",
1930
+ message: `No repository state found for ${repoName}`,
1931
+ stream: "stderr"
1932
+ });
1933
+ }
1934
+ break;
1935
+ }
1936
+ }
1937
+ return { effects };
1938
+ }
1939
+ function handleMessageParseError(rawData) {
1940
+ return {
1941
+ effects: [
1942
+ {
1943
+ type: "log",
1944
+ message: `Failed to parse WebSocket message: ${rawData}`,
1945
+ stream: "stderr"
1946
+ }
1947
+ ]
1948
+ };
1949
+ }
1950
+ function handleInvalidMessageFormat(rawData) {
1951
+ return {
1952
+ effects: [
1953
+ {
1954
+ type: "log",
1955
+ message: `Invalid WebSocket message format: ${rawData}`,
1956
+ stream: "stderr"
1957
+ }
1958
+ ]
1959
+ };
1960
+ }
1961
+ var KEYS = {
1962
+ UP: "\x1B[A",
1963
+ DOWN: "\x1B[B",
1964
+ RIGHT: "\x1B[C",
1965
+ LEFT: "\x1B[D",
1966
+ PAGE_UP: "\x1B[5~",
1967
+ PAGE_DOWN: "\x1B[6~",
1968
+ HOME: "\x1B[H",
1969
+ END: "\x1B[F",
1970
+ CTRL_C: "\x03"
1971
+ };
1972
+ var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
1973
+ function parseSGRMouse(key) {
1974
+ const match = key.match(SGR_MOUSE_RE);
1975
+ if (!match)
1976
+ return null;
1977
+ return Number.parseInt(match[1], 10);
1978
+ }
1979
+ function handleKeypress(state, key) {
1980
+ const effects = [];
1981
+ const mouseButton = parseSGRMouse(key);
1982
+ if (mouseButton !== null) {
1983
+ if (mouseButton === 64) {
1984
+ effects.push({ type: "scroll", direction: "up" });
1985
+ effects.push({ type: "scroll", direction: "up" });
1986
+ effects.push({ type: "scroll", direction: "up" });
1987
+ } else if (mouseButton === 65) {
1988
+ effects.push({ type: "scroll", direction: "down" });
1989
+ effects.push({ type: "scroll", direction: "down" });
1990
+ effects.push({ type: "scroll", direction: "down" });
1991
+ }
1992
+ return { effects };
1993
+ }
1994
+ switch (key) {
1995
+ case "q":
1996
+ case KEYS.CTRL_C:
1997
+ effects.push({ type: "quit" });
1998
+ break;
1999
+ case KEYS.LEFT:
2000
+ effects.push({ type: "selectPrevious" });
2001
+ break;
2002
+ case KEYS.RIGHT:
2003
+ effects.push({ type: "selectNext" });
2004
+ break;
2005
+ case KEYS.UP:
2006
+ effects.push({ type: "scroll", direction: "up" });
2007
+ break;
2008
+ case KEYS.DOWN:
2009
+ effects.push({ type: "scroll", direction: "down" });
2010
+ break;
2011
+ case KEYS.PAGE_UP:
2012
+ effects.push({ type: "scroll", direction: "pageUp" });
2013
+ break;
2014
+ case KEYS.PAGE_DOWN:
2015
+ effects.push({ type: "scroll", direction: "pageDown" });
2016
+ break;
2017
+ case "g":
2018
+ case KEYS.HOME:
2019
+ effects.push({ type: "scroll", direction: "top" });
2020
+ break;
2021
+ case "G":
2022
+ case KEYS.END:
2023
+ effects.push({ type: "scroll", direction: "bottom" });
2024
+ break;
2025
+ case "o": {
2026
+ if (state.selectedIndex === -1) {
2027
+ break;
2028
+ }
2029
+ const repoName = state.repositories[state.selectedIndex];
2030
+ if (!repoName)
2031
+ break;
2032
+ const url = state.repositoryUrls[repoName];
2033
+ if (url) {
2034
+ effects.push({ type: "openBrowser", url });
2035
+ }
2036
+ break;
2037
+ }
2038
+ }
2039
+ return { effects };
2040
+ }
2041
+ var INITIAL_RECONNECT_DELAY_MS = 1000;
2042
+ var MAX_RECONNECT_DELAY_MS = 30000;
2043
+ function handleClose(state, code, reason) {
2044
+ const effects = [];
2045
+ effects.push({
2046
+ type: "log",
2047
+ message: `bucket.disconnected code=${code} reason=${reason || "none"}`,
2048
+ stream: "stdout"
2049
+ });
2050
+ if (code === 4000) {
2051
+ effects.push({
2052
+ type: "log",
2053
+ message: "Another connection replaced this one. Not reconnecting.",
2054
+ stream: "stdout"
2055
+ });
2056
+ return { state, effects };
2057
+ }
2058
+ if (!state.shuttingDown) {
2059
+ effects.push({
2060
+ type: "log",
2061
+ message: `Reconnecting in ${state.reconnectDelay / 1000} seconds...`,
2062
+ stream: "stdout"
2063
+ });
2064
+ effects.push({
2065
+ type: "scheduleReconnect",
2066
+ delayMs: state.reconnectDelay
2067
+ });
2068
+ const nextDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
2069
+ return {
2070
+ state: { ...state, reconnectDelay: nextDelay },
2071
+ effects
2072
+ };
2073
+ }
2074
+ return { state, effects };
2075
+ }
2076
+ function handleError(state, errorMessage) {
2077
+ return {
2078
+ state,
2079
+ effects: [
2080
+ {
2081
+ type: "log",
2082
+ message: `WebSocket error: ${errorMessage}`,
2083
+ stream: "stderr"
2084
+ }
2085
+ ]
2086
+ };
2087
+ }
2088
+ function handleOpen(state) {
2089
+ return {
2090
+ state: { ...state, reconnectDelay: INITIAL_RECONNECT_DELAY_MS },
2091
+ effects: [
2092
+ {
2093
+ type: "log",
2094
+ message: "bucket.connected",
2095
+ stream: "stdout"
2096
+ }
2097
+ ]
2098
+ };
2099
+ }
2100
+
1843
2101
  // lib/bucket/events.ts
1844
2102
  var WS_OPEN = 1;
1845
2103
  function formatBucketEvent(event) {
@@ -1965,7 +2223,8 @@ class FileSink {
1965
2223
 
1966
2224
  // lib/logging/index.ts
1967
2225
  var DUST_LOG_FILE = "DUST_LOG_FILE";
1968
- function createLoggingService() {
2226
+ function createLoggingService(options) {
2227
+ const writeStdout = options?.stdout ?? process.stdout.write.bind(process.stdout);
1969
2228
  let patterns = null;
1970
2229
  let initialized = false;
1971
2230
  let activeFileSink = null;
@@ -1995,12 +2254,12 @@ function createLoggingService() {
1995
2254
  }
1996
2255
  activeFileSink = sinkForTesting ?? new FileSink(path);
1997
2256
  },
1998
- createLogger(name, options) {
2257
+ createLogger(name, options2) {
1999
2258
  let perLoggerSink;
2000
- if (options?.file === false) {
2259
+ if (options2?.file === false) {
2001
2260
  perLoggerSink = null;
2002
- } else if (typeof options?.file === "string") {
2003
- perLoggerSink = getOrCreateFileSink(options.file);
2261
+ } else if (typeof options2?.file === "string") {
2262
+ perLoggerSink = getOrCreateFileSink(options2.file);
2004
2263
  }
2005
2264
  return (...messages) => {
2006
2265
  init();
@@ -2013,7 +2272,7 @@ function createLoggingService() {
2013
2272
  activeFileSink.write(line);
2014
2273
  }
2015
2274
  if (patterns && matchesAny(name, patterns)) {
2016
- process.stdout.write(line);
2275
+ writeStdout(line);
2017
2276
  }
2018
2277
  };
2019
2278
  },
@@ -2548,22 +2807,21 @@ async function removeRepository(path, spawn, context) {
2548
2807
  // lib/bucket/repository-loop.ts
2549
2808
  import { existsSync as fsExistsSync } from "node:fs";
2550
2809
  import os2 from "node:os";
2551
- import path3 from "node:path";
2552
2810
 
2553
2811
  // lib/agent-events.ts
2554
- function rawEventToAgentEvent(rawEvent) {
2812
+ function rawEventToAgentEvent(rawEvent, provider) {
2555
2813
  if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
2556
2814
  return { type: "agent-session-activity" };
2557
2815
  }
2558
- return { type: "claude-event", rawEvent };
2816
+ return { type: "agent-event", provider, rawEvent };
2559
2817
  }
2560
2818
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
2561
- function createHeartbeatThrottler(onAgentEvent, options) {
2819
+ function createHeartbeatThrottler(onAgentEvent, provider, options) {
2562
2820
  const intervalMs = options?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
2563
2821
  const now = options?.now ?? Date.now;
2564
2822
  let lastHeartbeatTime;
2565
2823
  return (rawEvent) => {
2566
- const event = rawEventToAgentEvent(rawEvent);
2824
+ const event = rawEventToAgentEvent(rawEvent, provider);
2567
2825
  if (event.type === "agent-session-activity") {
2568
2826
  const currentTime = now();
2569
2827
  if (lastHeartbeatTime !== undefined && currentTime - lastHeartbeatTime < intervalMs) {
@@ -2588,7 +2846,7 @@ function formatAgentEvent(event) {
2588
2846
  case "agent-session-ended":
2589
2847
  return event.success ? "\uD83E\uDD16 Agent session ended (success)" : `\uD83E\uDD16 Agent session ended (error: ${event.error})`;
2590
2848
  case "agent-session-activity":
2591
- case "claude-event":
2849
+ case "agent-event":
2592
2850
  return null;
2593
2851
  }
2594
2852
  }
@@ -2597,7 +2855,6 @@ function formatAgentEvent(event) {
2597
2855
  import { spawn as nodeSpawn3 } from "node:child_process";
2598
2856
  import { existsSync } from "node:fs";
2599
2857
  import os from "node:os";
2600
- import path2 from "node:path";
2601
2858
 
2602
2859
  // lib/docker/docker-agent.ts
2603
2860
  import path from "node:path";
@@ -2655,6 +2912,36 @@ function hasDockerfile(repoPath, dependencies) {
2655
2912
  const dockerfilePath = path.join(repoPath, ".dust", "Dockerfile");
2656
2913
  return dependencies.existsSync(dockerfilePath);
2657
2914
  }
2915
+ async function prepareDockerConfig(repoPath, dependencies, onEvent) {
2916
+ log(`checking for .dust/Dockerfile in ${repoPath}`);
2917
+ if (!hasDockerfile(repoPath, dependencies)) {
2918
+ log("no .dust/Dockerfile found, running without Docker");
2919
+ return {};
2920
+ }
2921
+ const imageTag = generateImageTag(repoPath);
2922
+ log(`Dockerfile found, image tag: ${imageTag}`);
2923
+ onEvent({ type: "loop.docker_detected", imageTag });
2924
+ if (!await isDockerAvailable(dependencies)) {
2925
+ const error = "Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.";
2926
+ return { error };
2927
+ }
2928
+ onEvent({ type: "loop.docker_building", imageTag });
2929
+ const buildResult = await buildDockerImage({ repoPath, imageTag }, dependencies);
2930
+ if (!buildResult.success) {
2931
+ onEvent({ type: "loop.docker_error", error: buildResult.error });
2932
+ return { error: buildResult.error };
2933
+ }
2934
+ onEvent({ type: "loop.docker_built", imageTag });
2935
+ const homeDir = dependencies.homedir();
2936
+ const config = {
2937
+ imageTag,
2938
+ repoPath,
2939
+ homeDir,
2940
+ hasGitconfig: dependencies.existsSync(path.join(homeDir, ".gitconfig"))
2941
+ };
2942
+ log(`Docker config ready: ${JSON.stringify(config)}`);
2943
+ return { config };
2944
+ }
2658
2945
 
2659
2946
  // lib/artifacts/workflow-tasks.ts
2660
2947
  var IDEA_TRANSITION_PREFIXES = [
@@ -3181,36 +3468,17 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3181
3468
  homedir: loopDependencies.dockerDeps?.homedir ?? os.homedir,
3182
3469
  existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
3183
3470
  };
3184
- log2(`checking for .dust/Dockerfile in ${context.cwd}`);
3185
- if (hasDockerfile(context.cwd, dockerDeps)) {
3186
- const imageTag = generateImageTag(context.cwd);
3187
- log2(`Dockerfile found, image tag: ${imageTag}`);
3188
- onLoopEvent({ type: "loop.docker_detected", imageTag });
3189
- if (!await isDockerAvailable(dockerDeps)) {
3190
- context.stderr("Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.");
3191
- return { exitCode: 1 };
3192
- }
3193
- onLoopEvent({ type: "loop.docker_building", imageTag });
3194
- const buildResult = await buildDockerImage({ repoPath: context.cwd, imageTag }, dockerDeps);
3195
- if (!buildResult.success) {
3196
- onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3197
- context.stderr(buildResult.error);
3198
- return { exitCode: 1 };
3199
- }
3200
- onLoopEvent({ type: "loop.docker_built", imageTag });
3201
- const homeDir = os.homedir();
3202
- dockerConfig = {
3203
- imageTag,
3204
- repoPath: context.cwd,
3205
- homeDir,
3206
- hasGitconfig: existsSync(path2.join(homeDir, ".gitconfig"))
3207
- };
3471
+ const dockerResult = await prepareDockerConfig(context.cwd, dockerDeps, onLoopEvent);
3472
+ if ("error" in dockerResult) {
3473
+ context.stderr(dockerResult.error);
3474
+ return { exitCode: 1 };
3475
+ }
3476
+ if ("config" in dockerResult) {
3477
+ dockerConfig = dockerResult.config;
3208
3478
  if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3209
3479
  context.stderr("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.");
3210
3480
  return { exitCode: 1 };
3211
3481
  }
3212
- } else {
3213
- log2("no .dust/Dockerfile found, running without Docker");
3214
3482
  }
3215
3483
  log2(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
3216
3484
  onLoopEvent({ type: "loop.warning" });
@@ -3227,7 +3495,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
3227
3495
  docker: dockerConfig
3228
3496
  };
3229
3497
  if (eventsUrl) {
3230
- iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
3498
+ iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent, loopDependencies.agentType ?? "claude");
3231
3499
  }
3232
3500
  while (completedIterations < maxIterations) {
3233
3501
  agentSessionId = crypto.randomUUID();
@@ -3287,6 +3555,7 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
3287
3555
  });
3288
3556
  proc.on("error", reject);
3289
3557
  });
3558
+ closePromise.catch(() => {});
3290
3559
  const abortHandler = () => {
3291
3560
  if (!proc.killed) {
3292
3561
  proc.kill();
@@ -3439,9 +3708,15 @@ function createBufferStdoutSink(loopState, logBuffer) {
3439
3708
  }
3440
3709
  };
3441
3710
  }
3711
+ function createStdoutSinkFactory(loopState, logBuffer) {
3712
+ return () => createBufferStdoutSink(loopState, logBuffer);
3713
+ }
3442
3714
  function createBufferRun(run3, bufferSinkDeps) {
3443
3715
  return (prompt, options) => run3(prompt, options, bufferSinkDeps);
3444
3716
  }
3717
+ function createCodexBufferRun(run3, codexBufferSinkDeps) {
3718
+ return (prompt, options) => run3(prompt, options, codexBufferSinkDeps);
3719
+ }
3445
3720
  async function noOpPostEvent() {}
3446
3721
  function createLoopEventHandler(logBuffer) {
3447
3722
  return function onLoopEvent(event) {
@@ -3508,30 +3783,6 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3508
3783
  sequence: 0,
3509
3784
  agentSessionId: undefined
3510
3785
  };
3511
- const isCodex = repoState.repository.agentProvider === "codex";
3512
- const agentType = isCodex ? "codex" : "claude";
3513
- const createStdoutSink2 = () => createBufferStdoutSink(loopState, repoState.logBuffer);
3514
- let bufferRun;
3515
- if (isCodex) {
3516
- const codexBufferSinkDeps = {
3517
- ...defaultRunnerDependencies2,
3518
- createStdoutSink: createStdoutSink2
3519
- };
3520
- bufferRun = (prompt, options) => run2(prompt, options, codexBufferSinkDeps);
3521
- } else {
3522
- const bufferSinkDeps = {
3523
- ...defaultRunnerDependencies,
3524
- createStdoutSink: createStdoutSink2
3525
- };
3526
- bufferRun = createBufferRun(run3, bufferSinkDeps);
3527
- }
3528
- const loopDeps = {
3529
- spawn,
3530
- run: bufferRun,
3531
- sleep,
3532
- postEvent: noOpPostEvent,
3533
- agentType
3534
- };
3535
3786
  const onLoopEvent = createLoopEventHandler(repoState.logBuffer);
3536
3787
  const onAgentEvent = createAgentEventHandler({
3537
3788
  repoState,
@@ -3547,43 +3798,53 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3547
3798
  homedir: repoDeps.dockerDeps?.homedir ?? os2.homedir,
3548
3799
  existsSync: repoDeps.dockerDeps?.existsSync ?? fsExistsSync
3549
3800
  };
3550
- log3(`checking for .dust/Dockerfile in ${repoState.path}`);
3551
- if (hasDockerfile(repoState.path, dockerDeps)) {
3552
- const imageTag = generateImageTag(repoState.path);
3553
- log3(`Dockerfile found, image tag: ${imageTag}`);
3554
- onLoopEvent({ type: "loop.docker_detected", imageTag });
3555
- if (!await isDockerAvailable(dockerDeps)) {
3556
- log3("Docker not available");
3557
- appendLogLine(repoState.logBuffer, createLogLine("Docker not available. Install Docker or remove .dust/Dockerfile.", "stderr"));
3558
- } else {
3559
- onLoopEvent({ type: "loop.docker_building", imageTag });
3560
- const buildResult = await buildDockerImage({ repoPath: repoState.path, imageTag }, dockerDeps);
3561
- if (!buildResult.success) {
3562
- onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3563
- log3(`Docker build failed: ${buildResult.error}`);
3564
- } else {
3565
- onLoopEvent({ type: "loop.docker_built", imageTag });
3566
- const homeDir = dockerDeps.homedir();
3567
- dockerConfig = {
3568
- imageTag,
3569
- repoPath: repoState.path,
3570
- homeDir,
3571
- hasGitconfig: dockerDeps.existsSync(path3.join(homeDir, ".gitconfig"))
3572
- };
3573
- log3(`Docker config ready: ${JSON.stringify(dockerConfig)}`);
3574
- if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3575
- log3("CLAUDE_CODE_OAUTH_TOKEN is not set, cannot run in Docker mode");
3576
- appendLogLine(repoState.logBuffer, createLogLine("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.", "stderr"));
3577
- return;
3578
- }
3579
- }
3801
+ const dockerResult = await prepareDockerConfig(repoState.path, dockerDeps, onLoopEvent);
3802
+ if ("error" in dockerResult) {
3803
+ log3(`Docker error: ${dockerResult.error}`);
3804
+ appendLogLine(repoState.logBuffer, createLogLine(dockerResult.error, "stderr"));
3805
+ } else if ("config" in dockerResult) {
3806
+ if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
3807
+ log3("CLAUDE_CODE_OAUTH_TOKEN is not set, cannot run in Docker mode");
3808
+ appendLogLine(repoState.logBuffer, createLogLine("Docker mode requires CLAUDE_CODE_OAUTH_TOKEN. Run `claude setup-token` and export the token.", "stderr"));
3809
+ return;
3580
3810
  }
3581
- } else {
3582
- log3("no .dust/Dockerfile found, running without Docker");
3811
+ dockerConfig = dockerResult.config;
3583
3812
  }
3584
3813
  log3(`loop started for ${repoName} at ${repoState.path}`);
3585
3814
  while (!repoState.stopRequested) {
3586
3815
  loopState.agentSessionId = crypto.randomUUID();
3816
+ const isCodex = repoState.repository.agentProvider === "codex";
3817
+ const agentType = isCodex ? "codex" : "claude";
3818
+ log3(`${repoName}: agentProvider=${repoState.repository.agentProvider ?? "(unset)"}, using ${agentType}`);
3819
+ const createStdoutSink2 = createStdoutSinkFactory(loopState, repoState.logBuffer);
3820
+ let bufferRun;
3821
+ if (isCodex) {
3822
+ const codexBufferSinkDeps = {
3823
+ ...defaultRunnerDependencies2,
3824
+ spawnCodex: (prompt, options = {}) => {
3825
+ const spawnDeps = {
3826
+ ...defaultDependencies2,
3827
+ spawn
3828
+ };
3829
+ return defaultRunnerDependencies2.spawnCodex(prompt, options, spawnDeps);
3830
+ },
3831
+ createStdoutSink: createStdoutSink2
3832
+ };
3833
+ bufferRun = createCodexBufferRun(run2, codexBufferSinkDeps);
3834
+ } else {
3835
+ const bufferSinkDeps = {
3836
+ ...defaultRunnerDependencies,
3837
+ createStdoutSink: createStdoutSink2
3838
+ };
3839
+ bufferRun = createBufferRun(run3, bufferSinkDeps);
3840
+ }
3841
+ const loopDeps = {
3842
+ spawn,
3843
+ run: bufferRun,
3844
+ sleep,
3845
+ postEvent: noOpPostEvent,
3846
+ agentType
3847
+ };
3587
3848
  const abortController = new AbortController;
3588
3849
  const cancelCurrentIteration = createCancelHandler(abortController);
3589
3850
  repoState.cancelCurrentIteration = cancelCurrentIteration;
@@ -3593,7 +3854,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3593
3854
  hooksInstalled,
3594
3855
  signal: abortController.signal,
3595
3856
  repositoryId: repoState.repository.id.toString(),
3596
- onRawEvent: createHeartbeatThrottler(onAgentEvent),
3857
+ onRawEvent: createHeartbeatThrottler(onAgentEvent, agentType),
3597
3858
  docker: dockerConfig
3598
3859
  });
3599
3860
  } catch (error) {
@@ -3653,7 +3914,8 @@ function parseRepository(data) {
3653
3914
  gitUrl: repositoryData.gitUrl,
3654
3915
  gitSshUrl: typeof repositoryData.gitSshUrl === "string" ? repositoryData.gitSshUrl : undefined,
3655
3916
  url: repositoryData.url,
3656
- id: repositoryData.id
3917
+ id: repositoryData.id,
3918
+ agentProvider: typeof repositoryData.agentProvider === "string" ? repositoryData.agentProvider : undefined
3657
3919
  };
3658
3920
  }
3659
3921
  }
@@ -3729,8 +3991,14 @@ async function handleRepositoryList(repositories, manager, repoDeps, context) {
3729
3991
  }
3730
3992
  }
3731
3993
  for (const [name, repo] of incomingRepos) {
3732
- if (!manager.repositories.has(name)) {
3994
+ const existing = manager.repositories.get(name);
3995
+ if (!existing) {
3733
3996
  await addRepository(repo, manager, repoDeps, context);
3997
+ } else if (existing.repository.agentProvider !== repo.agentProvider) {
3998
+ const from = existing.repository.agentProvider ?? "(unset)";
3999
+ const to = repo.agentProvider ?? "(unset)";
4000
+ log4(`${name}: agentProvider changed from ${from} to ${to}`);
4001
+ existing.repository.agentProvider = repo.agentProvider;
3734
4002
  }
3735
4003
  }
3736
4004
  for (const name of manager.repositories.keys()) {
@@ -3843,18 +4111,19 @@ function visibleLength(text) {
3843
4111
  function truncateLine(text, maxWidth) {
3844
4112
  if (maxWidth <= 0)
3845
4113
  return "";
3846
- if (visibleLength(text) <= maxWidth)
4114
+ const textLength = visibleLength(text);
4115
+ if (textLength <= maxWidth)
3847
4116
  return text;
3848
4117
  const ansiRegex = /\x1b\[[0-9;]*m/g;
4118
+ const truncateAt = maxWidth - CHARS.ellipsis.length;
3849
4119
  let visibleCount = 0;
3850
4120
  let result = "";
3851
4121
  let lastIndex = 0;
3852
4122
  for (let match = ansiRegex.exec(text);match !== null; match = ansiRegex.exec(text)) {
3853
4123
  const textBefore = text.slice(lastIndex, match.index);
3854
4124
  for (const char of textBefore) {
3855
- if (visibleCount >= maxWidth - CHARS.ellipsis.length) {
3856
- result += CHARS.ellipsis;
3857
- return result + ANSI.RESET;
4125
+ if (visibleCount >= truncateAt) {
4126
+ return result + CHARS.ellipsis + ANSI.RESET;
3858
4127
  }
3859
4128
  result += char;
3860
4129
  visibleCount++;
@@ -3864,14 +4133,13 @@ function truncateLine(text, maxWidth) {
3864
4133
  }
3865
4134
  const remaining = text.slice(lastIndex);
3866
4135
  for (const char of remaining) {
3867
- if (visibleCount >= maxWidth - CHARS.ellipsis.length) {
3868
- result += CHARS.ellipsis;
3869
- return result + ANSI.RESET;
4136
+ if (visibleCount >= truncateAt) {
4137
+ return result + CHARS.ellipsis + ANSI.RESET;
3870
4138
  }
3871
4139
  result += char;
3872
4140
  visibleCount++;
3873
4141
  }
3874
- return result;
4142
+ return result + ANSI.RESET;
3875
4143
  }
3876
4144
  function createTerminalUIState() {
3877
4145
  return {
@@ -4001,7 +4269,7 @@ function getVisibleLogs(state) {
4001
4269
  const buffer2 = state.logBuffers.get(repoName2);
4002
4270
  if (!buffer2)
4003
4271
  continue;
4004
- const color2 = repoColors.get(repoName2) ?? ANSI.FG_WHITE;
4272
+ const color2 = repoColors.get(repoName2);
4005
4273
  const lines = getLogLines(buffer2);
4006
4274
  for (const line of lines) {
4007
4275
  allLogs.push({ ...line, repository: repoName2, color: color2 });
@@ -4136,90 +4404,37 @@ function enterAlternateScreen() {
4136
4404
  function exitAlternateScreen() {
4137
4405
  return `\x1B[?1006l\x1B[?1000l${ANSI.EXIT_ALT_SCREEN}${ANSI.SHOW_CURSOR}`;
4138
4406
  }
4139
- var KEYS = {
4140
- UP: "\x1B[A",
4141
- DOWN: "\x1B[B",
4142
- RIGHT: "\x1B[C",
4143
- LEFT: "\x1B[D",
4144
- PAGE_UP: "\x1B[5~",
4145
- PAGE_DOWN: "\x1B[6~",
4146
- HOME: "\x1B[H",
4147
- END: "\x1B[F",
4148
- CTRL_C: "\x03"
4149
- };
4150
- var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4151
- function parseSGRMouse(key) {
4152
- const match = key.match(SGR_MOUSE_RE);
4153
- if (!match)
4154
- return null;
4155
- return Number.parseInt(match[1], 10);
4156
- }
4157
- function handleKeyInput(state, key, options) {
4158
- const mouseButton = parseSGRMouse(key);
4159
- if (mouseButton !== null) {
4160
- if (mouseButton === 64) {
4161
- scrollUp(state, 3);
4162
- } else if (mouseButton === 65) {
4163
- scrollDown(state, 3);
4164
- }
4165
- return false;
4166
- }
4167
- switch (key) {
4168
- case "q":
4169
- case KEYS.CTRL_C:
4170
- return true;
4171
- case KEYS.LEFT:
4172
- selectPrevious(state);
4173
- state.scrollOffset = 0;
4174
- state.autoScroll = true;
4175
- break;
4176
- case KEYS.RIGHT:
4177
- selectNext(state);
4178
- state.scrollOffset = 0;
4179
- state.autoScroll = true;
4180
- break;
4181
- case KEYS.UP:
4182
- scrollUp(state, 1);
4183
- break;
4184
- case KEYS.DOWN:
4185
- scrollDown(state, 1);
4186
- break;
4187
- case KEYS.PAGE_UP:
4188
- scrollUp(state, getLogAreaHeight(state));
4189
- break;
4190
- case KEYS.PAGE_DOWN:
4191
- scrollDown(state, getLogAreaHeight(state));
4192
- break;
4193
- case "g":
4194
- case KEYS.HOME:
4195
- scrollToTop(state);
4196
- break;
4197
- case "G":
4198
- case KEYS.END:
4199
- scrollToBottom(state);
4200
- break;
4201
- case "o": {
4202
- if (state.selectedIndex === -1) {
4203
- break;
4204
- }
4205
- const repoName = state.repositories[state.selectedIndex];
4206
- if (!repoName)
4207
- break;
4208
- const url = state.repositoryUrls.get(repoName);
4209
- if (url && options?.openBrowser) {
4210
- options.openBrowser(url);
4211
- }
4212
- break;
4213
- }
4214
- }
4215
- return false;
4216
- }
4407
+ var SGR_MOUSE_RE2 = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
4217
4408
 
4218
4409
  // lib/cli/commands/bucket.ts
4219
4410
  var log5 = createLogger("dust:cli:commands:bucket");
4220
4411
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
4221
- var INITIAL_RECONNECT_DELAY_MS = 1000;
4222
- var MAX_RECONNECT_DELAY_MS = 30000;
4412
+ function createAuthFileSystem(dependencies) {
4413
+ return {
4414
+ exists: (path2) => {
4415
+ try {
4416
+ dependencies.accessSync(path2);
4417
+ return true;
4418
+ } catch {
4419
+ return false;
4420
+ }
4421
+ },
4422
+ isDirectory: (path2) => {
4423
+ try {
4424
+ return dependencies.statSync(path2).isDirectory();
4425
+ } catch {
4426
+ return false;
4427
+ }
4428
+ },
4429
+ getFileCreationTime: (path2) => dependencies.statSync(path2).birthtimeMs,
4430
+ readFile: (path2) => dependencies.readFile(path2, "utf8"),
4431
+ writeFile: (path2, content) => dependencies.writeFile(path2, content, "utf8"),
4432
+ mkdir: (path2, options) => dependencies.mkdir(path2, options).then(() => {}),
4433
+ readdir: (path2) => dependencies.readdir(path2),
4434
+ chmod: (path2, mode) => dependencies.chmod(path2, mode),
4435
+ rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4436
+ };
4437
+ }
4223
4438
  function defaultCreateWebSocket(url, token) {
4224
4439
  const ws = new WebSocket(url, {
4225
4440
  headers: {
@@ -4274,32 +4489,6 @@ function defaultGetTerminalSize() {
4274
4489
  function defaultWriteStdout(data) {
4275
4490
  process.stdout.write(data);
4276
4491
  }
4277
- function createAuthFileSystem(dependencies) {
4278
- return {
4279
- exists: (path4) => {
4280
- try {
4281
- dependencies.accessSync(path4);
4282
- return true;
4283
- } catch {
4284
- return false;
4285
- }
4286
- },
4287
- isDirectory: (path4) => {
4288
- try {
4289
- return dependencies.statSync(path4).isDirectory();
4290
- } catch {
4291
- return false;
4292
- }
4293
- },
4294
- getFileCreationTime: (path4) => dependencies.statSync(path4).birthtimeMs,
4295
- readFile: (path4) => dependencies.readFile(path4, "utf8"),
4296
- writeFile: (path4, content) => dependencies.writeFile(path4, content, "utf8"),
4297
- mkdir: (path4, options) => dependencies.mkdir(path4, options).then(() => {}),
4298
- readdir: (path4) => dependencies.readdir(path4),
4299
- chmod: (path4, mode) => dependencies.chmod(path4, mode),
4300
- rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
4301
- };
4302
- }
4303
4492
  function createDefaultBucketDependencies() {
4304
4493
  const authFileSystem = createAuthFileSystem({
4305
4494
  accessSync,
@@ -4423,6 +4612,20 @@ function syncTUI(state) {
4423
4612
  }
4424
4613
  }
4425
4614
  }
4615
+ function handleRepositoryListSuccess(state, repos, repoDeps, context, useTUI) {
4616
+ syncTUI(state);
4617
+ for (const repoData of repos) {
4618
+ if (repoData.hasTask) {
4619
+ const repoState = state.repositories.get(repoData.name);
4620
+ if (repoState) {
4621
+ signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4622
+ }
4623
+ }
4624
+ }
4625
+ }
4626
+ function handleRepositoryListError(state, context, useTUI, error) {
4627
+ logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
4628
+ }
4426
4629
  function logMessage(state, context, useTUI, message, stream = "stdout") {
4427
4630
  if (useTUI) {
4428
4631
  const systemBuffer = state.logBuffers.get("system");
@@ -4470,8 +4673,13 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4470
4673
  state.ws = ws;
4471
4674
  ws.onopen = () => {
4472
4675
  state.emit({ type: "bucket.connected" });
4473
- logMessage(state, context, useTUI, formatBucketEvent({ type: "bucket.connected" }));
4474
- state.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
4676
+ const lifecycleState = {
4677
+ reconnectDelay: state.reconnectDelay,
4678
+ shuttingDown: state.shuttingDown
4679
+ };
4680
+ const result = handleOpen(lifecycleState);
4681
+ state.reconnectDelay = result.state.reconnectDelay;
4682
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4475
4683
  };
4476
4684
  }
4477
4685
  ws.onclose = (event) => {
@@ -4481,80 +4689,110 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4481
4689
  reason: event.reason || "none"
4482
4690
  };
4483
4691
  state.emit(disconnectEvent);
4484
- logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
4485
4692
  state.ws = null;
4486
- if (event.code === 4000) {
4487
- logMessage(state, context, useTUI, "Another connection replaced this one. Not reconnecting.");
4488
- return;
4489
- }
4490
- if (!state.shuttingDown) {
4491
- logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
4492
- state.reconnectTimer = setTimeout(() => {
4493
- connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
4494
- }, state.reconnectDelay);
4495
- state.reconnectDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
4496
- }
4693
+ const lifecycleState = {
4694
+ reconnectDelay: state.reconnectDelay,
4695
+ shuttingDown: state.shuttingDown
4696
+ };
4697
+ const result = handleClose(lifecycleState, event.code, event.reason || "");
4698
+ state.reconnectDelay = result.state.reconnectDelay;
4699
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4497
4700
  };
4498
4701
  ws.onerror = (error) => {
4499
- logMessage(state, context, useTUI, `WebSocket error: ${error.message}`, "stderr");
4702
+ const lifecycleState = {
4703
+ reconnectDelay: state.reconnectDelay,
4704
+ shuttingDown: state.shuttingDown
4705
+ };
4706
+ const result = handleError(lifecycleState, error.message);
4707
+ executeLifecycleEffects(result.effects, { state, context, useTUI, bucketDependencies, fileSystem }, token);
4500
4708
  };
4501
4709
  ws.onmessage = (event) => {
4502
4710
  let rawData;
4503
4711
  try {
4504
4712
  rawData = JSON.parse(event.data);
4505
4713
  } catch {
4506
- logMessage(state, context, useTUI, `Failed to parse WebSocket message: ${event.data}`, "stderr");
4714
+ const result2 = handleMessageParseError(event.data);
4715
+ executeEffects(result2.effects, {
4716
+ state,
4717
+ context,
4718
+ useTUI,
4719
+ bucketDependencies,
4720
+ fileSystem
4721
+ });
4507
4722
  return;
4508
4723
  }
4509
4724
  const message = parseServerMessage(rawData);
4510
4725
  if (!message) {
4511
- logMessage(state, context, useTUI, `Invalid WebSocket message format: ${event.data}`, "stderr");
4726
+ const result2 = handleInvalidMessageFormat(event.data);
4727
+ executeEffects(result2.effects, {
4728
+ state,
4729
+ context,
4730
+ useTUI,
4731
+ bucketDependencies,
4732
+ fileSystem
4733
+ });
4512
4734
  return;
4513
4735
  }
4514
- log5(`ws message: ${message.type}`);
4515
- if (message.type === "repository-list") {
4516
- const repos = message.repositories;
4517
- logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories):`);
4518
- if (repos.length === 0) {
4519
- logMessage(state, context, useTUI, " (empty)");
4520
- } else {
4521
- for (const r of repos) {
4522
- logMessage(state, context, useTUI, ` - name=${r.name}`);
4523
- logMessage(state, context, useTUI, ` id=${r.id}`);
4524
- logMessage(state, context, useTUI, ` gitUrl=${r.gitUrl}`);
4525
- logMessage(state, context, useTUI, ` gitSshUrl=${r.gitSshUrl ?? "(none)"}`);
4526
- logMessage(state, context, useTUI, ` url=${r.url}`);
4527
- logMessage(state, context, useTUI, ` hasTask=${r.hasTask}`);
4528
- }
4736
+ const handlerState = {
4737
+ repositoryNames: Array.from(state.repositories.keys())
4738
+ };
4739
+ const result = handleServerMessage(handlerState, message);
4740
+ executeEffects(result.effects, {
4741
+ state,
4742
+ context,
4743
+ useTUI,
4744
+ bucketDependencies,
4745
+ fileSystem
4746
+ });
4747
+ };
4748
+ }
4749
+ function executeEffects(effects, dependencies) {
4750
+ const { state, context, useTUI, bucketDependencies, fileSystem } = dependencies;
4751
+ for (const effect of effects) {
4752
+ switch (effect.type) {
4753
+ case "log":
4754
+ logMessage(state, context, useTUI, effect.message, effect.stream);
4755
+ break;
4756
+ case "debugLog":
4757
+ log5(effect.message);
4758
+ break;
4759
+ case "syncUI":
4760
+ syncUIWithRepoList(state, effect.repositories);
4761
+ break;
4762
+ case "handleRepositoryList": {
4763
+ const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4764
+ const repoContext = createTUIContext(state, context, useTUI);
4765
+ const repos = effect.repositories;
4766
+ handleRepositoryList(repos, state, repoDeps, repoContext).then(() => handleRepositoryListSuccess(state, repos, repoDeps, context, useTUI)).catch((error) => handleRepositoryListError(state, context, useTUI, error));
4767
+ break;
4529
4768
  }
4530
- syncUIWithRepoList(state, repos);
4531
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4532
- const repoContext = createTUIContext(state, context, useTUI);
4533
- handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
4534
- syncTUI(state);
4535
- for (const repoData of repos) {
4536
- if (repoData.hasTask) {
4537
- const repoState = state.repositories.get(repoData.name);
4538
- if (repoState) {
4539
- signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4540
- }
4541
- }
4769
+ case "signalTaskAvailable": {
4770
+ const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4771
+ const repoState = state.repositories.get(effect.repositoryName);
4772
+ if (repoState) {
4773
+ signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4542
4774
  }
4543
- }).catch((error) => {
4544
- logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
4545
- });
4546
- } else if (message.type === "task-available") {
4547
- const repoName = message.repository;
4548
- const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
4549
- logMessage(state, context, useTUI, `Received task-available for ${repoName}`);
4550
- const repoState = state.repositories.get(repoName);
4551
- if (repoState) {
4552
- signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
4553
- } else {
4554
- logMessage(state, context, useTUI, `No repository state found for ${repoName}`, "stderr");
4775
+ break;
4555
4776
  }
4777
+ case "scheduleReconnect":
4778
+ break;
4556
4779
  }
4557
- };
4780
+ }
4781
+ }
4782
+ function executeLifecycleEffects(effects, dependencies, token) {
4783
+ const { state, context, useTUI, bucketDependencies, fileSystem } = dependencies;
4784
+ for (const effect of effects) {
4785
+ switch (effect.type) {
4786
+ case "log":
4787
+ logMessage(state, context, useTUI, effect.message, effect.stream);
4788
+ break;
4789
+ case "scheduleReconnect":
4790
+ state.reconnectTimer = setTimeout(() => {
4791
+ connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
4792
+ }, effect.delayMs);
4793
+ break;
4794
+ }
4795
+ }
4558
4796
  }
4559
4797
  async function shutdown(state, bucketDeps, context) {
4560
4798
  if (state.shuttingDown)
@@ -4608,17 +4846,80 @@ function setupTUI(state, bucketDeps) {
4608
4846
  }
4609
4847
  };
4610
4848
  }
4849
+ function createKeypressHandlerState(ui) {
4850
+ const repositoryUrls = {};
4851
+ for (const [name, url] of ui.repositoryUrls) {
4852
+ repositoryUrls[name] = url;
4853
+ }
4854
+ return {
4855
+ selectedIndex: ui.selectedIndex,
4856
+ repositories: ui.repositories,
4857
+ repositoryUrls
4858
+ };
4859
+ }
4860
+ function executeKeypressEffects(ui, effects, onQuit, options) {
4861
+ for (const effect of effects) {
4862
+ switch (effect.type) {
4863
+ case "quit":
4864
+ onQuit();
4865
+ break;
4866
+ case "openBrowser":
4867
+ if (options?.openBrowser) {
4868
+ options.openBrowser(effect.url);
4869
+ }
4870
+ break;
4871
+ case "selectNext":
4872
+ selectNext(ui);
4873
+ ui.scrollOffset = 0;
4874
+ ui.autoScroll = true;
4875
+ break;
4876
+ case "selectPrevious":
4877
+ selectPrevious(ui);
4878
+ ui.scrollOffset = 0;
4879
+ ui.autoScroll = true;
4880
+ break;
4881
+ case "scroll":
4882
+ switch (effect.direction) {
4883
+ case "up":
4884
+ scrollUp(ui, 1);
4885
+ break;
4886
+ case "down":
4887
+ scrollDown(ui, 1);
4888
+ break;
4889
+ case "pageUp":
4890
+ scrollUp(ui, getLogAreaHeight(ui));
4891
+ break;
4892
+ case "pageDown":
4893
+ scrollDown(ui, getLogAreaHeight(ui));
4894
+ break;
4895
+ case "top":
4896
+ scrollToTop(ui);
4897
+ break;
4898
+ case "bottom":
4899
+ scrollToBottom(ui);
4900
+ break;
4901
+ }
4902
+ break;
4903
+ }
4904
+ }
4905
+ }
4611
4906
  function createKeypressHandler(useTUI, state, onQuit, options) {
4612
4907
  if (useTUI) {
4613
4908
  return (key) => {
4614
- const shouldQuit = handleKeyInput(state.ui, key, options);
4615
- if (shouldQuit)
4616
- onQuit();
4909
+ const keypressState = createKeypressHandlerState(state.ui);
4910
+ const { effects } = handleKeypress(keypressState, key);
4911
+ executeKeypressEffects(state.ui, effects, onQuit, options);
4617
4912
  };
4618
4913
  }
4619
4914
  return (key) => {
4620
- if (key === "q" || key === "\x03")
4621
- onQuit();
4915
+ const keypressState = createKeypressHandlerState(state.ui);
4916
+ const { effects } = handleKeypress(keypressState, key);
4917
+ for (const effect of effects) {
4918
+ if (effect.type === "quit") {
4919
+ onQuit();
4920
+ break;
4921
+ }
4922
+ }
4622
4923
  };
4623
4924
  }
4624
4925
  async function resolveToken(authDeps, context) {
@@ -4763,16 +5064,16 @@ function createDefaultUploadDependencies() {
4763
5064
  getHomeDir: () => homedir2(),
4764
5065
  fileSystem: authFileSystem
4765
5066
  },
4766
- readFileBytes: async (path4) => {
4767
- const buffer = await Bun.file(path4).arrayBuffer();
5067
+ readFileBytes: async (path2) => {
5068
+ const buffer = await Bun.file(path2).arrayBuffer();
4768
5069
  return new Uint8Array(buffer);
4769
5070
  },
4770
- getFileSize: async (path4) => {
4771
- const file = Bun.file(path4);
5071
+ getFileSize: async (path2) => {
5072
+ const file = Bun.file(path2);
4772
5073
  return file.size;
4773
5074
  },
4774
- fileExists: async (path4) => {
4775
- const file = Bun.file(path4);
5075
+ fileExists: async (path2) => {
5076
+ const file = Bun.file(path2);
4776
5077
  return file.exists();
4777
5078
  },
4778
5079
  uploadFile: async (url, token, fileBytes, contentType, fileName) => {
@@ -4868,7 +5169,8 @@ async function bucketAssetUpload(dependencies, uploadDeps = createDefaultUploadD
4868
5169
  }
4869
5170
  const fileBytes = await uploadDeps.readFileBytes(filePath);
4870
5171
  const contentType = getContentType(filePath);
4871
- const fileName = filePath.split("/").pop();
5172
+ const parts = filePath.split("/");
5173
+ const fileName = parts[parts.length - 1];
4872
5174
  const uploadUrl = `${getDustbucketHost()}/api/assets?repositoryId=${encodeURIComponent(repositoryId)}`;
4873
5175
  try {
4874
5176
  const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType, fileName);
@@ -5566,12 +5868,12 @@ function validateNoCycles(allPrincipleRelationships) {
5566
5868
  }
5567
5869
  for (const rel of allPrincipleRelationships) {
5568
5870
  const visited = new Set;
5569
- const path4 = [];
5871
+ const path2 = [];
5570
5872
  let current = rel.filePath;
5571
5873
  while (current) {
5572
5874
  if (visited.has(current)) {
5573
- const cycleStart = path4.indexOf(current);
5574
- const cyclePath = path4.slice(cycleStart).concat(current);
5875
+ const cycleStart = path2.indexOf(current);
5876
+ const cyclePath = path2.slice(cycleStart).concat(current);
5575
5877
  violations.push({
5576
5878
  file: rel.filePath,
5577
5879
  message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
@@ -5579,7 +5881,7 @@ function validateNoCycles(allPrincipleRelationships) {
5579
5881
  break;
5580
5882
  }
5581
5883
  visited.add(current);
5582
- path4.push(current);
5884
+ path2.push(current);
5583
5885
  const currentRel = relationshipMap.get(current);
5584
5886
  if (currentRel && currentRel.parentPrinciples.length > 0) {
5585
5887
  current = currentRel.parentPrinciples[0];
@@ -5801,13 +6103,13 @@ function truncateOutput(output) {
5801
6103
  ].join(`
5802
6104
  `);
5803
6105
  }
5804
- async function runSingleCheck(check, cwd, runner, emitEvent) {
6106
+ async function runSingleCheck(check, cwd, runner, emitEvent, clock = Date.now) {
5805
6107
  const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
5806
6108
  log6(`running check ${check.name}: ${check.command}`);
5807
6109
  emitEvent?.({ type: "check-started", name: check.name });
5808
- const startTime = Date.now();
6110
+ const startTime = clock();
5809
6111
  const result = await runner.run(check.command, cwd, timeoutMs);
5810
- const durationMs = Date.now() - startTime;
6112
+ const durationMs = clock() - startTime;
5811
6113
  const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
5812
6114
  log6(`check ${check.name} ${status} (${durationMs}ms)`);
5813
6115
  if (result.exitCode === 0) {
@@ -5833,18 +6135,18 @@ async function runSingleCheck(check, cwd, runner, emitEvent) {
5833
6135
  timeoutSeconds: timeoutMs / 1000
5834
6136
  };
5835
6137
  }
5836
- async function runConfiguredChecks(checks, cwd, runner, emitEvent) {
5837
- const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent));
6138
+ async function runConfiguredChecks(checks, cwd, runner, emitEvent, clock = Date.now) {
6139
+ const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent, clock));
5838
6140
  return Promise.all(promises);
5839
6141
  }
5840
- async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent) {
6142
+ async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent, clock = Date.now) {
5841
6143
  const results = [];
5842
6144
  for (const check of checks) {
5843
- results.push(await runSingleCheck(check, cwd, runner, emitEvent));
6145
+ results.push(await runSingleCheck(check, cwd, runner, emitEvent, clock));
5844
6146
  }
5845
6147
  return results;
5846
6148
  }
5847
- async function runValidationCheck(dependencies, emitEvent) {
6149
+ async function runValidationCheck(dependencies, emitEvent, clock = Date.now) {
5848
6150
  const outputLines = [];
5849
6151
  const bufferedContext = {
5850
6152
  cwd: dependencies.context.cwd,
@@ -5853,13 +6155,13 @@ async function runValidationCheck(dependencies, emitEvent) {
5853
6155
  };
5854
6156
  log6("running built-in check: dust lint");
5855
6157
  emitEvent?.({ type: "check-started", name: "lint" });
5856
- const startTime = Date.now();
6158
+ const startTime = clock();
5857
6159
  const result = await lintMarkdown({
5858
6160
  ...dependencies,
5859
6161
  context: bufferedContext,
5860
6162
  arguments: []
5861
6163
  });
5862
- const durationMs = Date.now() - startTime;
6164
+ const durationMs = clock() - startTime;
5863
6165
  const lintStatus = result.exitCode === 0 ? "passed" : "failed";
5864
6166
  log6(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5865
6167
  const output = outputLines.join(`
@@ -5867,14 +6169,12 @@ async function runValidationCheck(dependencies, emitEvent) {
5867
6169
  if (result.exitCode === 0) {
5868
6170
  emitEvent?.({ type: "check-passed", name: "lint", durationMs });
5869
6171
  } else {
5870
- const failedEvent = {
6172
+ emitEvent?.({
5871
6173
  type: "check-failed",
5872
6174
  name: "lint",
5873
- durationMs
5874
- };
5875
- if (output)
5876
- failedEvent.output = output;
5877
- emitEvent?.(failedEvent);
6175
+ durationMs,
6176
+ output
6177
+ });
5878
6178
  }
5879
6179
  return {
5880
6180
  name: "lint",
@@ -5923,7 +6223,7 @@ function displayResults(results, context) {
5923
6223
  context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
5924
6224
  return failed.length > 0 ? 1 : 0;
5925
6225
  }
5926
- async function check(dependencies, shellRunner = defaultShellRunner) {
6226
+ async function check(dependencies, shellRunner = defaultShellRunner, clock = Date.now) {
5927
6227
  const {
5928
6228
  arguments: commandArguments,
5929
6229
  context,
@@ -5949,18 +6249,18 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
5949
6249
  if (serial) {
5950
6250
  const results2 = [];
5951
6251
  if (hasDustDir) {
5952
- results2.push(await runValidationCheck(dependencies, context.emitEvent));
6252
+ results2.push(await runValidationCheck(dependencies, context.emitEvent, clock));
5953
6253
  }
5954
- const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent);
6254
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent, clock);
5955
6255
  results2.push(...configuredResults);
5956
6256
  const exitCode2 = displayResults(results2, context);
5957
6257
  return { exitCode: exitCode2 };
5958
6258
  }
5959
6259
  const checkPromises = [];
5960
6260
  if (hasDustDir) {
5961
- checkPromises.push(runValidationCheck(dependencies, context.emitEvent));
6261
+ checkPromises.push(runValidationCheck(dependencies, context.emitEvent, clock));
5962
6262
  }
5963
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent));
6263
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent, clock));
5964
6264
  const promiseResults = await Promise.all(checkPromises);
5965
6265
  const results = [];
5966
6266
  for (const result of promiseResults) {
@@ -6299,7 +6599,7 @@ async function list(dependencies) {
6299
6599
  ideas: collectedItems.map((i) => ({
6300
6600
  path: i.path,
6301
6601
  title: i.title,
6302
- status: i.status ?? "draft"
6602
+ status: i.status
6303
6603
  }))
6304
6604
  });
6305
6605
  } else if (type === "principles") {
@@ -6566,8 +6866,8 @@ function parseGitDiffNameStatus(output) {
6566
6866
  const parts = line.split("\t");
6567
6867
  if (parts.length >= 2) {
6568
6868
  const statusChar = parts[0].charAt(0);
6569
- const path4 = parts.length > 2 ? parts[2] : parts[1];
6570
- changes.push({ status: statusChar, path: path4 });
6869
+ const path2 = parts.length > 2 ? parts[2] : parts[1];
6870
+ changes.push({ status: statusChar, path: path2 });
6571
6871
  }
6572
6872
  }
6573
6873
  return changes;
@@ -6628,12 +6928,12 @@ async function getUncommittedFiles(cwd, gitRunner) {
6628
6928
  `).filter((line) => line.length > 0);
6629
6929
  for (const line of lines) {
6630
6930
  if (line.length > 3) {
6631
- const path4 = line.substring(3);
6632
- const arrowIndex = path4.indexOf(" -> ");
6931
+ const path2 = line.substring(3);
6932
+ const arrowIndex = path2.indexOf(" -> ");
6633
6933
  if (arrowIndex !== -1) {
6634
- files.push(path4.substring(arrowIndex + 4));
6934
+ files.push(path2.substring(arrowIndex + 4));
6635
6935
  } else {
6636
- files.push(path4);
6936
+ files.push(path2);
6637
6937
  }
6638
6938
  }
6639
6939
  }
@@ -6784,24 +7084,24 @@ async function main(options) {
6784
7084
  function createFileSystem(primitives) {
6785
7085
  return {
6786
7086
  exists: primitives.existsSync,
6787
- isDirectory: (path4) => {
7087
+ isDirectory: (path2) => {
6788
7088
  try {
6789
- return primitives.statSync(path4).isDirectory();
7089
+ return primitives.statSync(path2).isDirectory();
6790
7090
  } catch {
6791
7091
  return false;
6792
7092
  }
6793
7093
  },
6794
- readFile: (path4) => primitives.readFile(path4, "utf-8"),
6795
- writeFile: (path4, content, options) => primitives.writeFile(path4, content, {
7094
+ readFile: (path2) => primitives.readFile(path2, "utf-8"),
7095
+ writeFile: (path2, content, options) => primitives.writeFile(path2, content, {
6796
7096
  encoding: "utf-8",
6797
7097
  flag: options?.flag
6798
7098
  }),
6799
- mkdir: async (path4, options) => {
6800
- await primitives.mkdir(path4, options);
7099
+ mkdir: async (path2, options) => {
7100
+ await primitives.mkdir(path2, options);
6801
7101
  },
6802
- getFileCreationTime: (path4) => primitives.statSync(path4).birthtimeMs,
6803
- readdir: (path4) => primitives.readdir(path4),
6804
- chmod: (path4, mode) => primitives.chmod(path4, mode),
7102
+ getFileCreationTime: (path2) => primitives.statSync(path2).birthtimeMs,
7103
+ readdir: (path2) => primitives.readdir(path2),
7104
+ chmod: (path2, mode) => primitives.chmod(path2, mode),
6805
7105
  rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
6806
7106
  };
6807
7107
  }