ralph-review 0.2.2 → 0.2.4

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.
Files changed (56) hide show
  1. package/README.md +123 -16
  2. package/package.json +7 -5
  3. package/src/cli-core.ts +51 -88
  4. package/src/cli-rrr.ts +1 -4
  5. package/src/cli.ts +1 -2
  6. package/src/commands/apply.ts +35 -20
  7. package/src/commands/config-handlers.ts +68 -69
  8. package/src/commands/config-model.ts +147 -125
  9. package/src/commands/doctor.ts +2 -4
  10. package/src/commands/fix.ts +73 -51
  11. package/src/commands/handoff-selection.ts +6 -8
  12. package/src/commands/interactive-deps.ts +43 -0
  13. package/src/commands/list.ts +24 -7
  14. package/src/commands/log.ts +12 -12
  15. package/src/commands/run.ts +32 -33
  16. package/src/commands/status.ts +25 -4
  17. package/src/commands/stop.ts +99 -62
  18. package/src/commands/update.ts +2 -4
  19. package/src/lib/agents/claude.ts +4 -16
  20. package/src/lib/agents/core.ts +16 -0
  21. package/src/lib/agents/droid.ts +4 -15
  22. package/src/lib/agents/models.ts +9 -0
  23. package/src/lib/cli-parser.ts +19 -14
  24. package/src/lib/handoff.ts +16 -7
  25. package/src/lib/logging/session-log.ts +2 -1
  26. package/src/lib/prompts/defaults/review.md +1 -1
  27. package/src/lib/prompts/protocol.ts +2 -1
  28. package/src/lib/review-workflow/findings/artifact.ts +3 -1
  29. package/src/lib/review-workflow/findings/types.ts +1 -1
  30. package/src/lib/review-workflow/remediation/prompt.ts +7 -7
  31. package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
  32. package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
  33. package/src/lib/review-workflow/results/finalize-result.ts +20 -3
  34. package/src/lib/review-workflow/run-review-cycle.ts +1 -12
  35. package/src/lib/review-workflow/session-status.ts +13 -0
  36. package/src/lib/review-workflow/shared/framed-json.ts +2 -47
  37. package/src/lib/session/state.ts +50 -38
  38. package/src/lib/structured-output.ts +24 -9
  39. package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
  40. package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +2 -2
  41. package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
  42. package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
  43. package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
  44. package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
  45. package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
  46. package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
  47. package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
  48. package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
  49. package/src/lib/tui/shared/CenteredModal.tsx +44 -0
  50. package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
  51. package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
  52. package/src/lib/tui/workspace/Workspace.tsx +6 -91
  53. package/src/lib/tui/workspace/use-workspace-state.ts +113 -61
  54. package/src/lib/types/fix.ts +15 -48
  55. package/src/lib/types/guards.ts +47 -0
  56. package/src/lib/types/review.ts +5 -39
@@ -12,7 +12,10 @@ import {
12
12
  } from "@/lib/priority-list";
13
13
  import { loadFindingsArtifactBySessionId } from "@/lib/review-workflow/findings/artifact";
14
14
  import type { FindingId, FindingsArtifact } from "@/lib/review-workflow/findings/types";
15
- import { runFixSession } from "@/lib/review-workflow/remediation/run-fix-session";
15
+ import {
16
+ promptForFixSelection,
17
+ runFixSession,
18
+ } from "@/lib/review-workflow/remediation/run-fix-session";
16
19
  import {
17
20
  createSessionState,
18
21
  HEARTBEAT_INTERVAL_MS,
@@ -67,30 +70,19 @@ export interface FixCommandDeps {
67
70
  exit: (code: number) => void;
68
71
  }
69
72
 
70
- function defaultPromptForSelection(artifact: FindingsArtifact): Promise<FindingId[] | null> {
71
- return p
72
- .multiselect({
73
- message: "Choose findings to fix",
74
- options: artifact.findings.map((finding) => ({
75
- value: finding.id,
76
- label: `${finding.id} [${finding.priority}] ${finding.title}`,
77
- hint: `${finding.filePath}:${finding.startLine}-${finding.endLine}`,
78
- })),
79
- required: false,
80
- })
81
- .then((selection) => {
82
- if (p.isCancel(selection)) {
83
- return null;
84
- }
73
+ interface LoadedFixArtifact {
74
+ parsed: ParsedFixCommandOptions;
75
+ artifact: FindingsArtifact;
76
+ }
85
77
 
86
- return (selection as FindingId[]) ?? [];
87
- });
78
+ interface PreparedFixCommand extends LoadedFixArtifact {
79
+ commandDeps: FixCommandDeps;
88
80
  }
89
81
 
90
82
  const DEFAULT_FIX_COMMAND_DEPS: FixCommandDeps = {
91
83
  loadConfig: loadEffectiveConfig,
92
84
  loadFindingsArtifactBySessionId,
93
- promptForSelection: defaultPromptForSelection,
85
+ promptForSelection: promptForFixSelection,
94
86
  runFixSession,
95
87
  isTTY: () => process.stdout.isTTY === true,
96
88
  isTmuxInstalled,
@@ -332,31 +324,69 @@ export function parseFixCommandOptions(args: string[]): ParsedFixCommandOptions
332
324
  };
333
325
  }
334
326
 
335
- export async function runFix(
336
- args: string[] = [],
337
- deps: Partial<FixCommandDeps> = {}
338
- ): Promise<void> {
339
- const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
340
-
327
+ async function loadFixCommandArtifact(
328
+ args: string[],
329
+ commandDeps: FixCommandDeps
330
+ ): Promise<LoadedFixArtifact | null> {
341
331
  let parsed: ParsedFixCommandOptions;
342
332
  try {
343
333
  parsed = parseFixCommandOptions(args);
344
334
  } catch (error) {
345
335
  commandDeps.logError(`${error}`);
346
336
  commandDeps.exit(1);
347
- return;
337
+ return null;
348
338
  }
349
339
 
350
340
  const artifact = await commandDeps.loadFindingsArtifactBySessionId(CONFIG_DIR, parsed.sessionId);
351
341
  if (!artifact) {
352
342
  commandDeps.logError(`Findings artifact not found for session ${parsed.sessionId}`);
353
343
  commandDeps.exit(1);
344
+ return null;
345
+ }
346
+
347
+ return { parsed, artifact };
348
+ }
349
+
350
+ async function prepareFixCommand(
351
+ args: string[],
352
+ deps: Partial<FixCommandDeps>
353
+ ): Promise<PreparedFixCommand | null> {
354
+ const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
355
+ const loaded = await loadFixCommandArtifact(args, commandDeps);
356
+ return loaded ? { commandDeps, ...loaded } : null;
357
+ }
358
+
359
+ async function withPreparedFixCommand(
360
+ args: string[],
361
+ deps: Partial<FixCommandDeps>,
362
+ run: (prepared: PreparedFixCommand) => Promise<void>
363
+ ): Promise<void> {
364
+ const prepared = await prepareFixCommand(args, deps);
365
+ if (!prepared) {
354
366
  return;
355
367
  }
356
368
 
357
- if (!(await commandDeps.loadConfig(artifact.projectPath))) {
369
+ await run(prepared);
370
+ }
371
+
372
+ async function loadRequiredFixConfig(
373
+ commandDeps: FixCommandDeps,
374
+ projectPath: string
375
+ ): ReturnType<FixCommandDeps["loadConfig"]> {
376
+ const config = await commandDeps.loadConfig(projectPath);
377
+ if (!config) {
358
378
  commandDeps.logError("Failed to load configuration");
359
379
  commandDeps.exit(1);
380
+ }
381
+ return config;
382
+ }
383
+
384
+ async function runPreparedFix({
385
+ commandDeps,
386
+ parsed,
387
+ artifact,
388
+ }: PreparedFixCommand): Promise<void> {
389
+ if (!(await loadRequiredFixConfig(commandDeps, artifact.projectPath))) {
360
390
  return;
361
391
  }
362
392
 
@@ -423,33 +453,18 @@ export async function runFix(
423
453
  }
424
454
  }
425
455
 
426
- export async function runFixForeground(
427
- args: string[] = [],
428
- deps: Partial<FixCommandDeps> = {}
429
- ): Promise<void> {
430
- const commandDeps = { ...DEFAULT_FIX_COMMAND_DEPS, ...deps };
431
-
432
- let parsed: ParsedFixCommandOptions;
433
- try {
434
- parsed = parseFixCommandOptions(args);
435
- } catch (error) {
436
- commandDeps.logError(`${error}`);
437
- commandDeps.exit(1);
438
- return;
439
- }
440
-
441
- const artifact = await commandDeps.loadFindingsArtifactBySessionId(CONFIG_DIR, parsed.sessionId);
442
- if (!artifact) {
443
- commandDeps.logError(`Findings artifact not found for session ${parsed.sessionId}`);
444
- commandDeps.exit(1);
445
- return;
446
- }
456
+ export function runFix(args: string[] = [], deps: Partial<FixCommandDeps> = {}): Promise<void> {
457
+ return withPreparedFixCommand(args, deps, runPreparedFix);
458
+ }
447
459
 
460
+ async function runPreparedFixForeground({
461
+ commandDeps,
462
+ parsed,
463
+ artifact,
464
+ }: PreparedFixCommand): Promise<void> {
448
465
  const projectPath = commandDeps.env.RR_PROJECT_PATH || artifact.projectPath || commandDeps.cwd();
449
- const config = await commandDeps.loadConfig(projectPath);
466
+ const config = await loadRequiredFixConfig(commandDeps, projectPath);
450
467
  if (!config) {
451
- commandDeps.logError("Failed to load configuration");
452
- commandDeps.exit(1);
453
468
  return;
454
469
  }
455
470
 
@@ -593,3 +608,10 @@ export async function runFixForeground(
593
608
  });
594
609
  }
595
610
  }
611
+
612
+ export function runFixForeground(
613
+ args: string[] = [],
614
+ deps: Partial<FixCommandDeps> = {}
615
+ ): Promise<void> {
616
+ return withPreparedFixCommand(args, deps, runPreparedFixForeground);
617
+ }
@@ -2,6 +2,10 @@ import * as p from "@clack/prompts";
2
2
  import type { PendingHandoffArtifact } from "@/lib/handoff";
3
3
 
4
4
  type HandoffAction = "apply" | "discard";
5
+ type HandoffSelect = (input: {
6
+ message: string;
7
+ options: Array<{ value: string; label: string; hint: string }>;
8
+ }) => Promise<unknown>;
5
9
 
6
10
  interface SelectableHandoff {
7
11
  handoffId: string;
@@ -14,10 +18,7 @@ interface ResolvePendingHandoffSelectionOptions {
14
18
  selector?: string;
15
19
  action: HandoffAction;
16
20
  isTTY: boolean;
17
- select?: (input: {
18
- message: string;
19
- options: Array<{ value: string; label: string; hint: string }>;
20
- }) => Promise<unknown>;
21
+ select?: HandoffSelect;
21
22
  isCancel?: (value: unknown) => boolean;
22
23
  }
23
24
 
@@ -105,10 +106,7 @@ async function resolveHandoffSelection<T extends SelectableHandoff>(options: {
105
106
  action: HandoffAction;
106
107
  isTTY: boolean;
107
108
  multipleHandoffsMessage: string;
108
- select?: (input: {
109
- message: string;
110
- options: Array<{ value: string; label: string; hint: string }>;
111
- }) => Promise<unknown>;
109
+ select?: HandoffSelect;
112
110
  isCancel?: (value: unknown) => boolean;
113
111
  }): Promise<HandoffSelectionResult<T>> {
114
112
  if (options.selector) {
@@ -0,0 +1,43 @@
1
+ import * as p from "@clack/prompts";
2
+ import { getCommandDef } from "@/cli";
3
+
4
+ export interface InteractiveCommandDeps {
5
+ getCommandDef: typeof getCommandDef;
6
+ logError: (message: string) => void;
7
+ exit: (code: number) => void;
8
+ isTTY: () => boolean;
9
+ }
10
+
11
+ export type PromptSelect = (input: {
12
+ message: string;
13
+ options: Array<{ value: string; label: string; hint: string }>;
14
+ }) => Promise<unknown>;
15
+
16
+ export interface PromptDeps {
17
+ logInfo: (message: string) => void;
18
+ logMessage: (message: string) => void;
19
+ logStep: (message: string) => void;
20
+ logSuccess: (message: string) => void;
21
+ select: PromptSelect;
22
+ isCancel: (value: unknown) => boolean;
23
+ }
24
+
25
+ export function createInteractiveCommandDeps(): InteractiveCommandDeps {
26
+ return {
27
+ getCommandDef,
28
+ logError: (message) => p.log.error(message),
29
+ exit: (code) => process.exit(code),
30
+ isTTY: () => process.stdout.isTTY === true,
31
+ };
32
+ }
33
+
34
+ export function createPromptDeps(): PromptDeps {
35
+ return {
36
+ logInfo: p.log.info,
37
+ logMessage: p.log.message,
38
+ logStep: p.log.step,
39
+ logSuccess: p.log.success,
40
+ select: p.select,
41
+ isCancel: p.isCancel,
42
+ };
43
+ }
@@ -2,6 +2,20 @@ import * as p from "@clack/prompts";
2
2
  import { listAllActiveSessions } from "@/lib/session-state";
3
3
  import { listRalphSessions } from "@/lib/tmux";
4
4
 
5
+ interface ListDeps {
6
+ listAllActiveSessions: typeof listAllActiveSessions;
7
+ listRalphSessions: typeof listRalphSessions;
8
+ logInfo: (message: string) => void;
9
+ print: (...args: unknown[]) => void;
10
+ }
11
+
12
+ const DEFAULT_LIST_DEPS: ListDeps = {
13
+ listAllActiveSessions,
14
+ listRalphSessions,
15
+ logInfo: p.log.info,
16
+ print: (...args) => console.log(...args),
17
+ };
18
+
5
19
  function formatRelativeStart(startTime: number): string {
6
20
  const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startTime) / 1000));
7
21
 
@@ -18,10 +32,11 @@ function formatRelativeStart(startTime: number): string {
18
32
  return `${elapsedHours}h ago`;
19
33
  }
20
34
 
21
- export async function runList(): Promise<void> {
35
+ export async function runList(deps: Partial<ListDeps> = {}): Promise<void> {
36
+ const listDeps = { ...DEFAULT_LIST_DEPS, ...deps };
22
37
  const [activeSessions, tmuxSessions] = await Promise.all([
23
- listAllActiveSessions(),
24
- listRalphSessions(),
38
+ listDeps.listAllActiveSessions(),
39
+ listDeps.listRalphSessions(),
25
40
  ]);
26
41
  const trackedTmuxSessions = new Set(activeSessions.map((session) => session.sessionName));
27
42
  const untrackedTmuxSessions = tmuxSessions.filter(
@@ -29,17 +44,19 @@ export async function runList(): Promise<void> {
29
44
  );
30
45
 
31
46
  if (activeSessions.length === 0 && untrackedTmuxSessions.length === 0) {
32
- p.log.info("No active review sessions.");
47
+ listDeps.logInfo("No active review sessions.");
33
48
  } else {
34
- p.log.info("Active review sessions:");
49
+ listDeps.logInfo("Active review sessions:");
35
50
  for (const session of activeSessions) {
36
51
  const worktree = session.worktreeBranch ? ` ${session.worktreeBranch}` : "";
37
- console.log(
52
+ listDeps.print(
38
53
  `${session.sessionId.slice(0, 8)} ${session.sessionName} ${session.projectPath}${worktree} ${formatRelativeStart(session.startTime)}`
39
54
  );
40
55
  }
41
56
  for (const sessionName of untrackedTmuxSessions) {
42
- console.log(`${sessionName} (tmux only)`);
57
+ listDeps.print(`${sessionName} (tmux only)`);
43
58
  }
44
59
  }
45
60
  }
61
+
62
+ export type { ListDeps };
@@ -82,6 +82,16 @@ function isUnknownEmptySession(session: SessionStats): boolean {
82
82
  return session.status === "unknown" && session.iterations === 0;
83
83
  }
84
84
 
85
+ function printNoProjectSessions(projectName: string, json: boolean): void {
86
+ if (json) {
87
+ console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
88
+ return;
89
+ }
90
+
91
+ p.log.info("No review sessions found for current working directory.");
92
+ p.log.message('Start a review with "rr run" first.');
93
+ }
94
+
85
95
  function formatDate(timestamp: number): string {
86
96
  return new Date(timestamp).toLocaleString();
87
97
  }
@@ -412,12 +422,7 @@ export async function runLog(args: string[]): Promise<void> {
412
422
  const projectSessions = await listProjectLogSessions(CONFIG_DIR, currentProjectPath);
413
423
 
414
424
  if (projectSessions.length === 0) {
415
- if (options.json) {
416
- console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
417
- } else {
418
- p.log.info("No review sessions found for current working directory.");
419
- p.log.message('Start a review with "rr run" first.');
420
- }
425
+ printNoProjectSessions(projectName, options.json);
421
426
  return;
422
427
  }
423
428
 
@@ -451,12 +456,7 @@ export async function runLog(args: string[]): Promise<void> {
451
456
  }
452
457
 
453
458
  if (sessionStats.length === 0) {
454
- if (options.json) {
455
- console.log(JSON.stringify({ project: projectName, sessions: [] }, null, 2));
456
- } else {
457
- p.log.info("No review sessions found for current working directory.");
458
- p.log.message('Start a review with "rr run" first.');
459
- }
459
+ printNoProjectSessions(projectName, options.json);
460
460
  return;
461
461
  }
462
462
 
@@ -5,7 +5,7 @@ import { parseCommand } from "@/lib/cli-parser";
5
5
  import { loadEffectiveConfig } from "@/lib/config";
6
6
  import { collectIssueItems, runDiagnostics } from "@/lib/diagnostics";
7
7
  import { getTmuxInstallHint } from "@/lib/diagnostics/tmux-install";
8
- import type { DiagnosticsReport } from "@/lib/diagnostics/types";
8
+ import type { DiagnosticItem, DiagnosticsReport } from "@/lib/diagnostics/types";
9
9
  import { type CycleResult, runReviewCycle } from "@/lib/engine";
10
10
  import { formatReviewType } from "@/lib/format";
11
11
  import { formatHandoffNote } from "@/lib/handoff-note";
@@ -19,6 +19,7 @@ import {
19
19
  } from "@/lib/priority-list";
20
20
  import { runFixSession } from "@/lib/review-workflow/remediation/run-fix-session";
21
21
  import type { FixSessionResult } from "@/lib/review-workflow/remediation/types";
22
+ import { mapSessionStatusToFinalStatus } from "@/lib/review-workflow/session-status";
22
23
  import {
23
24
  createSessionId,
24
25
  createSessionState,
@@ -108,20 +109,6 @@ export function formatRunAgentsNote(config: Config, reviewOptions: ReviewOptions
108
109
  return lines.join("\n");
109
110
  }
110
111
 
111
- function mapSessionStatusToFinalStatus(
112
- status: FixSessionResult["sessionStatus"]
113
- ): CycleResult["finalStatus"] {
114
- if (status === "failed") {
115
- return "failed";
116
- }
117
-
118
- if (status === "interrupted") {
119
- return "interrupted";
120
- }
121
-
122
- return "completed";
123
- }
124
-
125
112
  function hasAutoFixPriorityMatches(result: CycleResult, priorities: Priority[]): boolean {
126
113
  return (
127
114
  result.artifact?.findings.some((finding) => priorities.includes(finding.priority)) ?? false
@@ -257,6 +244,32 @@ export interface RunRuntimeOverrides
257
244
  timer?: Partial<RunRuntime["timer"]>;
258
245
  }
259
246
 
247
+ function createRunCommandContext(overrides: RunRuntimeOverrides): {
248
+ runtime: RunRuntime;
249
+ projectPath: string;
250
+ } {
251
+ const runtime = createRunRuntime(overrides);
252
+ return {
253
+ runtime,
254
+ projectPath: runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd(),
255
+ };
256
+ }
257
+
258
+ function logPreflightItems(
259
+ runtime: RunRuntime,
260
+ items: DiagnosticItem[],
261
+ heading: string,
262
+ severity: "error" | "warn"
263
+ ): void {
264
+ runtime.prompt.log[severity](heading);
265
+ for (const item of items) {
266
+ runtime.prompt.log.message(` ${item.summary}`);
267
+ item.remediation.forEach((remediation) => {
268
+ runtime.prompt.log.message(` -> ${remediation}`);
269
+ });
270
+ }
271
+ }
272
+
260
273
  export function createRunRuntime(overrides: RunRuntimeOverrides = {}): RunRuntime {
261
274
  const defaults: RunRuntime = {
262
275
  prompt: {
@@ -449,8 +462,7 @@ export async function runForeground(
449
462
  args: string[] = [],
450
463
  overrides: RunRuntimeOverrides = {}
451
464
  ): Promise<void> {
452
- const runtime = createRunRuntime(overrides);
453
- const projectPath = runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd();
465
+ const { runtime, projectPath } = createRunCommandContext(overrides);
454
466
  const config = await runtime.loadConfig(projectPath);
455
467
  if (!config) {
456
468
  runtime.prompt.log.error("Failed to load config");
@@ -714,8 +726,7 @@ export async function startReview(
714
726
  args: string[],
715
727
  overrides: RunRuntimeOverrides = {}
716
728
  ): Promise<void> {
717
- const runtime = createRunRuntime(overrides);
718
- const projectPath = runtime.process.env.RR_PROJECT_PATH || runtime.process.cwd();
729
+ const { runtime, projectPath } = createRunCommandContext(overrides);
719
730
  // Parse options using command definition
720
731
  const runDef = runtime.getCommandDef("run");
721
732
  if (!runDef) {
@@ -856,25 +867,13 @@ export async function startReview(
856
867
  const warnings = issues.filter((item) => item.severity === "warning");
857
868
 
858
869
  if (errors.length > 0) {
859
- runtime.prompt.log.error("Cannot run review:");
860
- for (const item of errors) {
861
- runtime.prompt.log.message(` ${item.summary}`);
862
- item.remediation.forEach((remediation) => {
863
- runtime.prompt.log.message(` -> ${remediation}`);
864
- });
865
- }
870
+ logPreflightItems(runtime, errors, "Cannot run review:", "error");
866
871
  runtime.process.exit(1);
867
872
  return;
868
873
  }
869
874
 
870
875
  if (warnings.length > 0) {
871
- runtime.prompt.log.warn("Preflight warnings:");
872
- for (const item of warnings) {
873
- runtime.prompt.log.message(` ${item.summary}`);
874
- item.remediation.forEach((remediation) => {
875
- runtime.prompt.log.message(` -> ${remediation}`);
876
- });
877
- }
876
+ logPreflightItems(runtime, warnings, "Preflight warnings:", "warn");
878
877
  }
879
878
 
880
879
  const config = diagnostics.config ?? (await runtime.loadConfig(projectPath));
@@ -1,13 +1,34 @@
1
1
  import { getGitBranch } from "@/lib/logger";
2
2
 
3
- export async function runStatus(): Promise<void> {
4
- const projectPath = process.cwd();
5
- const branch = await getGitBranch(projectPath);
3
+ interface StatusDeps {
4
+ cwd: () => string;
5
+ getGitBranch: typeof getGitBranch;
6
+ renderDashboard: (payload: { projectPath: string; branch: string | undefined }) => Promise<void>;
7
+ }
6
8
 
9
+ async function renderDashboardWithDynamicImport(payload: {
10
+ projectPath: string;
11
+ branch: string | undefined;
12
+ }): Promise<void> {
7
13
  const { renderDashboard } = await import("@/lib/tui/index");
14
+ await renderDashboard(payload);
15
+ }
16
+
17
+ const DEFAULT_STATUS_DEPS: StatusDeps = {
18
+ cwd: () => process.cwd(),
19
+ getGitBranch,
20
+ renderDashboard: renderDashboardWithDynamicImport,
21
+ };
8
22
 
9
- await renderDashboard({
23
+ export async function runStatus(deps: Partial<StatusDeps> = {}): Promise<void> {
24
+ const statusDeps = { ...DEFAULT_STATUS_DEPS, ...deps };
25
+ const projectPath = statusDeps.cwd();
26
+ const branch = await statusDeps.getGitBranch(projectPath);
27
+
28
+ await statusDeps.renderDashboard({
10
29
  projectPath,
11
30
  branch,
12
31
  });
13
32
  }
33
+
34
+ export type { StatusDeps };