@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,282 @@
1
+ import { existsSync } from "node:fs"
2
+ import { isAbsolute } from "node:path"
3
+ import { isActive, type ProcessDefinition, type ProcessStatus } from "../processes/ManagedProcess.js"
4
+ import type { ProcessManager } from "../processes/ProcessManager.js"
5
+ import { DevccError } from "../utils/errors.js"
6
+ import { exec, type ExecOptions, type ExecResult } from "../utils/exec.js"
7
+ import type { ScopedLogger } from "../utils/logger.js"
8
+
9
+ /**
10
+ * AI coding agents (Claude Code, Codex, ...).
11
+ *
12
+ * An agent is not a new kind of process: an adapter only describes how to run
13
+ * the agent's CLI non-interactively, and `AgentManager` hands the result to the
14
+ * project's `ProcessManager`. Status, PID, logs, stop and cleanup on quit all
15
+ * come from the existing process infrastructure.
16
+ */
17
+
18
+ export interface AgentLaunchOptions {
19
+ /** Working directory - the selected worktree. Must be absolute. */
20
+ cwd: string
21
+ /** The task for the agent. */
22
+ prompt: string
23
+ /** Branch of the worktree, for display. */
24
+ branch?: string
25
+ }
26
+
27
+ export interface AgentAdapter {
28
+ id: string
29
+ name: string
30
+ /** Executable, overridable per machine through config. */
31
+ command: string
32
+ /** Whether the CLI is installed and runnable. */
33
+ detect: () => Promise<boolean>
34
+ /** argv for a non-interactive run of `prompt`. */
35
+ args: (prompt: string) => string[]
36
+ /**
37
+ * True when `args` puts `--` before the prompt, so a task starting with `-`
38
+ * cannot be read as an option. Tasks starting with `-` are refused otherwise.
39
+ */
40
+ separatesPrompt: boolean
41
+ }
42
+
43
+ export interface AgentConfig {
44
+ /** Display name (defaults per built-in, or to the id). */
45
+ name?: string
46
+ /** Executable name or path. */
47
+ command?: string
48
+ /** Extra arguments placed before the prompt, e.g. `["--permission-mode", "acceptEdits"]`. */
49
+ args?: string[]
50
+ /** Set false to hide a built-in agent. */
51
+ enabled?: boolean
52
+ }
53
+
54
+ export interface AgentSession {
55
+ /** Process id in the ProcessManager, also the log source id. */
56
+ id: string
57
+ agentId: string
58
+ agentName: string
59
+ cwd: string
60
+ branch?: string
61
+ /** First line of the prompt, for display. */
62
+ task: string
63
+ status: ProcessStatus
64
+ pid?: number
65
+ startedAt: Date
66
+ exitCode?: number
67
+ }
68
+
69
+ type Exec = (command: string, args: readonly string[], options?: ExecOptions) => Promise<ExecResult>
70
+
71
+ interface BuiltinAgent {
72
+ name: string
73
+ command: string
74
+ /** Where the prompt goes relative to the user's extra args. */
75
+ args: (extra: string[], prompt: string) => string[]
76
+ }
77
+
78
+ /**
79
+ * Non-interactive invocations: `claude -p -- <prompt>` and `codex exec -- <prompt>`.
80
+ * The `--` matters: without it a task like `--dangerously-skip-permissions`
81
+ * would be parsed as a flag.
82
+ */
83
+ export const BUILTIN_AGENTS: Readonly<Record<string, BuiltinAgent>> = {
84
+ claude: {
85
+ name: "Claude Code",
86
+ command: "claude",
87
+ args: (extra, prompt) => [...extra, "-p", "--", prompt],
88
+ },
89
+ codex: {
90
+ name: "Codex",
91
+ command: "codex",
92
+ args: (extra, prompt) => ["exec", ...extra, "--", prompt],
93
+ },
94
+ }
95
+
96
+ /**
97
+ * Build adapters from config. Built-ins are always present unless disabled;
98
+ * any other key defines a custom agent whose prompt is the last argument.
99
+ */
100
+ export function createAgentAdapters(
101
+ config: Readonly<Record<string, AgentConfig>> = {},
102
+ run: Exec = exec,
103
+ ): AgentAdapter[] {
104
+ const ids = [...new Set([...Object.keys(BUILTIN_AGENTS), ...Object.keys(config)])]
105
+ const adapters: AgentAdapter[] = []
106
+
107
+ for (const id of ids) {
108
+ const entry = config[id] ?? {}
109
+ if (entry.enabled === false) continue
110
+ const builtin = BUILTIN_AGENTS[id]
111
+ const command = entry.command ?? builtin?.command
112
+ if (!command) continue
113
+
114
+ const extra = entry.args ?? []
115
+ adapters.push({
116
+ id,
117
+ name: entry.name ?? builtin?.name ?? id,
118
+ command,
119
+ detect: async () => {
120
+ const result = await run(command, ["--version"], { timeoutMs: 3_000 })
121
+ return result.ok
122
+ },
123
+ // Custom agents keep the prompt as the literal last argument (it may be an
124
+ // option's value, e.g. `--message`), so they cannot take a `--` guard.
125
+ args: (prompt) => (builtin ? builtin.args(extra, prompt) : [...extra, prompt]),
126
+ separatesPrompt: builtin !== undefined,
127
+ })
128
+ }
129
+
130
+ return adapters
131
+ }
132
+
133
+ export interface AgentManagerOptions {
134
+ processes: ProcessManager
135
+ adapters: readonly AgentAdapter[]
136
+ logger?: ScopedLogger
137
+ /** Injected for tests. */
138
+ exists?: (path: string) => boolean
139
+ }
140
+
141
+ interface SessionMeta {
142
+ agentId: string
143
+ agentName: string
144
+ cwd: string
145
+ branch?: string
146
+ task: string
147
+ startedAt: Date
148
+ }
149
+
150
+ export const AGENT_PROCESS_PREFIX = "agent:"
151
+
152
+ export class AgentManager {
153
+ private readonly adapters: Map<string, AgentAdapter>
154
+ private readonly meta = new Map<string, SessionMeta>()
155
+ /** Detection results, cached for the session: installing a CLI mid-session is rare. */
156
+ private readonly detected = new Map<string, Promise<boolean>>()
157
+ private sequence = 0
158
+
159
+ constructor(private readonly options: AgentManagerOptions) {
160
+ this.adapters = new Map(options.adapters.map((adapter) => [adapter.id, adapter]))
161
+ }
162
+
163
+ list(): AgentAdapter[] {
164
+ return [...this.adapters.values()]
165
+ }
166
+
167
+ get(id: string): AgentAdapter | undefined {
168
+ return this.adapters.get(id)
169
+ }
170
+
171
+ /** Whether an agent's CLI is installed. `refresh` re-runs the probe. */
172
+ detect(id: string, refresh = false): Promise<boolean> {
173
+ const adapter = this.adapters.get(id)
174
+ if (!adapter) return Promise.resolve(false)
175
+ let pending = refresh ? undefined : this.detected.get(id)
176
+ if (!pending) {
177
+ pending = adapter.detect().catch(() => false)
178
+ this.detected.set(id, pending)
179
+ }
180
+ return pending
181
+ }
182
+
183
+ /**
184
+ * Start an agent in `options.cwd` as a managed task. Throws a `DevccError`
185
+ * with an actionable message when the agent is unknown, not installed, or
186
+ * the directory is missing.
187
+ */
188
+ async launch(agentId: string, options: AgentLaunchOptions): Promise<AgentSession> {
189
+ const adapter = this.adapters.get(agentId)
190
+ if (!adapter) throw new DevccError({ scope: "process", message: `Unknown agent "${agentId}"` })
191
+
192
+ const prompt = options.prompt.trim()
193
+ if (prompt === "") throw new DevccError({ scope: "process", message: `${adapter.name} needs a task` })
194
+ if (prompt.startsWith("-") && !adapter.separatesPrompt) {
195
+ throw new DevccError({
196
+ scope: "process",
197
+ message: `${adapter.name}: a task cannot start with "-" (it would be read as an option)`,
198
+ })
199
+ }
200
+
201
+ const exists = this.options.exists ?? existsSync
202
+ if (!isAbsolute(options.cwd) || !exists(options.cwd)) {
203
+ throw new DevccError({ scope: "process", message: `Working directory does not exist: ${options.cwd}` })
204
+ }
205
+
206
+ if (!(await this.detect(agentId, true))) {
207
+ throw new DevccError({
208
+ scope: "process",
209
+ message: `${adapter.name} is not installed (\`${adapter.command}\` was not found)`,
210
+ detail: `Install it, or point devcc at it with agents.${agentId}.command in .devcc.ts.`,
211
+ })
212
+ }
213
+
214
+ this.sequence += 1
215
+ const id = `${AGENT_PROCESS_PREFIX}${agentId}:${this.sequence}`
216
+ const where = options.branch ?? options.cwd
217
+ const definition: ProcessDefinition = {
218
+ id,
219
+ name: `${adapter.name} · ${where}`,
220
+ command: adapter.command,
221
+ args: adapter.args(prompt),
222
+ cwd: options.cwd,
223
+ kind: "task",
224
+ }
225
+
226
+ this.meta.set(id, {
227
+ agentId,
228
+ agentName: adapter.name,
229
+ cwd: options.cwd,
230
+ branch: options.branch,
231
+ task: prompt.split("\n")[0] ?? prompt,
232
+ startedAt: new Date(),
233
+ })
234
+
235
+ this.options.processes.register(definition)
236
+ this.options.logger?.info("agent launch", { agent: agentId, cwd: options.cwd })
237
+ try {
238
+ await this.options.processes.start(id)
239
+ } catch (error) {
240
+ this.options.logger?.warn("agent launch failed", { agent: agentId, cwd: options.cwd })
241
+ throw error
242
+ }
243
+ return this.session(id) as AgentSession
244
+ }
245
+
246
+ /** Every agent session this manager started, newest first. */
247
+ sessions(): AgentSession[] {
248
+ return [...this.meta.keys()]
249
+ .map((id) => this.session(id))
250
+ .filter((session): session is AgentSession => session !== undefined)
251
+ .reverse()
252
+ }
253
+
254
+ async stop(sessionId: string): Promise<void> {
255
+ await this.options.processes.stop(sessionId)
256
+ }
257
+
258
+ /** Directories where an agent is still running; worktree removal refuses these. */
259
+ busyPaths(): string[] {
260
+ return this.sessions()
261
+ .filter((session) => isActive(session.status))
262
+ .map((session) => session.cwd)
263
+ }
264
+
265
+ private session(id: string): AgentSession | undefined {
266
+ const meta = this.meta.get(id)
267
+ const process = this.options.processes.get(id)
268
+ if (!meta || !process) return undefined
269
+ return {
270
+ id,
271
+ agentId: meta.agentId,
272
+ agentName: meta.agentName,
273
+ cwd: meta.cwd,
274
+ branch: meta.branch,
275
+ task: meta.task,
276
+ status: process.status,
277
+ pid: process.pid,
278
+ startedAt: process.startedAt ?? meta.startedAt,
279
+ exitCode: process.exitCode,
280
+ }
281
+ }
282
+ }