@doriandev/devcc 0.1.2 → 0.2.0

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 (136) hide show
  1. package/README.md +556 -386
  2. package/bin/devcc.mjs +50 -23
  3. package/dist/devcc/agents/agents.d.ts +112 -0
  4. package/dist/devcc/agents/agents.d.ts.map +1 -0
  5. package/dist/devcc/agents/agents.js +179 -0
  6. package/dist/devcc/agents/agents.js.map +1 -0
  7. package/dist/devcc/app/App.d.ts +15 -0
  8. package/dist/devcc/app/App.d.ts.map +1 -1
  9. package/dist/devcc/app/App.js +326 -17
  10. package/dist/devcc/app/App.js.map +1 -1
  11. package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
  12. package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
  13. package/dist/devcc/app/controlPlaneCommands.js +179 -0
  14. package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
  15. package/dist/devcc/app/keymap.d.ts.map +1 -1
  16. package/dist/devcc/app/keymap.js +24 -3
  17. package/dist/devcc/app/keymap.js.map +1 -1
  18. package/dist/devcc/automation.d.ts +31 -0
  19. package/dist/devcc/automation.d.ts.map +1 -0
  20. package/dist/devcc/automation.js +202 -0
  21. package/dist/devcc/automation.js.map +1 -0
  22. package/dist/devcc/cli.d.ts.map +1 -1
  23. package/dist/devcc/cli.js +46 -3
  24. package/dist/devcc/cli.js.map +1 -1
  25. package/dist/devcc/components/Sidebar.d.ts.map +1 -1
  26. package/dist/devcc/components/Sidebar.js +1 -0
  27. package/dist/devcc/components/Sidebar.js.map +1 -1
  28. package/dist/devcc/components/primitives.d.ts.map +1 -1
  29. package/dist/devcc/components/primitives.js +3 -0
  30. package/dist/devcc/components/primitives.js.map +1 -1
  31. package/dist/devcc/core/commands.d.ts +25 -0
  32. package/dist/devcc/core/commands.d.ts.map +1 -1
  33. package/dist/devcc/core/commands.js.map +1 -1
  34. package/dist/devcc/core/config.d.ts +69 -3
  35. package/dist/devcc/core/config.d.ts.map +1 -1
  36. package/dist/devcc/core/config.js +185 -5
  37. package/dist/devcc/core/config.js.map +1 -1
  38. package/dist/devcc/core/state.d.ts +19 -1
  39. package/dist/devcc/core/state.d.ts.map +1 -1
  40. package/dist/devcc/core/state.js +6 -0
  41. package/dist/devcc/core/state.js.map +1 -1
  42. package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
  43. package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
  44. package/dist/devcc/environment/EnvironmentService.js +159 -0
  45. package/dist/devcc/environment/EnvironmentService.js.map +1 -0
  46. package/dist/devcc/environment/integrations.d.ts +52 -0
  47. package/dist/devcc/environment/integrations.d.ts.map +1 -0
  48. package/dist/devcc/environment/integrations.js +94 -0
  49. package/dist/devcc/environment/integrations.js.map +1 -0
  50. package/dist/devcc/environment/tools.d.ts +51 -0
  51. package/dist/devcc/environment/tools.d.ts.map +1 -0
  52. package/dist/devcc/environment/tools.js +99 -0
  53. package/dist/devcc/environment/tools.js.map +1 -0
  54. package/dist/devcc/git/GitService.d.ts +26 -0
  55. package/dist/devcc/git/GitService.d.ts.map +1 -1
  56. package/dist/devcc/git/GitService.js +71 -2
  57. package/dist/devcc/git/GitService.js.map +1 -1
  58. package/dist/devcc/git/worktrees.d.ts +88 -0
  59. package/dist/devcc/git/worktrees.d.ts.map +1 -0
  60. package/dist/devcc/git/worktrees.js +226 -0
  61. package/dist/devcc/git/worktrees.js.map +1 -0
  62. package/dist/devcc/index.d.ts +13 -3
  63. package/dist/devcc/index.d.ts.map +1 -1
  64. package/dist/devcc/index.js +11 -1
  65. package/dist/devcc/index.js.map +1 -1
  66. package/dist/devcc/platform/launch.d.ts +30 -0
  67. package/dist/devcc/platform/launch.d.ts.map +1 -0
  68. package/dist/devcc/platform/launch.js +93 -0
  69. package/dist/devcc/platform/launch.js.map +1 -0
  70. package/dist/devcc/services/ServiceManager.d.ts +10 -2
  71. package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
  72. package/dist/devcc/services/ServiceManager.js +18 -4
  73. package/dist/devcc/services/ServiceManager.js.map +1 -1
  74. package/dist/devcc/services/ollama.d.ts +64 -5
  75. package/dist/devcc/services/ollama.d.ts.map +1 -1
  76. package/dist/devcc/services/ollama.js +118 -17
  77. package/dist/devcc/services/ollama.js.map +1 -1
  78. package/dist/devcc/services/remote.d.ts +90 -0
  79. package/dist/devcc/services/remote.d.ts.map +1 -0
  80. package/dist/devcc/services/remote.js +233 -0
  81. package/dist/devcc/services/remote.js.map +1 -0
  82. package/dist/devcc/services/types.d.ts +8 -1
  83. package/dist/devcc/services/types.d.ts.map +1 -1
  84. package/dist/devcc/services/types.js.map +1 -1
  85. package/dist/devcc/utils/exec.d.ts +6 -0
  86. package/dist/devcc/utils/exec.d.ts.map +1 -1
  87. package/dist/devcc/utils/exec.js +12 -2
  88. package/dist/devcc/utils/exec.js.map +1 -1
  89. package/dist/devcc/verify/verify.d.ts +107 -0
  90. package/dist/devcc/verify/verify.d.ts.map +1 -0
  91. package/dist/devcc/verify/verify.js +203 -0
  92. package/dist/devcc/verify/verify.js.map +1 -0
  93. package/dist/devcc/views/OverviewView.d.ts.map +1 -1
  94. package/dist/devcc/views/OverviewView.js +55 -1
  95. package/dist/devcc/views/OverviewView.js.map +1 -1
  96. package/dist/devcc/views/ServicesView.d.ts +2 -0
  97. package/dist/devcc/views/ServicesView.d.ts.map +1 -1
  98. package/dist/devcc/views/ServicesView.js +15 -0
  99. package/dist/devcc/views/ServicesView.js.map +1 -1
  100. package/dist/devcc/views/SystemView.d.ts +10 -1
  101. package/dist/devcc/views/SystemView.d.ts.map +1 -1
  102. package/dist/devcc/views/SystemView.js +127 -19
  103. package/dist/devcc/views/SystemView.js.map +1 -1
  104. package/dist/devcc/views/WorktreesView.d.ts +48 -0
  105. package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
  106. package/dist/devcc/views/WorktreesView.js +235 -0
  107. package/dist/devcc/views/WorktreesView.js.map +1 -0
  108. package/package.json +1 -1
  109. package/src/devcc/agents/agents.ts +282 -0
  110. package/src/devcc/app/App.ts +376 -20
  111. package/src/devcc/app/controlPlaneCommands.ts +197 -0
  112. package/src/devcc/app/keymap.ts +24 -3
  113. package/src/devcc/automation.ts +242 -0
  114. package/src/devcc/cli.ts +45 -3
  115. package/src/devcc/components/Sidebar.ts +1 -0
  116. package/src/devcc/components/primitives.ts +3 -0
  117. package/src/devcc/core/commands.ts +26 -0
  118. package/src/devcc/core/config.ts +271 -11
  119. package/src/devcc/core/state.ts +25 -0
  120. package/src/devcc/environment/EnvironmentService.ts +215 -0
  121. package/src/devcc/environment/integrations.ts +133 -0
  122. package/src/devcc/environment/tools.ts +151 -0
  123. package/src/devcc/git/GitService.ts +104 -2
  124. package/src/devcc/git/worktrees.ts +256 -0
  125. package/src/devcc/index.ts +57 -2
  126. package/src/devcc/platform/launch.ts +125 -0
  127. package/src/devcc/services/ServiceManager.ts +15 -3
  128. package/src/devcc/services/ollama.ts +162 -19
  129. package/src/devcc/services/remote.ts +306 -0
  130. package/src/devcc/services/types.ts +9 -1
  131. package/src/devcc/utils/exec.ts +26 -2
  132. package/src/devcc/verify/verify.ts +316 -0
  133. package/src/devcc/views/OverviewView.ts +76 -1
  134. package/src/devcc/views/ServicesView.ts +17 -0
  135. package/src/devcc/views/SystemView.ts +144 -22
  136. package/src/devcc/views/WorktreesView.ts +270 -0
@@ -0,0 +1,270 @@
1
+ import type { KeyEvent } from "@opentui/core"
2
+ import type { AgentSession } from "../agents/agents.js"
3
+ import { theme } from "../app/theme.js"
4
+ import type { KeyHint } from "../components/Footer.js"
5
+ import { RowList, type ListRow } from "../components/primitives.js"
6
+ import { processBadge } from "../components/StatusBadge.js"
7
+ import { chunk, dim, styled } from "../components/styled.js"
8
+ import { setSelection, updateUi, type AppState, type ViewId } from "../core/state.js"
9
+ import { isInside, worktreeLabel, type GitWorktree } from "../git/worktrees.js"
10
+ import { isActive } from "../processes/ManagedProcess.js"
11
+ import { errorMessage } from "../utils/errors.js"
12
+ import { fit, formatDuration } from "../utils/format.js"
13
+ import { tildify } from "../utils/paths.js"
14
+ import { View, type ViewDeps } from "./View.js"
15
+
16
+ export interface WorktreeEntry {
17
+ kind: "heading" | "blank" | "note" | "worktree" | "agent"
18
+ key: string
19
+ text?: string
20
+ worktree?: GitWorktree
21
+ agent?: AgentSession
22
+ }
23
+
24
+ /**
25
+ * The rows of the Worktrees view, derived from state alone so commands can
26
+ * resolve "the selected worktree" without asking the view.
27
+ */
28
+ export function worktreeEntries(state: AppState): WorktreeEntry[] {
29
+ const entries: WorktreeEntry[] = [{ kind: "heading", key: "h-worktrees", text: "Worktrees" }]
30
+
31
+ if (!state.git.isRepository) {
32
+ entries.push({
33
+ kind: "note",
34
+ key: "n-repo",
35
+ text: state.git.lastRefreshedAt ? "Not a git repository." : "Reading git…",
36
+ })
37
+ } else {
38
+ for (const worktree of state.git.worktrees) {
39
+ entries.push({ kind: "worktree", key: `w-${worktree.path}`, worktree })
40
+ }
41
+ }
42
+
43
+ entries.push({ kind: "blank", key: "b-agents" })
44
+ entries.push({ kind: "heading", key: "h-agents", text: "AI agents" })
45
+ if (state.agents.length === 0) {
46
+ entries.push({
47
+ kind: "note",
48
+ key: "n-agents",
49
+ text: "No agents started. Select a worktree and press c (Claude) or x (Codex).",
50
+ })
51
+ } else {
52
+ for (const agent of state.agents) entries.push({ kind: "agent", key: `a-${agent.id}`, agent })
53
+ }
54
+ return entries
55
+ }
56
+
57
+ function selectable(entry: WorktreeEntry | undefined): boolean {
58
+ return entry?.kind === "worktree" || entry?.kind === "agent"
59
+ }
60
+
61
+ export function selectedWorktreeEntry(state: AppState): WorktreeEntry | undefined {
62
+ const entry = worktreeEntries(state)[state.ui.selection.worktrees]
63
+ return selectable(entry) ? entry : undefined
64
+ }
65
+
66
+ /**
67
+ * Git worktrees and the AI agents working in them.
68
+ *
69
+ * Keys act on the highlighted row: worktree actions on a worktree, stop/logs
70
+ * on an agent session. Everything here is also in the palette.
71
+ */
72
+ export class WorktreesView extends View {
73
+ readonly id: ViewId = "worktrees"
74
+ readonly title = "Worktrees"
75
+
76
+ private readonly list: RowList
77
+ private entries: WorktreeEntry[] = []
78
+ /**
79
+ * The highlighted row's key. Rows shift when worktrees or agents appear, so
80
+ * the selection follows the row, not its index - `s` must never stop the
81
+ * wrong agent.
82
+ */
83
+ private selectedKey: string | undefined
84
+
85
+ constructor(deps: ViewDeps) {
86
+ super(deps)
87
+ this.list = new RowList(deps.ctx, { flexGrow: 1 })
88
+ this.box.add(this.list.box)
89
+ }
90
+
91
+ update(state: AppState): void {
92
+ this.entries = worktreeEntries(state)
93
+ const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
94
+ const labelWidth = Math.min(30, Math.max(14, Math.floor(width * 0.3)))
95
+
96
+ let selected = state.ui.selection.worktrees
97
+ const followed = this.selectedKey ? this.entries.findIndex((entry) => entry.key === this.selectedKey) : -1
98
+ if (followed !== -1) selected = followed
99
+ else if (!selectable(this.entries[selected])) selected = this.entries.findIndex(selectable)
100
+ if (selected !== -1 && selected !== state.ui.selection.worktrees) {
101
+ setSelection(this.deps.app.store, "worktrees", selected)
102
+ }
103
+ this.selectedKey = this.entries[selected]?.key
104
+
105
+ const rows: ListRow[] = this.entries.map((entry) => {
106
+ switch (entry.kind) {
107
+ case "heading":
108
+ return {
109
+ key: entry.key,
110
+ disabled: true,
111
+ content: styled(chunk((entry.text ?? "").toUpperCase(), theme.accent)),
112
+ }
113
+ case "blank":
114
+ return { key: entry.key, disabled: true, content: "" }
115
+ case "note":
116
+ return { key: entry.key, disabled: true, content: styled(dim(` ${entry.text ?? ""}`)) }
117
+ case "worktree":
118
+ return {
119
+ key: entry.key,
120
+ content: this.worktreeRow(entry.worktree as GitWorktree, state, labelWidth, width),
121
+ }
122
+ case "agent":
123
+ return { key: entry.key, content: this.agentRow(entry.agent as AgentSession, width) }
124
+ }
125
+ })
126
+
127
+ this.list.setRows(rows, selected)
128
+ }
129
+
130
+ private worktreeRow(worktree: GitWorktree, state: AppState, labelWidth: number, width: number) {
131
+ const agents = state.agents.filter(
132
+ (agent) => isActive(agent.status) && isInside(agent.cwd, worktree.path),
133
+ )
134
+ const flags = [
135
+ worktree.isMain ? "main" : undefined,
136
+ worktree.locked ? "locked" : undefined,
137
+ worktree.prunable ? "prunable" : undefined,
138
+ ].filter(Boolean)
139
+ const pathWidth = Math.max(10, width - labelWidth - 24)
140
+
141
+ return styled(
142
+ chunk(worktree.isCurrent ? "● " : "○ ", worktree.isCurrent ? theme.accent : theme.textDim),
143
+ chunk(fit(worktreeLabel(worktree), labelWidth), worktree.isCurrent ? theme.textBright : theme.text),
144
+ dim(fit(tildify(worktree.path), pathWidth)),
145
+ agents.length > 0
146
+ ? chunk(fit(` ${agents.length} agent${agents.length === 1 ? "" : "s"} ▸`, 12), theme.success)
147
+ : dim(fit("", 12)),
148
+ worktree.locked || worktree.prunable ? chunk(flags.join(" "), theme.warning) : dim(flags.join(" ")),
149
+ )
150
+ }
151
+
152
+ private agentRow(agent: AgentSession, width: number) {
153
+ const where = agent.branch ?? tildify(agent.cwd)
154
+ const elapsed = isActive(agent.status)
155
+ ? formatDuration(Date.now() - agent.startedAt.getTime())
156
+ : agent.exitCode !== undefined
157
+ ? `exit ${agent.exitCode}`
158
+ : ""
159
+ return styled(
160
+ ...processBadge(agent.status).chunks,
161
+ " ",
162
+ chunk(fit(agent.agentName, 13), theme.textBright),
163
+ chunk(fit(where, 24), theme.text),
164
+ dim(fit(agent.task, Math.max(10, width - 64))),
165
+ dim(elapsed),
166
+ )
167
+ }
168
+
169
+ override handleKey(key: KeyEvent, state: AppState): boolean {
170
+ const index = state.ui.selection.worktrees
171
+ const entry = this.entries[index]
172
+ const worktree = entry?.worktree
173
+ const agent = entry?.agent
174
+ const app = this.deps.app
175
+
176
+ switch (key.name) {
177
+ case "up":
178
+ case "k":
179
+ this.move(index, -1)
180
+ return true
181
+ case "down":
182
+ case "j":
183
+ this.move(index, 1)
184
+ return true
185
+ case "n":
186
+ void app.createWorktree()
187
+ return true
188
+ case "r":
189
+ void app.git.refresh().then(() => app.notify("info", "Worktrees refreshed"))
190
+ return true
191
+ case "d":
192
+ if (worktree) void app.removeWorktree(worktree)
193
+ else app.notify("warn", "Select a worktree to remove")
194
+ return true
195
+ case "o":
196
+ case "return":
197
+ if (agent) this.showLogs(agent)
198
+ else if (worktree) void this.openProject(worktree)
199
+ return true
200
+ case "t":
201
+ void app.openInTerminal(worktree?.path ?? agent?.cwd ?? state.project.rootDir)
202
+ return true
203
+ case "e":
204
+ void app.openInEditor(worktree?.path ?? agent?.cwd ?? state.project.rootDir)
205
+ return true
206
+ case "c":
207
+ void app.launchAgent("claude", worktree)
208
+ return true
209
+ case "x":
210
+ void app.launchAgent("codex", worktree)
211
+ return true
212
+ case "s":
213
+ if (agent && isActive(agent.status)) void this.stopAgent(agent)
214
+ else if (agent) app.notify("info", `${agent.agentName} is not running`)
215
+ return true
216
+ case "l":
217
+ if (agent) {
218
+ this.showLogs(agent)
219
+ return true
220
+ }
221
+ return false
222
+ default:
223
+ return false
224
+ }
225
+ }
226
+
227
+ override hints(): KeyHint[] {
228
+ return [
229
+ { keys: "↑↓", label: "select" },
230
+ { keys: "n", label: "new" },
231
+ { keys: "d", label: "remove" },
232
+ { keys: "o", label: "open" },
233
+ { keys: "t/e", label: "terminal/editor" },
234
+ { keys: "c/x", label: "claude/codex" },
235
+ { keys: "s", label: "stop agent" },
236
+ { keys: "r", label: "refresh" },
237
+ ]
238
+ }
239
+
240
+ private move(from: number, direction: 1 | -1): void {
241
+ for (let index = from + direction; index >= 0 && index < this.entries.length; index += direction) {
242
+ if (selectable(this.entries[index])) {
243
+ this.selectedKey = this.entries[index]?.key
244
+ setSelection(this.deps.app.store, "worktrees", index)
245
+ return
246
+ }
247
+ }
248
+ }
249
+
250
+ private async openProject(worktree: GitWorktree): Promise<void> {
251
+ if (worktree.isCurrent) {
252
+ this.deps.app.notify("info", "devcc is already showing this worktree")
253
+ return
254
+ }
255
+ await this.deps.app.switchProject(worktree.path)
256
+ }
257
+
258
+ private showLogs(agent: AgentSession): void {
259
+ updateUi(this.deps.app.store, { logSource: agent.id, view: "logs" })
260
+ }
261
+
262
+ private async stopAgent(agent: AgentSession): Promise<void> {
263
+ try {
264
+ await this.deps.app.agents.stop(agent.id)
265
+ this.deps.app.notify("info", `Stopped ${agent.agentName}`)
266
+ } catch (error) {
267
+ this.deps.app.notify("error", errorMessage(error))
268
+ }
269
+ }
270
+ }