@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
package/src/devcc/app/App.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoxRenderable, createCliRenderer, type CliRenderer, type KeyEvent } from "@opentui/core"
|
|
2
2
|
import { existsSync } from "node:fs"
|
|
3
3
|
import { basename, join, resolve } from "node:path"
|
|
4
|
+
import { AgentManager, createAgentAdapters } from "../agents/agents.js"
|
|
4
5
|
import { ConfirmDialog, PromptDialog } from "../components/ConfirmDialog.js"
|
|
5
6
|
import { Footer, type KeyHint } from "../components/Footer.js"
|
|
6
7
|
import { Header } from "../components/Header.js"
|
|
@@ -11,6 +12,7 @@ import { CommandPalette } from "../components/CommandPalette.js"
|
|
|
11
12
|
import { ProjectPicker } from "../components/ProjectPicker.js"
|
|
12
13
|
import { CommandRegistry, type CommandContext } from "../core/commands.js"
|
|
13
14
|
import { loadConfig, type CommandConfigEntry, type ResolvedConfig } from "../core/config.js"
|
|
15
|
+
import { EnvironmentService, type EnvironmentSnapshot } from "../environment/EnvironmentService.js"
|
|
14
16
|
import { Subscriptions } from "../core/events.js"
|
|
15
17
|
import {
|
|
16
18
|
createInitialSelection,
|
|
@@ -26,10 +28,12 @@ import {
|
|
|
26
28
|
type NotificationLevel,
|
|
27
29
|
type ProjectState,
|
|
28
30
|
type ScriptRun,
|
|
31
|
+
type VerifyState,
|
|
29
32
|
type ViewId,
|
|
30
33
|
} from "../core/state.js"
|
|
31
34
|
import { DependencyService, updateArgs } from "../deps/DependencyService.js"
|
|
32
35
|
import { GitService } from "../git/GitService.js"
|
|
36
|
+
import { defaultWorktreePath, removalBlocker, worktreeLabel, type GitWorktree } from "../git/worktrees.js"
|
|
33
37
|
import {
|
|
34
38
|
buildProjectList,
|
|
35
39
|
defaultSearchDirs,
|
|
@@ -40,20 +44,36 @@ import {
|
|
|
40
44
|
} from "../projects/ProjectRegistry.js"
|
|
41
45
|
import { LogStore, MAX_LOG_LINES } from "../logs/LogBuffer.js"
|
|
42
46
|
import { ALL_SOURCES } from "../logs/merge.js"
|
|
43
|
-
import { isActive } from "../processes/ManagedProcess.js"
|
|
47
|
+
import { isActive, type ManagedProcess } from "../processes/ManagedProcess.js"
|
|
48
|
+
import {
|
|
49
|
+
editorCommand,
|
|
50
|
+
launchDetached,
|
|
51
|
+
sshCommand,
|
|
52
|
+
terminalCommand,
|
|
53
|
+
type LaunchCommand,
|
|
54
|
+
} from "../platform/launch.js"
|
|
44
55
|
import { ProcessManager } from "../processes/ProcessManager.js"
|
|
45
56
|
import { DockerService } from "../services/docker.js"
|
|
46
57
|
import { detectNodeServices, PortService } from "../services/node.js"
|
|
47
58
|
import { OllamaService } from "../services/ollama.js"
|
|
48
59
|
import { PostgresService } from "../services/postgres.js"
|
|
49
60
|
import { RedisService } from "../services/redis.js"
|
|
61
|
+
import { RemoteMachineService, TailscaleService } from "../services/remote.js"
|
|
50
62
|
import { ServiceManager } from "../services/ServiceManager.js"
|
|
51
|
-
import { SystemMonitor } from "../system/SystemMonitor.js"
|
|
63
|
+
import { SystemMonitor, type ToolVersions } from "../system/SystemMonitor.js"
|
|
52
64
|
import { errorMessage, toDevccError } from "../utils/errors.js"
|
|
53
65
|
import { createLogger, type Logger } from "../utils/logger.js"
|
|
54
66
|
import { detectPackageManager, readPackageJson, readScripts, runScriptArgs } from "../utils/packageManager.js"
|
|
55
67
|
import { fuzzyFilter } from "../utils/fuzzy.js"
|
|
56
|
-
import { findProjectRoot } from "../utils/paths.js"
|
|
68
|
+
import { findProjectRoot, tildify } from "../utils/paths.js"
|
|
69
|
+
import {
|
|
70
|
+
isVerifyIssue,
|
|
71
|
+
resolveVerifySteps,
|
|
72
|
+
runVerification,
|
|
73
|
+
summariseVerify,
|
|
74
|
+
type StepExecutor,
|
|
75
|
+
type VerifyStep,
|
|
76
|
+
} from "../verify/verify.js"
|
|
57
77
|
import { DependenciesView } from "../views/DependenciesView.js"
|
|
58
78
|
import { GitView } from "../views/GitView.js"
|
|
59
79
|
import { LogsView } from "../views/LogsView.js"
|
|
@@ -63,8 +83,10 @@ import { ScriptsView, scriptProcessId } from "../views/ScriptsView.js"
|
|
|
63
83
|
import { ServicesView } from "../views/ServicesView.js"
|
|
64
84
|
import { SettingsView } from "../views/SettingsView.js"
|
|
65
85
|
import { SystemView } from "../views/SystemView.js"
|
|
86
|
+
import { WorktreesView, selectedWorktreeEntry } from "../views/WorktreesView.js"
|
|
66
87
|
import type { View, ViewDeps } from "../views/View.js"
|
|
67
88
|
import { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./builtinCommands.js"
|
|
89
|
+
import { createControlPlaneCommands } from "./controlPlaneCommands.js"
|
|
68
90
|
import { HELP_SECTIONS, VIEW_SHORTCUTS } from "./keymap.js"
|
|
69
91
|
import { theme } from "./theme.js"
|
|
70
92
|
|
|
@@ -101,7 +123,15 @@ interface ProjectSession {
|
|
|
101
123
|
git: GitService
|
|
102
124
|
system: SystemMonitor
|
|
103
125
|
deps: DependencyService
|
|
126
|
+
agents: AgentManager
|
|
127
|
+
environment: EnvironmentService
|
|
104
128
|
project: ProjectState
|
|
129
|
+
/** Verification steps resolved from config, and the latest run. */
|
|
130
|
+
verifySteps: VerifyStep[]
|
|
131
|
+
verify: VerifyState
|
|
132
|
+
/** Config problems that make verification untrustworthy; running is refused while any exist. */
|
|
133
|
+
verifyIssues: string[]
|
|
134
|
+
verifyAbort: AbortController | null
|
|
105
135
|
scripts: { name: string; command: string }[]
|
|
106
136
|
/** Script runs belong to the project they ran in, not to the store. */
|
|
107
137
|
scriptRuns: Record<string, ScriptRun>
|
|
@@ -151,6 +181,12 @@ export class App {
|
|
|
151
181
|
private get deps(): DependencyService {
|
|
152
182
|
return this.session.deps
|
|
153
183
|
}
|
|
184
|
+
private get agents(): AgentManager {
|
|
185
|
+
return this.session.agents
|
|
186
|
+
}
|
|
187
|
+
private get environment(): EnvironmentService {
|
|
188
|
+
return this.session.environment
|
|
189
|
+
}
|
|
154
190
|
private get config(): ResolvedConfig {
|
|
155
191
|
return this.session.config
|
|
156
192
|
}
|
|
@@ -242,21 +278,36 @@ export class App {
|
|
|
242
278
|
logger: logger.child("services"),
|
|
243
279
|
intervalMs: config.refresh.services,
|
|
244
280
|
})
|
|
281
|
+
const tailscale = new TailscaleService({
|
|
282
|
+
machines: config.remoteMachines,
|
|
283
|
+
logger: logger.child("tailscale"),
|
|
284
|
+
})
|
|
245
285
|
services.registerAll([
|
|
246
286
|
new DockerService({ rootDir }),
|
|
247
287
|
new PostgresService(),
|
|
248
288
|
new RedisService(),
|
|
249
289
|
new OllamaService(),
|
|
250
290
|
...detectNodeServices(packageJson),
|
|
251
|
-
...config.services.map(
|
|
252
|
-
|
|
253
|
-
new
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
...config.services.map((entry) =>
|
|
292
|
+
entry.type === "ollama"
|
|
293
|
+
? new OllamaService({ id: entry.id, name: entry.name, baseUrl: entry.baseUrl })
|
|
294
|
+
: new PortService({
|
|
295
|
+
id: entry.id,
|
|
296
|
+
name: entry.name,
|
|
297
|
+
port: entry.port,
|
|
298
|
+
host: entry.host,
|
|
299
|
+
healthPath: entry.healthPath,
|
|
300
|
+
category: entry.category,
|
|
301
|
+
}),
|
|
302
|
+
),
|
|
303
|
+
tailscale,
|
|
304
|
+
...config.remoteMachines.map(
|
|
305
|
+
(machine) =>
|
|
306
|
+
new RemoteMachineService({
|
|
307
|
+
machine,
|
|
308
|
+
tailscale: () => tailscale.getLastStatus(),
|
|
309
|
+
service: (id) => services.get(id),
|
|
310
|
+
logger: logger.child("remote"),
|
|
260
311
|
}),
|
|
261
312
|
),
|
|
262
313
|
])
|
|
@@ -267,13 +318,25 @@ export class App {
|
|
|
267
318
|
manager: packageManager.manager,
|
|
268
319
|
logger: logger.child("deps"),
|
|
269
320
|
})
|
|
321
|
+
const agents = new AgentManager({
|
|
322
|
+
processes,
|
|
323
|
+
adapters: createAgentAdapters(config.agents),
|
|
324
|
+
logger: logger.child("agents"),
|
|
325
|
+
})
|
|
326
|
+
const environment = new EnvironmentService({ rootDir, logger: logger.child("environment") })
|
|
327
|
+
const scripts = readScripts(packageJson)
|
|
328
|
+
const resolvedVerify = resolveVerifySteps(config.verify, {
|
|
329
|
+
commands: config.commands,
|
|
330
|
+
scripts,
|
|
331
|
+
packageManager: packageManager.manager,
|
|
332
|
+
})
|
|
270
333
|
|
|
271
334
|
const project: ProjectState = {
|
|
272
335
|
name: config.name ?? (typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir)),
|
|
273
336
|
rootDir,
|
|
274
337
|
packageManager,
|
|
275
338
|
configPath: loaded.filePath,
|
|
276
|
-
configIssues: loaded.issues,
|
|
339
|
+
configIssues: [...loaded.issues, ...resolvedVerify.issues],
|
|
277
340
|
host: system.getHostInfo(),
|
|
278
341
|
tools: { git: null, docker: null, packageManager: null },
|
|
279
342
|
}
|
|
@@ -291,8 +354,14 @@ export class App {
|
|
|
291
354
|
git,
|
|
292
355
|
system,
|
|
293
356
|
deps,
|
|
357
|
+
agents,
|
|
358
|
+
environment,
|
|
294
359
|
project,
|
|
295
|
-
|
|
360
|
+
verifySteps: resolvedVerify.steps,
|
|
361
|
+
verify: { configured: resolvedVerify.steps.length, running: false, steps: [] },
|
|
362
|
+
verifyIssues: [...loaded.issues.filter(isVerifyIssue), ...resolvedVerify.issues],
|
|
363
|
+
verifyAbort: null,
|
|
364
|
+
scripts,
|
|
296
365
|
scriptRuns: {},
|
|
297
366
|
wiring: new Subscriptions(),
|
|
298
367
|
timers: [],
|
|
@@ -307,6 +376,12 @@ export class App {
|
|
|
307
376
|
this.commands.registerAll(
|
|
308
377
|
createConfigCommands(this.config.commands, (entry) => this.runConfigCommand(entry)),
|
|
309
378
|
)
|
|
379
|
+
this.commands.registerAll(
|
|
380
|
+
createControlPlaneCommands({
|
|
381
|
+
agents: this.session.agents.list(),
|
|
382
|
+
machines: this.config.remoteMachines,
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
310
385
|
}
|
|
311
386
|
|
|
312
387
|
private reportConfigIssues(): void {
|
|
@@ -366,6 +441,7 @@ export class App {
|
|
|
366
441
|
new ServicesView(deps),
|
|
367
442
|
new ScriptsView(deps),
|
|
368
443
|
new GitView(deps),
|
|
444
|
+
new WorktreesView(deps),
|
|
369
445
|
new LogsView(deps),
|
|
370
446
|
new DependenciesView(deps),
|
|
371
447
|
new SystemView(deps),
|
|
@@ -429,13 +505,13 @@ export class App {
|
|
|
429
505
|
*/
|
|
430
506
|
private wireSession(session: ProjectSession): void {
|
|
431
507
|
const { store } = this
|
|
432
|
-
const { wiring, processes, services, git, system, logs, deps } = session
|
|
508
|
+
const { wiring, processes, services, git, system, logs, deps, environment } = session
|
|
433
509
|
const active = () => this.session === session
|
|
434
510
|
const notify = (level: NotificationLevel, message: string, detail?: string) =>
|
|
435
511
|
this.pushNotification(level, active() ? message : `[${session.project.name}] ${message}`, detail)
|
|
436
512
|
|
|
437
513
|
const syncProcesses = () => {
|
|
438
|
-
if (active()) store.patch({ processes: processes.list() })
|
|
514
|
+
if (active()) store.patch({ processes: processes.list(), agents: session.agents.sessions() })
|
|
439
515
|
this.refreshSidebarProjects()
|
|
440
516
|
}
|
|
441
517
|
processes.on("process:changed", syncProcesses)
|
|
@@ -473,11 +549,16 @@ export class App {
|
|
|
473
549
|
system.on("system:metrics", (metrics) => {
|
|
474
550
|
if (active()) store.patch({ system: metrics })
|
|
475
551
|
})
|
|
476
|
-
|
|
552
|
+
wiring.add(() => system.removeAllListeners())
|
|
553
|
+
|
|
554
|
+
environment.on("environment:changed", (snapshot) => {
|
|
555
|
+
const tools = toolVersionsFrom(snapshot, session.project.packageManager.manager)
|
|
477
556
|
session.project = { ...session.project, tools }
|
|
478
|
-
if (active())
|
|
557
|
+
if (active()) {
|
|
558
|
+
store.setState((state) => ({ ...state, environment: snapshot, project: { ...state.project, tools } }))
|
|
559
|
+
}
|
|
479
560
|
})
|
|
480
|
-
wiring.add(() =>
|
|
561
|
+
wiring.add(() => environment.removeAllListeners())
|
|
481
562
|
|
|
482
563
|
logs.on("log:sources", (sources) => {
|
|
483
564
|
if (!active()) return
|
|
@@ -540,6 +621,9 @@ export class App {
|
|
|
540
621
|
this.subscriptions.add(store.watch((state) => state.services, markDirty, shallowArrayEqual))
|
|
541
622
|
this.subscriptions.add(store.watch((state) => state.git, markDirty))
|
|
542
623
|
this.subscriptions.add(store.watch((state) => state.dependencies, markDirty))
|
|
624
|
+
this.subscriptions.add(store.watch((state) => state.agents, markDirty, shallowArrayEqual))
|
|
625
|
+
this.subscriptions.add(store.watch((state) => state.environment, markDirty))
|
|
626
|
+
this.subscriptions.add(store.watch((state) => state.verify, markDirty))
|
|
543
627
|
this.subscriptions.add(store.watch((state) => state.projects, markDirty, shallowArrayEqual))
|
|
544
628
|
this.subscriptions.add(store.watch((state) => state.system, markDirty))
|
|
545
629
|
this.subscriptions.add(store.watch((state) => state.scripts, markDirty))
|
|
@@ -557,6 +641,7 @@ export class App {
|
|
|
557
641
|
processes: this.processes.list(),
|
|
558
642
|
services: this.services.list(),
|
|
559
643
|
logSources: this.logs.sources(),
|
|
644
|
+
verify: this.session.verify,
|
|
560
645
|
})
|
|
561
646
|
store.setState((state) => ({
|
|
562
647
|
...state,
|
|
@@ -596,7 +681,8 @@ export class App {
|
|
|
596
681
|
|
|
597
682
|
void session.git.refresh()
|
|
598
683
|
void session.services.detectAll().then(() => session.services.start())
|
|
599
|
-
|
|
684
|
+
// Tool versions change rarely: detect once per project, refresh on request.
|
|
685
|
+
if (!session.environment.getSnapshot().checkedAt) void session.environment.refresh()
|
|
600
686
|
if (!session.processes.list().some((entry) => entry.startedAt)) {
|
|
601
687
|
// Autostart only the first time a project is opened, never on reactivation.
|
|
602
688
|
void session.processes.startAutoStart()
|
|
@@ -759,6 +845,11 @@ export class App {
|
|
|
759
845
|
void this.commands.run("app.refresh", this.context)
|
|
760
846
|
return
|
|
761
847
|
}
|
|
848
|
+
if (key.shift && key.name === "v") {
|
|
849
|
+
key.preventDefault()
|
|
850
|
+
void this.runCommand("verify.run")
|
|
851
|
+
return
|
|
852
|
+
}
|
|
762
853
|
|
|
763
854
|
const state = this.store.getState()
|
|
764
855
|
|
|
@@ -1035,6 +1126,8 @@ export class App {
|
|
|
1035
1126
|
logs: this.logs,
|
|
1036
1127
|
system: this.system,
|
|
1037
1128
|
deps: this.deps,
|
|
1129
|
+
agents: this.agents,
|
|
1130
|
+
environment: this.environment,
|
|
1038
1131
|
navigate: (view) => updateUi(this.store, { view }),
|
|
1039
1132
|
notify: (level, message, detail) => this.pushNotification(level, message, detail),
|
|
1040
1133
|
confirm: (title, message) => this.askConfirm(title, message),
|
|
@@ -1046,6 +1139,15 @@ export class App {
|
|
|
1046
1139
|
openProjects: () => this.openProjects(),
|
|
1047
1140
|
switchProject: (path) => this.openProject(path),
|
|
1048
1141
|
closeProject: (path) => this.closeProject(path),
|
|
1142
|
+
selectedWorktree: () => this.selectedWorktree(),
|
|
1143
|
+
createWorktree: () => this.createWorktree(),
|
|
1144
|
+
removeWorktree: (worktree) => this.removeWorktree(worktree),
|
|
1145
|
+
launchAgent: (agentId, worktree) => this.launchAgent(agentId, worktree),
|
|
1146
|
+
openInEditor: (path) => this.launch(() => editorCommand(path, this.config.editor), path),
|
|
1147
|
+
openInTerminal: (path) => this.launch(() => terminalCommand(path, this.config.terminal), path),
|
|
1148
|
+
runVerify: () => this.runVerify(),
|
|
1149
|
+
remoteMachines: () => this.config.remoteMachines,
|
|
1150
|
+
sshTo: (machineId) => this.sshTo(machineId),
|
|
1049
1151
|
quit: () => void this.quit(),
|
|
1050
1152
|
}
|
|
1051
1153
|
|
|
@@ -1058,6 +1160,8 @@ export class App {
|
|
|
1058
1160
|
logs: { get: () => this.logs, enumerable: true },
|
|
1059
1161
|
system: { get: () => this.system, enumerable: true },
|
|
1060
1162
|
deps: { get: () => this.deps, enumerable: true },
|
|
1163
|
+
agents: { get: () => this.agents, enumerable: true },
|
|
1164
|
+
environment: { get: () => this.environment, enumerable: true },
|
|
1061
1165
|
})
|
|
1062
1166
|
|
|
1063
1167
|
return context
|
|
@@ -1262,6 +1366,9 @@ export class App {
|
|
|
1262
1366
|
scripts: { available: session.scripts, runs: session.scriptRuns },
|
|
1263
1367
|
git: session.git.getSnapshot(),
|
|
1264
1368
|
dependencies: session.deps.getSnapshot(),
|
|
1369
|
+
agents: session.agents.sessions(),
|
|
1370
|
+
environment: session.environment.getSnapshot(),
|
|
1371
|
+
verify: session.verify,
|
|
1265
1372
|
system: session.system.getMetrics(),
|
|
1266
1373
|
logSources: session.logs.sources(),
|
|
1267
1374
|
projects: [],
|
|
@@ -1349,6 +1456,240 @@ export class App {
|
|
|
1349
1456
|
)
|
|
1350
1457
|
}
|
|
1351
1458
|
|
|
1459
|
+
// ---------------------------------------------------------------------
|
|
1460
|
+
// Worktrees, agents, verification, remote
|
|
1461
|
+
// ---------------------------------------------------------------------
|
|
1462
|
+
|
|
1463
|
+
private selectedWorktree(): GitWorktree | undefined {
|
|
1464
|
+
const state = this.store.getState()
|
|
1465
|
+
if (state.ui.view === "worktrees") {
|
|
1466
|
+
const entry = selectedWorktreeEntry(state)
|
|
1467
|
+
if (entry?.worktree) return entry.worktree
|
|
1468
|
+
}
|
|
1469
|
+
return state.git.worktrees.find((worktree) => worktree.isCurrent)
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private async createWorktree(): Promise<void> {
|
|
1473
|
+
const snapshot = this.git.getSnapshot()
|
|
1474
|
+
if (!snapshot.isRepository) {
|
|
1475
|
+
this.pushNotification("warn", "Not a git repository")
|
|
1476
|
+
return
|
|
1477
|
+
}
|
|
1478
|
+
const main = snapshot.worktrees.find((worktree) => worktree.isMain)
|
|
1479
|
+
if (!main) {
|
|
1480
|
+
this.pushNotification("warn", "Could not read this repository's worktrees yet")
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const branch = (
|
|
1485
|
+
await this.askPrompt("New worktree: branch (created if it does not exist)", "feature/name")
|
|
1486
|
+
)?.trim()
|
|
1487
|
+
if (!branch) return
|
|
1488
|
+
const current = snapshot.status.branch ?? "HEAD"
|
|
1489
|
+
const base = await this.askPrompt(`Base for ${branch} (empty: ${current})`, current)
|
|
1490
|
+
if (base === null) return
|
|
1491
|
+
const suggested = defaultWorktreePath(main.path, branch, this.config.worktrees?.directory)
|
|
1492
|
+
const path = await this.askPrompt(`Directory (empty: ${tildify(suggested)})`, tildify(suggested))
|
|
1493
|
+
if (path === null) return
|
|
1494
|
+
|
|
1495
|
+
const target = path.trim() === "" ? suggested : resolve(main.path, expandHome(path.trim()))
|
|
1496
|
+
try {
|
|
1497
|
+
this.footer.setStatus(`Creating worktree ${branch}…`)
|
|
1498
|
+
await this.git.createWorktree({ branch, base: base.trim() || undefined, path: target })
|
|
1499
|
+
this.pushNotification("success", `Created worktree ${branch} at ${tildify(target)}`)
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
const devccError = toDevccError(error, "git")
|
|
1502
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
private async removeWorktree(worktree: GitWorktree): Promise<void> {
|
|
1507
|
+
const busy = [
|
|
1508
|
+
...this.agents.busyPaths(),
|
|
1509
|
+
...this.processes
|
|
1510
|
+
.list()
|
|
1511
|
+
.filter((entry) => isActive(entry.status))
|
|
1512
|
+
.map((entry) => resolve(this.session.rootDir, entry.cwd ?? ".")),
|
|
1513
|
+
// A worktree open as a project in devcc is in use too.
|
|
1514
|
+
...[...this.sessions.keys()].filter((path) => path !== this.session.rootDir),
|
|
1515
|
+
]
|
|
1516
|
+
const blocker = removalBlocker(worktree, busy)
|
|
1517
|
+
if (blocker) {
|
|
1518
|
+
this.pushNotification("warn", blocker)
|
|
1519
|
+
return
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const confirmed = await this.askConfirm(
|
|
1523
|
+
`Remove worktree ${worktreeLabel(worktree)}?`,
|
|
1524
|
+
`Deletes ${tildify(worktree.path)}. The branch is kept. git refuses if there are uncommitted or untracked changes; devcc never forces it.`,
|
|
1525
|
+
)
|
|
1526
|
+
if (!confirmed) return
|
|
1527
|
+
|
|
1528
|
+
try {
|
|
1529
|
+
await this.git.removeWorktree(worktree.path, busy)
|
|
1530
|
+
this.pushNotification("success", `Removed worktree ${worktreeLabel(worktree)}`)
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
const devccError = toDevccError(error, "git")
|
|
1533
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
private async launchAgent(agentId: string, worktree?: GitWorktree): Promise<void> {
|
|
1538
|
+
const adapter = this.agents.get(agentId)
|
|
1539
|
+
if (!adapter) {
|
|
1540
|
+
this.pushNotification("warn", `Unknown agent "${agentId}"`)
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
1543
|
+
const target = worktree ?? this.selectedWorktree()
|
|
1544
|
+
const cwd = target?.path ?? this.session.rootDir
|
|
1545
|
+
const where = target ? worktreeLabel(target) : basename(cwd)
|
|
1546
|
+
|
|
1547
|
+
const task = await this.askPrompt(
|
|
1548
|
+
`Task for ${adapter.name} in ${where}`,
|
|
1549
|
+
"Describe what the agent should do",
|
|
1550
|
+
)
|
|
1551
|
+
if (task === null || task.trim() === "") return
|
|
1552
|
+
|
|
1553
|
+
try {
|
|
1554
|
+
const session = await this.agents.launch(agentId, { cwd, prompt: task, branch: target?.branch })
|
|
1555
|
+
updateUi(this.store, { logSource: session.id })
|
|
1556
|
+
this.pushNotification("success", `${adapter.name} started in ${where}`)
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
const devccError = toDevccError(error, "process")
|
|
1559
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** Build and start an external launch; a missing configuration becomes a notification. */
|
|
1564
|
+
private async launch(build: () => LaunchCommand, cwd: string): Promise<void> {
|
|
1565
|
+
try {
|
|
1566
|
+
const command = build()
|
|
1567
|
+
await launchDetached(command, cwd)
|
|
1568
|
+
this.logger.log("debug", "launch", "opened", { command: command.command, cwd })
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
const devccError = toDevccError(error, "command")
|
|
1571
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private async sshTo(machineId: string): Promise<void> {
|
|
1576
|
+
const machine = this.config.remoteMachines.find((entry) => entry.id === machineId)
|
|
1577
|
+
if (!machine) {
|
|
1578
|
+
this.pushNotification("warn", `Unknown remote machine "${machineId}"`)
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1581
|
+
await this.launch(() => sshCommand(machine, this.config.terminal), this.session.rootDir)
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Run verification through the ProcessManager: each step is a managed task,
|
|
1586
|
+
* so its output streams into Logs and quitting stops it like anything else.
|
|
1587
|
+
*/
|
|
1588
|
+
private async runVerify(): Promise<void> {
|
|
1589
|
+
const session = this.session
|
|
1590
|
+
if (session.verify.running) {
|
|
1591
|
+
this.pushNotification("warn", "Verification is already running")
|
|
1592
|
+
return
|
|
1593
|
+
}
|
|
1594
|
+
if (session.verifyIssues.length > 0) {
|
|
1595
|
+
this.pushNotification(
|
|
1596
|
+
"error",
|
|
1597
|
+
`Verify refused: ${session.verifyIssues[0] ?? "invalid verify config"}`,
|
|
1598
|
+
session.verifyIssues.join("\n"),
|
|
1599
|
+
)
|
|
1600
|
+
return
|
|
1601
|
+
}
|
|
1602
|
+
if (session.verifySteps.length === 0) {
|
|
1603
|
+
this.pushNotification(
|
|
1604
|
+
"warn",
|
|
1605
|
+
"No verification steps configured",
|
|
1606
|
+
'Add verify: ["lint", "test"] to .devcc.ts',
|
|
1607
|
+
)
|
|
1608
|
+
return
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const controller = new AbortController()
|
|
1612
|
+
session.verifyAbort = controller
|
|
1613
|
+
const publish = (verify: VerifyState) => {
|
|
1614
|
+
session.verify = verify
|
|
1615
|
+
if (session === this.session) this.store.patch({ verify })
|
|
1616
|
+
}
|
|
1617
|
+
const startedAt = new Date()
|
|
1618
|
+
publish({ configured: session.verifySteps.length, running: true, steps: [], startedAt })
|
|
1619
|
+
this.pushNotification("info", `Verifying: ${session.verifySteps.map((step) => step.name).join(", ")}`)
|
|
1620
|
+
|
|
1621
|
+
const report = await runVerification(session.verifySteps, this.processStepExecutor(session), {
|
|
1622
|
+
signal: controller.signal,
|
|
1623
|
+
onProgress: (steps) => publish({ ...session.verify, steps: [...steps] }),
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
session.verifyAbort = null
|
|
1627
|
+
publish({
|
|
1628
|
+
configured: session.verifySteps.length,
|
|
1629
|
+
running: false,
|
|
1630
|
+
steps: report.steps,
|
|
1631
|
+
startedAt,
|
|
1632
|
+
finishedAt: new Date(),
|
|
1633
|
+
ok: report.ok,
|
|
1634
|
+
})
|
|
1635
|
+
this.pushNotification(report.ok ? "success" : "error", `Verify: ${summariseVerify(report)}`)
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
private processStepExecutor(session: ProjectSession): StepExecutor {
|
|
1639
|
+
return async (step, signal) => {
|
|
1640
|
+
const { processes } = session
|
|
1641
|
+
const id = `verify:${step.id}`
|
|
1642
|
+
if (processes.isRunning(id)) return { exitCode: null, error: "already running" }
|
|
1643
|
+
|
|
1644
|
+
processes.register({
|
|
1645
|
+
id,
|
|
1646
|
+
name: `verify · ${step.name}`,
|
|
1647
|
+
command: step.command,
|
|
1648
|
+
args: step.args,
|
|
1649
|
+
cwd: step.cwd,
|
|
1650
|
+
env: { NO_COLOR: "1" },
|
|
1651
|
+
kind: "task",
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
let onChange: ((snapshot: ManagedProcess) => void) | undefined
|
|
1655
|
+
const finished = new Promise<ManagedProcess>((resolveFinished) => {
|
|
1656
|
+
let started = false
|
|
1657
|
+
onChange = (snapshot: ManagedProcess) => {
|
|
1658
|
+
if (snapshot.id !== id) return
|
|
1659
|
+
if (snapshot.status === "starting" || snapshot.status === "running") started = true
|
|
1660
|
+
else if (started && (snapshot.status === "stopped" || snapshot.status === "failed")) {
|
|
1661
|
+
resolveFinished(snapshot)
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
processes.on("process:changed", onChange)
|
|
1665
|
+
})
|
|
1666
|
+
const onAbort = () => void processes.stop(id).catch(() => undefined)
|
|
1667
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
1668
|
+
|
|
1669
|
+
try {
|
|
1670
|
+
if (session === this.session) updateUi(this.store, { logSource: id })
|
|
1671
|
+
await processes.start(id)
|
|
1672
|
+
const result = await finished
|
|
1673
|
+
return {
|
|
1674
|
+
exitCode: result.exitCode ?? null,
|
|
1675
|
+
error: signal.aborted
|
|
1676
|
+
? "cancelled"
|
|
1677
|
+
: result.exitSignal
|
|
1678
|
+
? `terminated by ${result.exitSignal}`
|
|
1679
|
+
: result.exitCode === undefined
|
|
1680
|
+
? result.lastError
|
|
1681
|
+
: undefined,
|
|
1682
|
+
}
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
return { exitCode: null, error: errorMessage(error) }
|
|
1685
|
+
} finally {
|
|
1686
|
+
signal.removeEventListener("abort", onAbort)
|
|
1687
|
+
// Also runs when start() throws, so the listener never outlives the step.
|
|
1688
|
+
if (onChange) processes.off("process:changed", onChange)
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1352
1693
|
// ---------------------------------------------------------------------
|
|
1353
1694
|
// Dependencies
|
|
1354
1695
|
// ---------------------------------------------------------------------
|
|
@@ -1404,6 +1745,7 @@ export class App {
|
|
|
1404
1745
|
this.footer.setStatus("Shutting down…")
|
|
1405
1746
|
|
|
1406
1747
|
this.stopBackground()
|
|
1748
|
+
for (const session of this.sessions.values()) session.verifyAbort?.abort()
|
|
1407
1749
|
|
|
1408
1750
|
// Every open project, not just the visible one.
|
|
1409
1751
|
await Promise.all(
|
|
@@ -1461,3 +1803,17 @@ export class App {
|
|
|
1461
1803
|
return this.logs
|
|
1462
1804
|
}
|
|
1463
1805
|
}
|
|
1806
|
+
|
|
1807
|
+
/** The header and System view still read `project.tools`; derive it from environment detection. */
|
|
1808
|
+
function toolVersionsFrom(snapshot: EnvironmentSnapshot, packageManager: string): ToolVersions {
|
|
1809
|
+
const version = (id: string) => {
|
|
1810
|
+
const tool = snapshot.tools.find((entry) => entry.id === id)
|
|
1811
|
+
return tool?.installed ? (tool.version ?? "installed") : null
|
|
1812
|
+
}
|
|
1813
|
+
return { git: version("git"), docker: version("docker"), packageManager: version(packageManager) }
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function expandHome(path: string): string {
|
|
1817
|
+
const home = process.env.HOME
|
|
1818
|
+
return home && (path === "~" || path.startsWith("~/")) ? `${home}${path.slice(1)}` : path
|
|
1819
|
+
}
|