@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.
- package/README.md +556 -386
- package/bin/devcc.mjs +50 -23
- package/dist/devcc/agents/agents.d.ts +112 -0
- package/dist/devcc/agents/agents.d.ts.map +1 -0
- package/dist/devcc/agents/agents.js +179 -0
- package/dist/devcc/agents/agents.js.map +1 -0
- package/dist/devcc/app/App.d.ts +15 -0
- package/dist/devcc/app/App.d.ts.map +1 -1
- package/dist/devcc/app/App.js +326 -17
- package/dist/devcc/app/App.js.map +1 -1
- package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
- package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
- package/dist/devcc/app/controlPlaneCommands.js +179 -0
- package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -1
- package/dist/devcc/app/keymap.js +24 -3
- package/dist/devcc/app/keymap.js.map +1 -1
- package/dist/devcc/automation.d.ts +31 -0
- package/dist/devcc/automation.d.ts.map +1 -0
- package/dist/devcc/automation.js +202 -0
- package/dist/devcc/automation.js.map +1 -0
- package/dist/devcc/cli.d.ts.map +1 -1
- package/dist/devcc/cli.js +46 -3
- package/dist/devcc/cli.js.map +1 -1
- package/dist/devcc/components/Sidebar.d.ts.map +1 -1
- package/dist/devcc/components/Sidebar.js +1 -0
- package/dist/devcc/components/Sidebar.js.map +1 -1
- package/dist/devcc/components/primitives.d.ts.map +1 -1
- package/dist/devcc/components/primitives.js +3 -0
- package/dist/devcc/components/primitives.js.map +1 -1
- package/dist/devcc/core/commands.d.ts +25 -0
- package/dist/devcc/core/commands.d.ts.map +1 -1
- package/dist/devcc/core/commands.js.map +1 -1
- package/dist/devcc/core/config.d.ts +69 -3
- package/dist/devcc/core/config.d.ts.map +1 -1
- package/dist/devcc/core/config.js +185 -5
- package/dist/devcc/core/config.js.map +1 -1
- package/dist/devcc/core/state.d.ts +19 -1
- package/dist/devcc/core/state.d.ts.map +1 -1
- package/dist/devcc/core/state.js +6 -0
- package/dist/devcc/core/state.js.map +1 -1
- package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
- package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
- package/dist/devcc/environment/EnvironmentService.js +159 -0
- package/dist/devcc/environment/EnvironmentService.js.map +1 -0
- package/dist/devcc/environment/integrations.d.ts +52 -0
- package/dist/devcc/environment/integrations.d.ts.map +1 -0
- package/dist/devcc/environment/integrations.js +94 -0
- package/dist/devcc/environment/integrations.js.map +1 -0
- package/dist/devcc/environment/tools.d.ts +51 -0
- package/dist/devcc/environment/tools.d.ts.map +1 -0
- package/dist/devcc/environment/tools.js +99 -0
- package/dist/devcc/environment/tools.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +26 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -1
- package/dist/devcc/git/GitService.js +71 -2
- package/dist/devcc/git/GitService.js.map +1 -1
- package/dist/devcc/git/worktrees.d.ts +88 -0
- package/dist/devcc/git/worktrees.d.ts.map +1 -0
- package/dist/devcc/git/worktrees.js +226 -0
- package/dist/devcc/git/worktrees.js.map +1 -0
- package/dist/devcc/index.d.ts +13 -3
- package/dist/devcc/index.d.ts.map +1 -1
- package/dist/devcc/index.js +11 -1
- package/dist/devcc/index.js.map +1 -1
- package/dist/devcc/platform/launch.d.ts +30 -0
- package/dist/devcc/platform/launch.d.ts.map +1 -0
- package/dist/devcc/platform/launch.js +93 -0
- package/dist/devcc/platform/launch.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +10 -2
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
- package/dist/devcc/services/ServiceManager.js +18 -4
- package/dist/devcc/services/ServiceManager.js.map +1 -1
- package/dist/devcc/services/ollama.d.ts +64 -5
- package/dist/devcc/services/ollama.d.ts.map +1 -1
- package/dist/devcc/services/ollama.js +118 -17
- package/dist/devcc/services/ollama.js.map +1 -1
- package/dist/devcc/services/remote.d.ts +90 -0
- package/dist/devcc/services/remote.d.ts.map +1 -0
- package/dist/devcc/services/remote.js +233 -0
- package/dist/devcc/services/remote.js.map +1 -0
- package/dist/devcc/services/types.d.ts +8 -1
- package/dist/devcc/services/types.d.ts.map +1 -1
- package/dist/devcc/services/types.js.map +1 -1
- package/dist/devcc/utils/exec.d.ts +6 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -1
- package/dist/devcc/utils/exec.js +12 -2
- package/dist/devcc/utils/exec.js.map +1 -1
- package/dist/devcc/verify/verify.d.ts +107 -0
- package/dist/devcc/verify/verify.d.ts.map +1 -0
- package/dist/devcc/verify/verify.js +203 -0
- package/dist/devcc/verify/verify.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -1
- package/dist/devcc/views/OverviewView.js +55 -1
- package/dist/devcc/views/OverviewView.js.map +1 -1
- package/dist/devcc/views/ServicesView.d.ts +2 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -1
- package/dist/devcc/views/ServicesView.js +15 -0
- package/dist/devcc/views/ServicesView.js.map +1 -1
- package/dist/devcc/views/SystemView.d.ts +10 -1
- package/dist/devcc/views/SystemView.d.ts.map +1 -1
- package/dist/devcc/views/SystemView.js +127 -19
- package/dist/devcc/views/SystemView.js.map +1 -1
- package/dist/devcc/views/WorktreesView.d.ts +48 -0
- package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
- package/dist/devcc/views/WorktreesView.js +235 -0
- package/dist/devcc/views/WorktreesView.js.map +1 -0
- package/package.json +1 -1
- package/src/devcc/agents/agents.ts +282 -0
- package/src/devcc/app/App.ts +376 -20
- package/src/devcc/app/controlPlaneCommands.ts +197 -0
- package/src/devcc/app/keymap.ts +24 -3
- package/src/devcc/automation.ts +242 -0
- package/src/devcc/cli.ts +45 -3
- package/src/devcc/components/Sidebar.ts +1 -0
- package/src/devcc/components/primitives.ts +3 -0
- package/src/devcc/core/commands.ts +26 -0
- package/src/devcc/core/config.ts +271 -11
- package/src/devcc/core/state.ts +25 -0
- package/src/devcc/environment/EnvironmentService.ts +215 -0
- package/src/devcc/environment/integrations.ts +133 -0
- package/src/devcc/environment/tools.ts +151 -0
- package/src/devcc/git/GitService.ts +104 -2
- package/src/devcc/git/worktrees.ts +256 -0
- package/src/devcc/index.ts +57 -2
- package/src/devcc/platform/launch.ts +125 -0
- package/src/devcc/services/ServiceManager.ts +15 -3
- package/src/devcc/services/ollama.ts +162 -19
- package/src/devcc/services/remote.ts +306 -0
- package/src/devcc/services/types.ts +9 -1
- package/src/devcc/utils/exec.ts +26 -2
- package/src/devcc/verify/verify.ts +316 -0
- package/src/devcc/views/OverviewView.ts +76 -1
- package/src/devcc/views/ServicesView.ts +17 -0
- package/src/devcc/views/SystemView.ts +144 -22
- 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
|
+
}
|