@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.
- package/README.md +589 -389
- package/bin/devcc.mjs +50 -23
- package/dist/devcc/agents/agents.d.ts +119 -0
- package/dist/devcc/agents/agents.d.ts.map +1 -0
- package/dist/devcc/agents/agents.js +182 -0
- package/dist/devcc/agents/agents.js.map +1 -0
- package/dist/devcc/app/App.d.ts +23 -0
- package/dist/devcc/app/App.d.ts.map +1 -1
- package/dist/devcc/app/App.js +451 -19
- 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 +204 -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 +25 -3
- package/dist/devcc/app/keymap.js.map +1 -1
- package/dist/devcc/automation.d.ts +52 -0
- package/dist/devcc/automation.d.ts.map +1 -0
- package/dist/devcc/automation.js +339 -0
- package/dist/devcc/automation.js.map +1 -0
- package/dist/devcc/cli.d.ts.map +1 -1
- package/dist/devcc/cli.js +73 -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 +33 -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 +71 -3
- package/dist/devcc/core/config.d.ts.map +1 -1
- package/dist/devcc/core/config.js +189 -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 +15 -3
- package/dist/devcc/index.d.ts.map +1 -1
- package/dist/devcc/index.js +14 -2
- package/dist/devcc/index.js.map +1 -1
- package/dist/devcc/init/detect.d.ts +55 -0
- package/dist/devcc/init/detect.d.ts.map +1 -0
- package/dist/devcc/init/detect.js +207 -0
- package/dist/devcc/init/detect.js.map +1 -0
- package/dist/devcc/init/prompt.d.ts +25 -0
- package/dist/devcc/init/prompt.d.ts.map +1 -0
- package/dist/devcc/init/prompt.js +156 -0
- package/dist/devcc/init/prompt.js.map +1 -0
- 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/node.d.ts +11 -0
- package/dist/devcc/services/node.d.ts.map +1 -1
- package/dist/devcc/services/node.js +20 -18
- package/dist/devcc/services/node.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 +58 -2
- 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 +292 -0
- package/src/devcc/app/App.ts +529 -22
- package/src/devcc/app/controlPlaneCommands.ts +227 -0
- package/src/devcc/app/keymap.ts +25 -3
- package/src/devcc/automation.ts +409 -0
- package/src/devcc/cli.ts +77 -3
- package/src/devcc/components/Sidebar.ts +1 -0
- package/src/devcc/components/primitives.ts +3 -0
- package/src/devcc/core/commands.ts +34 -0
- package/src/devcc/core/config.ts +275 -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 +66 -2
- package/src/devcc/init/detect.ts +268 -0
- package/src/devcc/init/prompt.ts +179 -0
- package/src/devcc/platform/launch.ts +125 -0
- package/src/devcc/services/ServiceManager.ts +15 -3
- package/src/devcc/services/node.ts +17 -14
- 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 +84 -2
- 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,10 @@
|
|
|
1
1
|
import { BoxRenderable, createCliRenderer, type CliRenderer, type KeyEvent } from "@opentui/core"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { basename, join, resolve } from "node:path"
|
|
4
|
+
import { AgentManager, createAgentAdapters } from "../agents/agents.js"
|
|
5
|
+
import { configProblems } from "../automation.js"
|
|
6
|
+
import { detectProject, renderStarterConfig } from "../init/detect.js"
|
|
7
|
+
import { buildSetupPrompt, extractConfigFromOutput } from "../init/prompt.js"
|
|
4
8
|
import { ConfirmDialog, PromptDialog } from "../components/ConfirmDialog.js"
|
|
5
9
|
import { Footer, type KeyHint } from "../components/Footer.js"
|
|
6
10
|
import { Header } from "../components/Header.js"
|
|
@@ -10,7 +14,8 @@ import { Sidebar } from "../components/Sidebar.js"
|
|
|
10
14
|
import { CommandPalette } from "../components/CommandPalette.js"
|
|
11
15
|
import { ProjectPicker } from "../components/ProjectPicker.js"
|
|
12
16
|
import { CommandRegistry, type CommandContext } from "../core/commands.js"
|
|
13
|
-
import { loadConfig, type CommandConfigEntry, type ResolvedConfig } from "../core/config.js"
|
|
17
|
+
import { loadConfig, loadConfigFile, type CommandConfigEntry, type ResolvedConfig } from "../core/config.js"
|
|
18
|
+
import { EnvironmentService, type EnvironmentSnapshot } from "../environment/EnvironmentService.js"
|
|
14
19
|
import { Subscriptions } from "../core/events.js"
|
|
15
20
|
import {
|
|
16
21
|
createInitialSelection,
|
|
@@ -26,10 +31,12 @@ import {
|
|
|
26
31
|
type NotificationLevel,
|
|
27
32
|
type ProjectState,
|
|
28
33
|
type ScriptRun,
|
|
34
|
+
type VerifyState,
|
|
29
35
|
type ViewId,
|
|
30
36
|
} from "../core/state.js"
|
|
31
37
|
import { DependencyService, updateArgs } from "../deps/DependencyService.js"
|
|
32
38
|
import { GitService } from "../git/GitService.js"
|
|
39
|
+
import { defaultWorktreePath, removalBlocker, worktreeLabel, type GitWorktree } from "../git/worktrees.js"
|
|
33
40
|
import {
|
|
34
41
|
buildProjectList,
|
|
35
42
|
defaultSearchDirs,
|
|
@@ -40,20 +47,36 @@ import {
|
|
|
40
47
|
} from "../projects/ProjectRegistry.js"
|
|
41
48
|
import { LogStore, MAX_LOG_LINES } from "../logs/LogBuffer.js"
|
|
42
49
|
import { ALL_SOURCES } from "../logs/merge.js"
|
|
43
|
-
import { isActive } from "../processes/ManagedProcess.js"
|
|
50
|
+
import { isActive, type ManagedProcess } from "../processes/ManagedProcess.js"
|
|
51
|
+
import {
|
|
52
|
+
editorCommand,
|
|
53
|
+
launchDetached,
|
|
54
|
+
sshCommand,
|
|
55
|
+
terminalCommand,
|
|
56
|
+
type LaunchCommand,
|
|
57
|
+
} from "../platform/launch.js"
|
|
44
58
|
import { ProcessManager } from "../processes/ProcessManager.js"
|
|
45
59
|
import { DockerService } from "../services/docker.js"
|
|
46
60
|
import { detectNodeServices, PortService } from "../services/node.js"
|
|
47
61
|
import { OllamaService } from "../services/ollama.js"
|
|
48
62
|
import { PostgresService } from "../services/postgres.js"
|
|
49
63
|
import { RedisService } from "../services/redis.js"
|
|
64
|
+
import { RemoteMachineService, TailscaleService } from "../services/remote.js"
|
|
50
65
|
import { ServiceManager } from "../services/ServiceManager.js"
|
|
51
|
-
import { SystemMonitor } from "../system/SystemMonitor.js"
|
|
66
|
+
import { SystemMonitor, type ToolVersions } from "../system/SystemMonitor.js"
|
|
52
67
|
import { errorMessage, toDevccError } from "../utils/errors.js"
|
|
53
68
|
import { createLogger, type Logger } from "../utils/logger.js"
|
|
54
69
|
import { detectPackageManager, readPackageJson, readScripts, runScriptArgs } from "../utils/packageManager.js"
|
|
55
70
|
import { fuzzyFilter } from "../utils/fuzzy.js"
|
|
56
|
-
import { findProjectRoot } from "../utils/paths.js"
|
|
71
|
+
import { findProjectRoot, tildify } from "../utils/paths.js"
|
|
72
|
+
import {
|
|
73
|
+
isVerifyIssue,
|
|
74
|
+
resolveVerifySteps,
|
|
75
|
+
runVerification,
|
|
76
|
+
summariseVerify,
|
|
77
|
+
type StepExecutor,
|
|
78
|
+
type VerifyStep,
|
|
79
|
+
} from "../verify/verify.js"
|
|
57
80
|
import { DependenciesView } from "../views/DependenciesView.js"
|
|
58
81
|
import { GitView } from "../views/GitView.js"
|
|
59
82
|
import { LogsView } from "../views/LogsView.js"
|
|
@@ -63,8 +86,10 @@ import { ScriptsView, scriptProcessId } from "../views/ScriptsView.js"
|
|
|
63
86
|
import { ServicesView } from "../views/ServicesView.js"
|
|
64
87
|
import { SettingsView } from "../views/SettingsView.js"
|
|
65
88
|
import { SystemView } from "../views/SystemView.js"
|
|
89
|
+
import { WorktreesView, selectedWorktreeEntry } from "../views/WorktreesView.js"
|
|
66
90
|
import type { View, ViewDeps } from "../views/View.js"
|
|
67
91
|
import { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./builtinCommands.js"
|
|
92
|
+
import { createControlPlaneCommands } from "./controlPlaneCommands.js"
|
|
68
93
|
import { HELP_SECTIONS, VIEW_SHORTCUTS } from "./keymap.js"
|
|
69
94
|
import { theme } from "./theme.js"
|
|
70
95
|
|
|
@@ -101,7 +126,17 @@ interface ProjectSession {
|
|
|
101
126
|
git: GitService
|
|
102
127
|
system: SystemMonitor
|
|
103
128
|
deps: DependencyService
|
|
129
|
+
agents: AgentManager
|
|
130
|
+
environment: EnvironmentService
|
|
104
131
|
project: ProjectState
|
|
132
|
+
/** Verification steps resolved from config, and the latest run. */
|
|
133
|
+
verifySteps: VerifyStep[]
|
|
134
|
+
verify: VerifyState
|
|
135
|
+
/** Config problems that make verification untrustworthy; running is refused while any exist. */
|
|
136
|
+
verifyIssues: string[]
|
|
137
|
+
verifyAbort: AbortController | null
|
|
138
|
+
/** The agent session proposing a .devcc.ts, if one is running. */
|
|
139
|
+
setupAgentSessionId: string | null
|
|
105
140
|
scripts: { name: string; command: string }[]
|
|
106
141
|
/** Script runs belong to the project they ran in, not to the store. */
|
|
107
142
|
scriptRuns: Record<string, ScriptRun>
|
|
@@ -151,6 +186,12 @@ export class App {
|
|
|
151
186
|
private get deps(): DependencyService {
|
|
152
187
|
return this.session.deps
|
|
153
188
|
}
|
|
189
|
+
private get agents(): AgentManager {
|
|
190
|
+
return this.session.agents
|
|
191
|
+
}
|
|
192
|
+
private get environment(): EnvironmentService {
|
|
193
|
+
return this.session.environment
|
|
194
|
+
}
|
|
154
195
|
private get config(): ResolvedConfig {
|
|
155
196
|
return this.session.config
|
|
156
197
|
}
|
|
@@ -242,21 +283,36 @@ export class App {
|
|
|
242
283
|
logger: logger.child("services"),
|
|
243
284
|
intervalMs: config.refresh.services,
|
|
244
285
|
})
|
|
286
|
+
const tailscale = new TailscaleService({
|
|
287
|
+
machines: config.remoteMachines,
|
|
288
|
+
logger: logger.child("tailscale"),
|
|
289
|
+
})
|
|
245
290
|
services.registerAll([
|
|
246
291
|
new DockerService({ rootDir }),
|
|
247
292
|
new PostgresService(),
|
|
248
293
|
new RedisService(),
|
|
249
294
|
new OllamaService(),
|
|
250
295
|
...detectNodeServices(packageJson),
|
|
251
|
-
...config.services.map(
|
|
252
|
-
|
|
253
|
-
new
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
296
|
+
...config.services.map((entry) =>
|
|
297
|
+
entry.type === "ollama"
|
|
298
|
+
? new OllamaService({ id: entry.id, name: entry.name, baseUrl: entry.baseUrl })
|
|
299
|
+
: new PortService({
|
|
300
|
+
id: entry.id,
|
|
301
|
+
name: entry.name,
|
|
302
|
+
port: entry.port,
|
|
303
|
+
host: entry.host,
|
|
304
|
+
healthPath: entry.healthPath,
|
|
305
|
+
category: entry.category,
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
tailscale,
|
|
309
|
+
...config.remoteMachines.map(
|
|
310
|
+
(machine) =>
|
|
311
|
+
new RemoteMachineService({
|
|
312
|
+
machine,
|
|
313
|
+
tailscale: () => tailscale.getLastStatus(),
|
|
314
|
+
service: (id) => services.get(id),
|
|
315
|
+
logger: logger.child("remote"),
|
|
260
316
|
}),
|
|
261
317
|
),
|
|
262
318
|
])
|
|
@@ -267,13 +323,25 @@ export class App {
|
|
|
267
323
|
manager: packageManager.manager,
|
|
268
324
|
logger: logger.child("deps"),
|
|
269
325
|
})
|
|
326
|
+
const agents = new AgentManager({
|
|
327
|
+
processes,
|
|
328
|
+
adapters: createAgentAdapters(config.agents),
|
|
329
|
+
logger: logger.child("agents"),
|
|
330
|
+
})
|
|
331
|
+
const environment = new EnvironmentService({ rootDir, logger: logger.child("environment") })
|
|
332
|
+
const scripts = readScripts(packageJson)
|
|
333
|
+
const resolvedVerify = resolveVerifySteps(config.verify, {
|
|
334
|
+
commands: config.commands,
|
|
335
|
+
scripts,
|
|
336
|
+
packageManager: packageManager.manager,
|
|
337
|
+
})
|
|
270
338
|
|
|
271
339
|
const project: ProjectState = {
|
|
272
340
|
name: config.name ?? (typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir)),
|
|
273
341
|
rootDir,
|
|
274
342
|
packageManager,
|
|
275
343
|
configPath: loaded.filePath,
|
|
276
|
-
configIssues: loaded.issues,
|
|
344
|
+
configIssues: [...loaded.issues, ...resolvedVerify.issues],
|
|
277
345
|
host: system.getHostInfo(),
|
|
278
346
|
tools: { git: null, docker: null, packageManager: null },
|
|
279
347
|
}
|
|
@@ -291,8 +359,15 @@ export class App {
|
|
|
291
359
|
git,
|
|
292
360
|
system,
|
|
293
361
|
deps,
|
|
362
|
+
agents,
|
|
363
|
+
environment,
|
|
294
364
|
project,
|
|
295
|
-
|
|
365
|
+
verifySteps: resolvedVerify.steps,
|
|
366
|
+
verify: { configured: resolvedVerify.steps.length, running: false, steps: [] },
|
|
367
|
+
verifyIssues: [...loaded.issues.filter(isVerifyIssue), ...resolvedVerify.issues],
|
|
368
|
+
verifyAbort: null,
|
|
369
|
+
setupAgentSessionId: null,
|
|
370
|
+
scripts,
|
|
296
371
|
scriptRuns: {},
|
|
297
372
|
wiring: new Subscriptions(),
|
|
298
373
|
timers: [],
|
|
@@ -307,12 +382,21 @@ export class App {
|
|
|
307
382
|
this.commands.registerAll(
|
|
308
383
|
createConfigCommands(this.config.commands, (entry) => this.runConfigCommand(entry)),
|
|
309
384
|
)
|
|
385
|
+
this.commands.registerAll(
|
|
386
|
+
createControlPlaneCommands({
|
|
387
|
+
agents: this.session.agents.list(),
|
|
388
|
+
machines: this.config.remoteMachines,
|
|
389
|
+
}),
|
|
390
|
+
)
|
|
310
391
|
}
|
|
311
392
|
|
|
312
393
|
private reportConfigIssues(): void {
|
|
313
394
|
for (const issue of this.session.project.configIssues) {
|
|
314
395
|
this.pushNotification("warn", `Config: ${issue}`)
|
|
315
396
|
}
|
|
397
|
+
if (!this.session.project.configPath) {
|
|
398
|
+
this.pushNotification("info", "No .devcc.ts yet: press / and run “Project: set up .devcc.ts”")
|
|
399
|
+
}
|
|
316
400
|
}
|
|
317
401
|
|
|
318
402
|
// ---------------------------------------------------------------------
|
|
@@ -366,6 +450,7 @@ export class App {
|
|
|
366
450
|
new ServicesView(deps),
|
|
367
451
|
new ScriptsView(deps),
|
|
368
452
|
new GitView(deps),
|
|
453
|
+
new WorktreesView(deps),
|
|
369
454
|
new LogsView(deps),
|
|
370
455
|
new DependenciesView(deps),
|
|
371
456
|
new SystemView(deps),
|
|
@@ -429,13 +514,13 @@ export class App {
|
|
|
429
514
|
*/
|
|
430
515
|
private wireSession(session: ProjectSession): void {
|
|
431
516
|
const { store } = this
|
|
432
|
-
const { wiring, processes, services, git, system, logs, deps } = session
|
|
517
|
+
const { wiring, processes, services, git, system, logs, deps, environment } = session
|
|
433
518
|
const active = () => this.session === session
|
|
434
519
|
const notify = (level: NotificationLevel, message: string, detail?: string) =>
|
|
435
520
|
this.pushNotification(level, active() ? message : `[${session.project.name}] ${message}`, detail)
|
|
436
521
|
|
|
437
522
|
const syncProcesses = () => {
|
|
438
|
-
if (active()) store.patch({ processes: processes.list() })
|
|
523
|
+
if (active()) store.patch({ processes: processes.list(), agents: session.agents.sessions() })
|
|
439
524
|
this.refreshSidebarProjects()
|
|
440
525
|
}
|
|
441
526
|
processes.on("process:changed", syncProcesses)
|
|
@@ -443,6 +528,7 @@ export class App {
|
|
|
443
528
|
processes.on("process:exit", (process) => {
|
|
444
529
|
this.finishScriptRun(session, process.id, process.exitCode ?? 0)
|
|
445
530
|
syncProcesses()
|
|
531
|
+
if (process.id === session.setupAgentSessionId) void this.finishConfigSetup(session, process)
|
|
446
532
|
})
|
|
447
533
|
processes.on("process:error", (error) => notify("error", error.message, error.detail))
|
|
448
534
|
processes.on("process:retry", (managed, delayMs) => {
|
|
@@ -473,11 +559,16 @@ export class App {
|
|
|
473
559
|
system.on("system:metrics", (metrics) => {
|
|
474
560
|
if (active()) store.patch({ system: metrics })
|
|
475
561
|
})
|
|
476
|
-
|
|
562
|
+
wiring.add(() => system.removeAllListeners())
|
|
563
|
+
|
|
564
|
+
environment.on("environment:changed", (snapshot) => {
|
|
565
|
+
const tools = toolVersionsFrom(snapshot, session.project.packageManager.manager)
|
|
477
566
|
session.project = { ...session.project, tools }
|
|
478
|
-
if (active())
|
|
567
|
+
if (active()) {
|
|
568
|
+
store.setState((state) => ({ ...state, environment: snapshot, project: { ...state.project, tools } }))
|
|
569
|
+
}
|
|
479
570
|
})
|
|
480
|
-
wiring.add(() =>
|
|
571
|
+
wiring.add(() => environment.removeAllListeners())
|
|
481
572
|
|
|
482
573
|
logs.on("log:sources", (sources) => {
|
|
483
574
|
if (!active()) return
|
|
@@ -540,6 +631,9 @@ export class App {
|
|
|
540
631
|
this.subscriptions.add(store.watch((state) => state.services, markDirty, shallowArrayEqual))
|
|
541
632
|
this.subscriptions.add(store.watch((state) => state.git, markDirty))
|
|
542
633
|
this.subscriptions.add(store.watch((state) => state.dependencies, markDirty))
|
|
634
|
+
this.subscriptions.add(store.watch((state) => state.agents, markDirty, shallowArrayEqual))
|
|
635
|
+
this.subscriptions.add(store.watch((state) => state.environment, markDirty))
|
|
636
|
+
this.subscriptions.add(store.watch((state) => state.verify, markDirty))
|
|
543
637
|
this.subscriptions.add(store.watch((state) => state.projects, markDirty, shallowArrayEqual))
|
|
544
638
|
this.subscriptions.add(store.watch((state) => state.system, markDirty))
|
|
545
639
|
this.subscriptions.add(store.watch((state) => state.scripts, markDirty))
|
|
@@ -557,6 +651,7 @@ export class App {
|
|
|
557
651
|
processes: this.processes.list(),
|
|
558
652
|
services: this.services.list(),
|
|
559
653
|
logSources: this.logs.sources(),
|
|
654
|
+
verify: this.session.verify,
|
|
560
655
|
})
|
|
561
656
|
store.setState((state) => ({
|
|
562
657
|
...state,
|
|
@@ -596,7 +691,8 @@ export class App {
|
|
|
596
691
|
|
|
597
692
|
void session.git.refresh()
|
|
598
693
|
void session.services.detectAll().then(() => session.services.start())
|
|
599
|
-
|
|
694
|
+
// Tool versions change rarely: detect once per project, refresh on request.
|
|
695
|
+
if (!session.environment.getSnapshot().checkedAt) void session.environment.refresh()
|
|
600
696
|
if (!session.processes.list().some((entry) => entry.startedAt)) {
|
|
601
697
|
// Autostart only the first time a project is opened, never on reactivation.
|
|
602
698
|
void session.processes.startAutoStart()
|
|
@@ -759,6 +855,11 @@ export class App {
|
|
|
759
855
|
void this.commands.run("app.refresh", this.context)
|
|
760
856
|
return
|
|
761
857
|
}
|
|
858
|
+
if (key.shift && key.name === "v") {
|
|
859
|
+
key.preventDefault()
|
|
860
|
+
void this.runCommand("verify.run")
|
|
861
|
+
return
|
|
862
|
+
}
|
|
762
863
|
|
|
763
864
|
const state = this.store.getState()
|
|
764
865
|
|
|
@@ -1035,6 +1136,8 @@ export class App {
|
|
|
1035
1136
|
logs: this.logs,
|
|
1036
1137
|
system: this.system,
|
|
1037
1138
|
deps: this.deps,
|
|
1139
|
+
agents: this.agents,
|
|
1140
|
+
environment: this.environment,
|
|
1038
1141
|
navigate: (view) => updateUi(this.store, { view }),
|
|
1039
1142
|
notify: (level, message, detail) => this.pushNotification(level, message, detail),
|
|
1040
1143
|
confirm: (title, message) => this.askConfirm(title, message),
|
|
@@ -1046,6 +1149,17 @@ export class App {
|
|
|
1046
1149
|
openProjects: () => this.openProjects(),
|
|
1047
1150
|
switchProject: (path) => this.openProject(path),
|
|
1048
1151
|
closeProject: (path) => this.closeProject(path),
|
|
1152
|
+
selectedWorktree: () => this.selectedWorktree(),
|
|
1153
|
+
createWorktree: () => this.createWorktree(),
|
|
1154
|
+
removeWorktree: (worktree) => this.removeWorktree(worktree),
|
|
1155
|
+
launchAgent: (agentId, worktree) => this.launchAgent(agentId, worktree),
|
|
1156
|
+
openInEditor: (path) => this.launch(() => editorCommand(path, this.config.editor), path),
|
|
1157
|
+
openInTerminal: (path) => this.launch(() => terminalCommand(path, this.config.terminal), path),
|
|
1158
|
+
runVerify: () => this.runVerify(),
|
|
1159
|
+
remoteMachines: () => this.config.remoteMachines,
|
|
1160
|
+
sshTo: (machineId) => this.sshTo(machineId),
|
|
1161
|
+
setUpConfig: (how) => this.setUpConfig(how),
|
|
1162
|
+
reloadConfig: () => this.reloadConfig(),
|
|
1049
1163
|
quit: () => void this.quit(),
|
|
1050
1164
|
}
|
|
1051
1165
|
|
|
@@ -1058,6 +1172,8 @@ export class App {
|
|
|
1058
1172
|
logs: { get: () => this.logs, enumerable: true },
|
|
1059
1173
|
system: { get: () => this.system, enumerable: true },
|
|
1060
1174
|
deps: { get: () => this.deps, enumerable: true },
|
|
1175
|
+
agents: { get: () => this.agents, enumerable: true },
|
|
1176
|
+
environment: { get: () => this.environment, enumerable: true },
|
|
1061
1177
|
})
|
|
1062
1178
|
|
|
1063
1179
|
return context
|
|
@@ -1262,6 +1378,9 @@ export class App {
|
|
|
1262
1378
|
scripts: { available: session.scripts, runs: session.scriptRuns },
|
|
1263
1379
|
git: session.git.getSnapshot(),
|
|
1264
1380
|
dependencies: session.deps.getSnapshot(),
|
|
1381
|
+
agents: session.agents.sessions(),
|
|
1382
|
+
environment: session.environment.getSnapshot(),
|
|
1383
|
+
verify: session.verify,
|
|
1265
1384
|
system: session.system.getMetrics(),
|
|
1266
1385
|
logSources: session.logs.sources(),
|
|
1267
1386
|
projects: [],
|
|
@@ -1349,6 +1468,379 @@ export class App {
|
|
|
1349
1468
|
)
|
|
1350
1469
|
}
|
|
1351
1470
|
|
|
1471
|
+
// ---------------------------------------------------------------------
|
|
1472
|
+
// Worktrees, agents, verification, remote
|
|
1473
|
+
// ---------------------------------------------------------------------
|
|
1474
|
+
|
|
1475
|
+
private selectedWorktree(): GitWorktree | undefined {
|
|
1476
|
+
const state = this.store.getState()
|
|
1477
|
+
if (state.ui.view === "worktrees") {
|
|
1478
|
+
const entry = selectedWorktreeEntry(state)
|
|
1479
|
+
if (entry?.worktree) return entry.worktree
|
|
1480
|
+
}
|
|
1481
|
+
return state.git.worktrees.find((worktree) => worktree.isCurrent)
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private async createWorktree(): Promise<void> {
|
|
1485
|
+
const snapshot = this.git.getSnapshot()
|
|
1486
|
+
if (!snapshot.isRepository) {
|
|
1487
|
+
this.pushNotification("warn", "Not a git repository")
|
|
1488
|
+
return
|
|
1489
|
+
}
|
|
1490
|
+
const main = snapshot.worktrees.find((worktree) => worktree.isMain)
|
|
1491
|
+
if (!main) {
|
|
1492
|
+
this.pushNotification("warn", "Could not read this repository's worktrees yet")
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const branch = (
|
|
1497
|
+
await this.askPrompt("New worktree: branch (created if it does not exist)", "feature/name")
|
|
1498
|
+
)?.trim()
|
|
1499
|
+
if (!branch) return
|
|
1500
|
+
const current = snapshot.status.branch ?? "HEAD"
|
|
1501
|
+
const base = await this.askPrompt(`Base for ${branch} (empty: ${current})`, current)
|
|
1502
|
+
if (base === null) return
|
|
1503
|
+
const suggested = defaultWorktreePath(main.path, branch, this.config.worktrees?.directory)
|
|
1504
|
+
const path = await this.askPrompt(`Directory (empty: ${tildify(suggested)})`, tildify(suggested))
|
|
1505
|
+
if (path === null) return
|
|
1506
|
+
|
|
1507
|
+
const target = path.trim() === "" ? suggested : resolve(main.path, expandHome(path.trim()))
|
|
1508
|
+
try {
|
|
1509
|
+
this.footer.setStatus(`Creating worktree ${branch}…`)
|
|
1510
|
+
await this.git.createWorktree({ branch, base: base.trim() || undefined, path: target })
|
|
1511
|
+
this.pushNotification("success", `Created worktree ${branch} at ${tildify(target)}`)
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
const devccError = toDevccError(error, "git")
|
|
1514
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private async removeWorktree(worktree: GitWorktree): Promise<void> {
|
|
1519
|
+
const busy = [
|
|
1520
|
+
...this.agents.busyPaths(),
|
|
1521
|
+
...this.processes
|
|
1522
|
+
.list()
|
|
1523
|
+
.filter((entry) => isActive(entry.status))
|
|
1524
|
+
.map((entry) => resolve(this.session.rootDir, entry.cwd ?? ".")),
|
|
1525
|
+
// A worktree open as a project in devcc is in use too.
|
|
1526
|
+
...[...this.sessions.keys()].filter((path) => path !== this.session.rootDir),
|
|
1527
|
+
]
|
|
1528
|
+
const blocker = removalBlocker(worktree, busy)
|
|
1529
|
+
if (blocker) {
|
|
1530
|
+
this.pushNotification("warn", blocker)
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const confirmed = await this.askConfirm(
|
|
1535
|
+
`Remove worktree ${worktreeLabel(worktree)}?`,
|
|
1536
|
+
`Deletes ${tildify(worktree.path)}. The branch is kept. git refuses if there are uncommitted or untracked changes; devcc never forces it.`,
|
|
1537
|
+
)
|
|
1538
|
+
if (!confirmed) return
|
|
1539
|
+
|
|
1540
|
+
try {
|
|
1541
|
+
await this.git.removeWorktree(worktree.path, busy)
|
|
1542
|
+
this.pushNotification("success", `Removed worktree ${worktreeLabel(worktree)}`)
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
const devccError = toDevccError(error, "git")
|
|
1545
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
private async launchAgent(agentId: string, worktree?: GitWorktree): Promise<void> {
|
|
1550
|
+
const adapter = this.agents.get(agentId)
|
|
1551
|
+
if (!adapter) {
|
|
1552
|
+
this.pushNotification("warn", `Unknown agent "${agentId}"`)
|
|
1553
|
+
return
|
|
1554
|
+
}
|
|
1555
|
+
const target = worktree ?? this.selectedWorktree()
|
|
1556
|
+
const cwd = target?.path ?? this.session.rootDir
|
|
1557
|
+
const where = target ? worktreeLabel(target) : basename(cwd)
|
|
1558
|
+
|
|
1559
|
+
const task = await this.askPrompt(
|
|
1560
|
+
`Task for ${adapter.name} in ${where}`,
|
|
1561
|
+
"Describe what the agent should do",
|
|
1562
|
+
)
|
|
1563
|
+
if (task === null || task.trim() === "") return
|
|
1564
|
+
|
|
1565
|
+
try {
|
|
1566
|
+
const session = await this.agents.launch(agentId, { cwd, prompt: task, branch: target?.branch })
|
|
1567
|
+
updateUi(this.store, { logSource: session.id })
|
|
1568
|
+
this.pushNotification("success", `${adapter.name} started in ${where}`)
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
const devccError = toDevccError(error, "process")
|
|
1571
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/** Build and start an external launch; a missing configuration becomes a notification. */
|
|
1576
|
+
private async launch(build: () => LaunchCommand, cwd: string): Promise<void> {
|
|
1577
|
+
try {
|
|
1578
|
+
const command = build()
|
|
1579
|
+
await launchDetached(command, cwd)
|
|
1580
|
+
this.logger.log("debug", "launch", "opened", { command: command.command, cwd })
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
const devccError = toDevccError(error, "command")
|
|
1583
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
private async sshTo(machineId: string): Promise<void> {
|
|
1588
|
+
const machine = this.config.remoteMachines.find((entry) => entry.id === machineId)
|
|
1589
|
+
if (!machine) {
|
|
1590
|
+
this.pushNotification("warn", `Unknown remote machine "${machineId}"`)
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
await this.launch(() => sshCommand(machine, this.config.terminal), this.session.rootDir)
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Run verification through the ProcessManager: each step is a managed task,
|
|
1598
|
+
* so its output streams into Logs and quitting stops it like anything else.
|
|
1599
|
+
*/
|
|
1600
|
+
private async runVerify(): Promise<void> {
|
|
1601
|
+
const session = this.session
|
|
1602
|
+
if (session.verify.running) {
|
|
1603
|
+
this.pushNotification("warn", "Verification is already running")
|
|
1604
|
+
return
|
|
1605
|
+
}
|
|
1606
|
+
if (session.verifyIssues.length > 0) {
|
|
1607
|
+
this.pushNotification(
|
|
1608
|
+
"error",
|
|
1609
|
+
`Verify refused: ${session.verifyIssues[0] ?? "invalid verify config"}`,
|
|
1610
|
+
session.verifyIssues.join("\n"),
|
|
1611
|
+
)
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1614
|
+
if (session.verifySteps.length === 0) {
|
|
1615
|
+
this.pushNotification(
|
|
1616
|
+
"warn",
|
|
1617
|
+
"No verification steps configured",
|
|
1618
|
+
'Add verify: ["lint", "test"] to .devcc.ts',
|
|
1619
|
+
)
|
|
1620
|
+
return
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const controller = new AbortController()
|
|
1624
|
+
session.verifyAbort = controller
|
|
1625
|
+
const publish = (verify: VerifyState) => {
|
|
1626
|
+
session.verify = verify
|
|
1627
|
+
if (session === this.session) this.store.patch({ verify })
|
|
1628
|
+
}
|
|
1629
|
+
const startedAt = new Date()
|
|
1630
|
+
publish({ configured: session.verifySteps.length, running: true, steps: [], startedAt })
|
|
1631
|
+
this.pushNotification("info", `Verifying: ${session.verifySteps.map((step) => step.name).join(", ")}`)
|
|
1632
|
+
|
|
1633
|
+
const report = await runVerification(session.verifySteps, this.processStepExecutor(session), {
|
|
1634
|
+
signal: controller.signal,
|
|
1635
|
+
onProgress: (steps) => publish({ ...session.verify, steps: [...steps] }),
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1638
|
+
session.verifyAbort = null
|
|
1639
|
+
publish({
|
|
1640
|
+
configured: session.verifySteps.length,
|
|
1641
|
+
running: false,
|
|
1642
|
+
steps: report.steps,
|
|
1643
|
+
startedAt,
|
|
1644
|
+
finishedAt: new Date(),
|
|
1645
|
+
ok: report.ok,
|
|
1646
|
+
})
|
|
1647
|
+
this.pushNotification(report.ok ? "success" : "error", `Verify: ${summariseVerify(report)}`)
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
private processStepExecutor(session: ProjectSession): StepExecutor {
|
|
1651
|
+
return async (step, signal) => {
|
|
1652
|
+
const { processes } = session
|
|
1653
|
+
const id = `verify:${step.id}`
|
|
1654
|
+
if (processes.isRunning(id)) return { exitCode: null, error: "already running" }
|
|
1655
|
+
|
|
1656
|
+
processes.register({
|
|
1657
|
+
id,
|
|
1658
|
+
name: `verify · ${step.name}`,
|
|
1659
|
+
command: step.command,
|
|
1660
|
+
args: step.args,
|
|
1661
|
+
cwd: step.cwd,
|
|
1662
|
+
env: { NO_COLOR: "1" },
|
|
1663
|
+
kind: "task",
|
|
1664
|
+
})
|
|
1665
|
+
|
|
1666
|
+
let onChange: ((snapshot: ManagedProcess) => void) | undefined
|
|
1667
|
+
const finished = new Promise<ManagedProcess>((resolveFinished) => {
|
|
1668
|
+
let started = false
|
|
1669
|
+
onChange = (snapshot: ManagedProcess) => {
|
|
1670
|
+
if (snapshot.id !== id) return
|
|
1671
|
+
if (snapshot.status === "starting" || snapshot.status === "running") started = true
|
|
1672
|
+
else if (started && (snapshot.status === "stopped" || snapshot.status === "failed")) {
|
|
1673
|
+
resolveFinished(snapshot)
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
processes.on("process:changed", onChange)
|
|
1677
|
+
})
|
|
1678
|
+
const onAbort = () => void processes.stop(id).catch(() => undefined)
|
|
1679
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
1680
|
+
|
|
1681
|
+
try {
|
|
1682
|
+
if (session === this.session) updateUi(this.store, { logSource: id })
|
|
1683
|
+
await processes.start(id)
|
|
1684
|
+
const result = await finished
|
|
1685
|
+
return {
|
|
1686
|
+
exitCode: result.exitCode ?? null,
|
|
1687
|
+
error: signal.aborted
|
|
1688
|
+
? "cancelled"
|
|
1689
|
+
: result.exitSignal
|
|
1690
|
+
? `terminated by ${result.exitSignal}`
|
|
1691
|
+
: result.exitCode === undefined
|
|
1692
|
+
? result.lastError
|
|
1693
|
+
: undefined,
|
|
1694
|
+
}
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
return { exitCode: null, error: errorMessage(error) }
|
|
1697
|
+
} finally {
|
|
1698
|
+
signal.removeEventListener("abort", onAbort)
|
|
1699
|
+
// Also runs when start() throws, so the listener never outlives the step.
|
|
1700
|
+
if (onChange) processes.off("process:changed", onChange)
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ---------------------------------------------------------------------
|
|
1706
|
+
// Project setup (.devcc.ts)
|
|
1707
|
+
// ---------------------------------------------------------------------
|
|
1708
|
+
|
|
1709
|
+
private async setUpConfig(how: string): Promise<void> {
|
|
1710
|
+
const session = this.session
|
|
1711
|
+
const facts = detectProject(session.rootDir)
|
|
1712
|
+
|
|
1713
|
+
if (how === "starter") {
|
|
1714
|
+
if (facts.configPath) {
|
|
1715
|
+
const replace = await this.askConfirm(
|
|
1716
|
+
"Replace the existing config with a starter?",
|
|
1717
|
+
`${tildify(facts.configPath)} will be overwritten with what devcc detects. Consider setting it up with an agent instead, which keeps what is still right.`,
|
|
1718
|
+
)
|
|
1719
|
+
if (!replace) return
|
|
1720
|
+
}
|
|
1721
|
+
const target = join(session.rootDir, ".devcc.ts")
|
|
1722
|
+
writeFileSync(target, renderStarterConfig(facts), "utf8")
|
|
1723
|
+
this.pushNotification("success", `Created ${tildify(target)}`)
|
|
1724
|
+
await this.reloadConfig()
|
|
1725
|
+
return
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const adapter = this.agents.get(how)
|
|
1729
|
+
if (!adapter) {
|
|
1730
|
+
this.pushNotification("warn", `Unknown agent "${how}"`)
|
|
1731
|
+
return
|
|
1732
|
+
}
|
|
1733
|
+
if (session.setupAgentSessionId && session.processes.isRunning(session.setupAgentSessionId)) {
|
|
1734
|
+
this.pushNotification("warn", "An agent is already setting up .devcc.ts")
|
|
1735
|
+
return
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
try {
|
|
1739
|
+
const agentSession = await this.agents.launch(how, {
|
|
1740
|
+
cwd: session.rootDir,
|
|
1741
|
+
prompt: buildSetupPrompt(facts, "print"),
|
|
1742
|
+
})
|
|
1743
|
+
session.setupAgentSessionId = agentSession.id
|
|
1744
|
+
updateUi(this.store, { logSource: agentSession.id })
|
|
1745
|
+
this.pushNotification(
|
|
1746
|
+
"info",
|
|
1747
|
+
`${adapter.name} is reading the project; devcc will offer its .devcc.ts when it finishes`,
|
|
1748
|
+
)
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
const devccError = toDevccError(error, "process")
|
|
1751
|
+
this.pushNotification("error", devccError.message, devccError.detail)
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* The setup agent exited: take the config it printed, validate it from a
|
|
1757
|
+
* scratch file under .devcc/, and write it only if the user agrees.
|
|
1758
|
+
*/
|
|
1759
|
+
private async finishConfigSetup(session: ProjectSession, agent: ManagedProcess): Promise<void> {
|
|
1760
|
+
session.setupAgentSessionId = null
|
|
1761
|
+
const name = session.project.name
|
|
1762
|
+
if (agent.exitCode !== 0) {
|
|
1763
|
+
this.pushNotification(
|
|
1764
|
+
"error",
|
|
1765
|
+
`[${name}] Config setup agent exited with code ${agent.exitCode ?? "?"}; see its logs`,
|
|
1766
|
+
)
|
|
1767
|
+
return
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const output = (session.logs.get(agent.id)?.toArray() ?? [])
|
|
1771
|
+
.filter((entry) => entry.stream !== "system")
|
|
1772
|
+
.map((entry) => entry.text)
|
|
1773
|
+
.join("\n")
|
|
1774
|
+
const proposal = extractConfigFromOutput(output)
|
|
1775
|
+
if (!proposal) {
|
|
1776
|
+
this.pushNotification("warn", `[${name}] The agent did not print a .devcc.ts; see its logs`)
|
|
1777
|
+
return
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const scratchDir = join(session.rootDir, ".devcc")
|
|
1781
|
+
const proposedPath = join(scratchDir, "proposed.devcc.ts")
|
|
1782
|
+
mkdirSync(scratchDir, { recursive: true })
|
|
1783
|
+
writeFileSync(proposedPath, proposal, "utf8")
|
|
1784
|
+
const loaded = await loadConfigFile(proposedPath)
|
|
1785
|
+
const problems = configProblems(session.rootDir, loaded)
|
|
1786
|
+
const { config } = loaded
|
|
1787
|
+
|
|
1788
|
+
const current = session.project.configPath
|
|
1789
|
+
const target = current && !current.endsWith(".json") ? current : join(session.rootDir, ".devcc.ts")
|
|
1790
|
+
const summary = [
|
|
1791
|
+
`${config.processes.length} process(es), ${config.commands.length} command(s), verify: ${
|
|
1792
|
+
config.verify.map((entry) => (typeof entry === "string" ? entry : entry.id)).join(", ") || "none"
|
|
1793
|
+
}.`,
|
|
1794
|
+
problems.length > 0 ? `${problems.length} issue(s): ${problems[0] ?? ""}` : "It validates cleanly.",
|
|
1795
|
+
`Review it first in ${tildify(proposedPath)}.`,
|
|
1796
|
+
].join(" ")
|
|
1797
|
+
|
|
1798
|
+
const write = await this.askConfirm(
|
|
1799
|
+
`Write ${current ? "the updated" : "the proposed"} ${tildify(target)}?`,
|
|
1800
|
+
summary,
|
|
1801
|
+
)
|
|
1802
|
+
if (!write) {
|
|
1803
|
+
this.pushNotification("info", `Kept the proposal at ${tildify(proposedPath)}`)
|
|
1804
|
+
return
|
|
1805
|
+
}
|
|
1806
|
+
writeFileSync(target, proposal, "utf8")
|
|
1807
|
+
this.pushNotification("success", `Wrote ${tildify(target)}`)
|
|
1808
|
+
if (session === this.session) await this.reloadConfig()
|
|
1809
|
+
else this.pushNotification("info", `Switch to ${name} and reload its config to use it`)
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/** Rebuild the active project's session so a changed config takes effect. */
|
|
1813
|
+
private async reloadConfig(): Promise<void> {
|
|
1814
|
+
const previous = this.session
|
|
1815
|
+
const running = previous.processes.list().filter((entry) => isActive(entry.status))
|
|
1816
|
+
if (running.length > 0) {
|
|
1817
|
+
const confirmed = await this.askConfirm(
|
|
1818
|
+
`Reload the config and stop ${running.length} running process(es)?`,
|
|
1819
|
+
`${running.map((entry) => entry.name).join(", ")} will be stopped; start them again afterwards.`,
|
|
1820
|
+
)
|
|
1821
|
+
if (!confirmed) return
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
this.stopSessionBackground(previous)
|
|
1825
|
+
previous.verifyAbort?.abort()
|
|
1826
|
+
await previous.processes.stopAll()
|
|
1827
|
+
previous.wiring.dispose()
|
|
1828
|
+
|
|
1829
|
+
try {
|
|
1830
|
+
const next = await this.createSession(previous.rootDir)
|
|
1831
|
+
this.sessions.set(next.rootDir, next)
|
|
1832
|
+
this.wireSession(next)
|
|
1833
|
+
this.activateSession(next)
|
|
1834
|
+
this.reportConfigIssues()
|
|
1835
|
+
this.pushNotification(
|
|
1836
|
+
"success",
|
|
1837
|
+
`Reloaded ${next.project.configPath ? basename(next.project.configPath) : "config"}`,
|
|
1838
|
+
)
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
this.pushNotification("error", `Could not reload the config: ${errorMessage(error)}`)
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1352
1844
|
// ---------------------------------------------------------------------
|
|
1353
1845
|
// Dependencies
|
|
1354
1846
|
// ---------------------------------------------------------------------
|
|
@@ -1404,6 +1896,7 @@ export class App {
|
|
|
1404
1896
|
this.footer.setStatus("Shutting down…")
|
|
1405
1897
|
|
|
1406
1898
|
this.stopBackground()
|
|
1899
|
+
for (const session of this.sessions.values()) session.verifyAbort?.abort()
|
|
1407
1900
|
|
|
1408
1901
|
// Every open project, not just the visible one.
|
|
1409
1902
|
await Promise.all(
|
|
@@ -1461,3 +1954,17 @@ export class App {
|
|
|
1461
1954
|
return this.logs
|
|
1462
1955
|
}
|
|
1463
1956
|
}
|
|
1957
|
+
|
|
1958
|
+
/** The header and System view still read `project.tools`; derive it from environment detection. */
|
|
1959
|
+
function toolVersionsFrom(snapshot: EnvironmentSnapshot, packageManager: string): ToolVersions {
|
|
1960
|
+
const version = (id: string) => {
|
|
1961
|
+
const tool = snapshot.tools.find((entry) => entry.id === id)
|
|
1962
|
+
return tool?.installed ? (tool.version ?? "installed") : null
|
|
1963
|
+
}
|
|
1964
|
+
return { git: version("git"), docker: version("docker"), packageManager: version(packageManager) }
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function expandHome(path: string): string {
|
|
1968
|
+
const home = process.env.HOME
|
|
1969
|
+
return home && (path === "~" || path.startsWith("~/")) ? `${home}${path.slice(1)}` : path
|
|
1970
|
+
}
|