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