@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/dist/devcc/app/App.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoxRenderable, createCliRenderer } 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 } from "../components/Footer.js";
|
|
6
7
|
import { Header } from "../components/Header.js";
|
|
@@ -10,27 +11,32 @@ import { CommandPalette } from "../components/CommandPalette.js";
|
|
|
10
11
|
import { ProjectPicker } from "../components/ProjectPicker.js";
|
|
11
12
|
import { CommandRegistry } from "../core/commands.js";
|
|
12
13
|
import { loadConfig } from "../core/config.js";
|
|
14
|
+
import { EnvironmentService } from "../environment/EnvironmentService.js";
|
|
13
15
|
import { Subscriptions } from "../core/events.js";
|
|
14
16
|
import { createInitialSelection, createInitialState, NARROW_WIDTH, setSelection, shallowArrayEqual, Store, updateUi, VIEW_IDS, } from "../core/state.js";
|
|
15
17
|
import { DependencyService, updateArgs } from "../deps/DependencyService.js";
|
|
16
18
|
import { GitService } from "../git/GitService.js";
|
|
19
|
+
import { defaultWorktreePath, removalBlocker, worktreeLabel } from "../git/worktrees.js";
|
|
17
20
|
import { buildProjectList, defaultSearchDirs, discoverProjects, readRecents, recordRecent, } from "../projects/ProjectRegistry.js";
|
|
18
21
|
import { LogStore, MAX_LOG_LINES } from "../logs/LogBuffer.js";
|
|
19
22
|
import { ALL_SOURCES } from "../logs/merge.js";
|
|
20
23
|
import { isActive } from "../processes/ManagedProcess.js";
|
|
24
|
+
import { editorCommand, launchDetached, sshCommand, terminalCommand, } from "../platform/launch.js";
|
|
21
25
|
import { ProcessManager } from "../processes/ProcessManager.js";
|
|
22
26
|
import { DockerService } from "../services/docker.js";
|
|
23
27
|
import { detectNodeServices, PortService } from "../services/node.js";
|
|
24
28
|
import { OllamaService } from "../services/ollama.js";
|
|
25
29
|
import { PostgresService } from "../services/postgres.js";
|
|
26
30
|
import { RedisService } from "../services/redis.js";
|
|
31
|
+
import { RemoteMachineService, TailscaleService } from "../services/remote.js";
|
|
27
32
|
import { ServiceManager } from "../services/ServiceManager.js";
|
|
28
33
|
import { SystemMonitor } from "../system/SystemMonitor.js";
|
|
29
34
|
import { errorMessage, toDevccError } from "../utils/errors.js";
|
|
30
35
|
import { createLogger } from "../utils/logger.js";
|
|
31
36
|
import { detectPackageManager, readPackageJson, readScripts, runScriptArgs } from "../utils/packageManager.js";
|
|
32
37
|
import { fuzzyFilter } from "../utils/fuzzy.js";
|
|
33
|
-
import { findProjectRoot } from "../utils/paths.js";
|
|
38
|
+
import { findProjectRoot, tildify } from "../utils/paths.js";
|
|
39
|
+
import { isVerifyIssue, resolveVerifySteps, runVerification, summariseVerify, } from "../verify/verify.js";
|
|
34
40
|
import { DependenciesView } from "../views/DependenciesView.js";
|
|
35
41
|
import { GitView } from "../views/GitView.js";
|
|
36
42
|
import { LogsView } from "../views/LogsView.js";
|
|
@@ -40,7 +46,9 @@ import { ScriptsView, scriptProcessId } from "../views/ScriptsView.js";
|
|
|
40
46
|
import { ServicesView } from "../views/ServicesView.js";
|
|
41
47
|
import { SettingsView } from "../views/SettingsView.js";
|
|
42
48
|
import { SystemView } from "../views/SystemView.js";
|
|
49
|
+
import { WorktreesView, selectedWorktreeEntry } from "../views/WorktreesView.js";
|
|
43
50
|
import { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./builtinCommands.js";
|
|
51
|
+
import { createControlPlaneCommands } from "./controlPlaneCommands.js";
|
|
44
52
|
import { HELP_SECTIONS, VIEW_SHORTCUTS } from "./keymap.js";
|
|
45
53
|
import { theme } from "./theme.js";
|
|
46
54
|
const LOG_UPDATE_THROTTLE_MS = 80;
|
|
@@ -83,6 +91,12 @@ export class App {
|
|
|
83
91
|
get deps() {
|
|
84
92
|
return this.session.deps;
|
|
85
93
|
}
|
|
94
|
+
get agents() {
|
|
95
|
+
return this.session.agents;
|
|
96
|
+
}
|
|
97
|
+
get environment() {
|
|
98
|
+
return this.session.environment;
|
|
99
|
+
}
|
|
86
100
|
get config() {
|
|
87
101
|
return this.session.config;
|
|
88
102
|
}
|
|
@@ -161,19 +175,32 @@ export class App {
|
|
|
161
175
|
logger: logger.child("services"),
|
|
162
176
|
intervalMs: config.refresh.services,
|
|
163
177
|
});
|
|
178
|
+
const tailscale = new TailscaleService({
|
|
179
|
+
machines: config.remoteMachines,
|
|
180
|
+
logger: logger.child("tailscale"),
|
|
181
|
+
});
|
|
164
182
|
services.registerAll([
|
|
165
183
|
new DockerService({ rootDir }),
|
|
166
184
|
new PostgresService(),
|
|
167
185
|
new RedisService(),
|
|
168
186
|
new OllamaService(),
|
|
169
187
|
...detectNodeServices(packageJson),
|
|
170
|
-
...config.services.map((entry) =>
|
|
171
|
-
id: entry.id,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
...config.services.map((entry) => entry.type === "ollama"
|
|
189
|
+
? new OllamaService({ id: entry.id, name: entry.name, baseUrl: entry.baseUrl })
|
|
190
|
+
: new PortService({
|
|
191
|
+
id: entry.id,
|
|
192
|
+
name: entry.name,
|
|
193
|
+
port: entry.port,
|
|
194
|
+
host: entry.host,
|
|
195
|
+
healthPath: entry.healthPath,
|
|
196
|
+
category: entry.category,
|
|
197
|
+
})),
|
|
198
|
+
tailscale,
|
|
199
|
+
...config.remoteMachines.map((machine) => new RemoteMachineService({
|
|
200
|
+
machine,
|
|
201
|
+
tailscale: () => tailscale.getLastStatus(),
|
|
202
|
+
service: (id) => services.get(id),
|
|
203
|
+
logger: logger.child("remote"),
|
|
177
204
|
})),
|
|
178
205
|
]);
|
|
179
206
|
const git = new GitService({ rootDir, logger: logger.child("git") });
|
|
@@ -182,12 +209,24 @@ export class App {
|
|
|
182
209
|
manager: packageManager.manager,
|
|
183
210
|
logger: logger.child("deps"),
|
|
184
211
|
});
|
|
212
|
+
const agents = new AgentManager({
|
|
213
|
+
processes,
|
|
214
|
+
adapters: createAgentAdapters(config.agents),
|
|
215
|
+
logger: logger.child("agents"),
|
|
216
|
+
});
|
|
217
|
+
const environment = new EnvironmentService({ rootDir, logger: logger.child("environment") });
|
|
218
|
+
const scripts = readScripts(packageJson);
|
|
219
|
+
const resolvedVerify = resolveVerifySteps(config.verify, {
|
|
220
|
+
commands: config.commands,
|
|
221
|
+
scripts,
|
|
222
|
+
packageManager: packageManager.manager,
|
|
223
|
+
});
|
|
185
224
|
const project = {
|
|
186
225
|
name: config.name ?? (typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir)),
|
|
187
226
|
rootDir,
|
|
188
227
|
packageManager,
|
|
189
228
|
configPath: loaded.filePath,
|
|
190
|
-
configIssues: loaded.issues,
|
|
229
|
+
configIssues: [...loaded.issues, ...resolvedVerify.issues],
|
|
191
230
|
host: system.getHostInfo(),
|
|
192
231
|
tools: { git: null, docker: null, packageManager: null },
|
|
193
232
|
};
|
|
@@ -204,8 +243,14 @@ export class App {
|
|
|
204
243
|
git,
|
|
205
244
|
system,
|
|
206
245
|
deps,
|
|
246
|
+
agents,
|
|
247
|
+
environment,
|
|
207
248
|
project,
|
|
208
|
-
|
|
249
|
+
verifySteps: resolvedVerify.steps,
|
|
250
|
+
verify: { configured: resolvedVerify.steps.length, running: false, steps: [] },
|
|
251
|
+
verifyIssues: [...loaded.issues.filter(isVerifyIssue), ...resolvedVerify.issues],
|
|
252
|
+
verifyAbort: null,
|
|
253
|
+
scripts,
|
|
209
254
|
scriptRuns: {},
|
|
210
255
|
wiring: new Subscriptions(),
|
|
211
256
|
timers: [],
|
|
@@ -217,6 +262,10 @@ export class App {
|
|
|
217
262
|
this.commands.registerAll(createBuiltinCommands());
|
|
218
263
|
this.commands.registerAll(createScriptCommands(this.session.scripts));
|
|
219
264
|
this.commands.registerAll(createConfigCommands(this.config.commands, (entry) => this.runConfigCommand(entry)));
|
|
265
|
+
this.commands.registerAll(createControlPlaneCommands({
|
|
266
|
+
agents: this.session.agents.list(),
|
|
267
|
+
machines: this.config.remoteMachines,
|
|
268
|
+
}));
|
|
220
269
|
}
|
|
221
270
|
reportConfigIssues() {
|
|
222
271
|
for (const issue of this.session.project.configIssues) {
|
|
@@ -267,6 +316,7 @@ export class App {
|
|
|
267
316
|
new ServicesView(deps),
|
|
268
317
|
new ScriptsView(deps),
|
|
269
318
|
new GitView(deps),
|
|
319
|
+
new WorktreesView(deps),
|
|
270
320
|
new LogsView(deps),
|
|
271
321
|
new DependenciesView(deps),
|
|
272
322
|
new SystemView(deps),
|
|
@@ -322,12 +372,12 @@ export class App {
|
|
|
322
372
|
*/
|
|
323
373
|
wireSession(session) {
|
|
324
374
|
const { store } = this;
|
|
325
|
-
const { wiring, processes, services, git, system, logs, deps } = session;
|
|
375
|
+
const { wiring, processes, services, git, system, logs, deps, environment } = session;
|
|
326
376
|
const active = () => this.session === session;
|
|
327
377
|
const notify = (level, message, detail) => this.pushNotification(level, active() ? message : `[${session.project.name}] ${message}`, detail);
|
|
328
378
|
const syncProcesses = () => {
|
|
329
379
|
if (active())
|
|
330
|
-
store.patch({ processes: processes.list() });
|
|
380
|
+
store.patch({ processes: processes.list(), agents: session.agents.sessions() });
|
|
331
381
|
this.refreshSidebarProjects();
|
|
332
382
|
};
|
|
333
383
|
processes.on("process:changed", syncProcesses);
|
|
@@ -362,12 +412,15 @@ export class App {
|
|
|
362
412
|
if (active())
|
|
363
413
|
store.patch({ system: metrics });
|
|
364
414
|
});
|
|
365
|
-
|
|
415
|
+
wiring.add(() => system.removeAllListeners());
|
|
416
|
+
environment.on("environment:changed", (snapshot) => {
|
|
417
|
+
const tools = toolVersionsFrom(snapshot, session.project.packageManager.manager);
|
|
366
418
|
session.project = { ...session.project, tools };
|
|
367
|
-
if (active())
|
|
368
|
-
store.setState((state) => ({ ...state, project: { ...state.project, tools } }));
|
|
419
|
+
if (active()) {
|
|
420
|
+
store.setState((state) => ({ ...state, environment: snapshot, project: { ...state.project, tools } }));
|
|
421
|
+
}
|
|
369
422
|
});
|
|
370
|
-
wiring.add(() =>
|
|
423
|
+
wiring.add(() => environment.removeAllListeners());
|
|
371
424
|
logs.on("log:sources", (sources) => {
|
|
372
425
|
if (!active())
|
|
373
426
|
return;
|
|
@@ -411,6 +464,9 @@ export class App {
|
|
|
411
464
|
this.subscriptions.add(store.watch((state) => state.services, markDirty, shallowArrayEqual));
|
|
412
465
|
this.subscriptions.add(store.watch((state) => state.git, markDirty));
|
|
413
466
|
this.subscriptions.add(store.watch((state) => state.dependencies, markDirty));
|
|
467
|
+
this.subscriptions.add(store.watch((state) => state.agents, markDirty, shallowArrayEqual));
|
|
468
|
+
this.subscriptions.add(store.watch((state) => state.environment, markDirty));
|
|
469
|
+
this.subscriptions.add(store.watch((state) => state.verify, markDirty));
|
|
414
470
|
this.subscriptions.add(store.watch((state) => state.projects, markDirty, shallowArrayEqual));
|
|
415
471
|
this.subscriptions.add(store.watch((state) => state.system, markDirty));
|
|
416
472
|
this.subscriptions.add(store.watch((state) => state.scripts, markDirty));
|
|
@@ -427,6 +483,7 @@ export class App {
|
|
|
427
483
|
processes: this.processes.list(),
|
|
428
484
|
services: this.services.list(),
|
|
429
485
|
logSources: this.logs.sources(),
|
|
486
|
+
verify: this.session.verify,
|
|
430
487
|
});
|
|
431
488
|
store.setState((state) => ({
|
|
432
489
|
...state,
|
|
@@ -461,7 +518,9 @@ export class App {
|
|
|
461
518
|
session.timers.push(gitTimer);
|
|
462
519
|
void session.git.refresh();
|
|
463
520
|
void session.services.detectAll().then(() => session.services.start());
|
|
464
|
-
|
|
521
|
+
// Tool versions change rarely: detect once per project, refresh on request.
|
|
522
|
+
if (!session.environment.getSnapshot().checkedAt)
|
|
523
|
+
void session.environment.refresh();
|
|
465
524
|
if (!session.processes.list().some((entry) => entry.startedAt)) {
|
|
466
525
|
// Autostart only the first time a project is opened, never on reactivation.
|
|
467
526
|
void session.processes.startAutoStart();
|
|
@@ -616,6 +675,11 @@ export class App {
|
|
|
616
675
|
void this.commands.run("app.refresh", this.context);
|
|
617
676
|
return;
|
|
618
677
|
}
|
|
678
|
+
if (key.shift && key.name === "v") {
|
|
679
|
+
key.preventDefault();
|
|
680
|
+
void this.runCommand("verify.run");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
619
683
|
const state = this.store.getState();
|
|
620
684
|
// Views get first refusal so `s`/`r`/`d` mean the right thing per pane.
|
|
621
685
|
const view = this.views.get(this.activeView);
|
|
@@ -872,6 +936,8 @@ export class App {
|
|
|
872
936
|
logs: this.logs,
|
|
873
937
|
system: this.system,
|
|
874
938
|
deps: this.deps,
|
|
939
|
+
agents: this.agents,
|
|
940
|
+
environment: this.environment,
|
|
875
941
|
navigate: (view) => updateUi(this.store, { view }),
|
|
876
942
|
notify: (level, message, detail) => this.pushNotification(level, message, detail),
|
|
877
943
|
confirm: (title, message) => this.askConfirm(title, message),
|
|
@@ -883,6 +949,15 @@ export class App {
|
|
|
883
949
|
openProjects: () => this.openProjects(),
|
|
884
950
|
switchProject: (path) => this.openProject(path),
|
|
885
951
|
closeProject: (path) => this.closeProject(path),
|
|
952
|
+
selectedWorktree: () => this.selectedWorktree(),
|
|
953
|
+
createWorktree: () => this.createWorktree(),
|
|
954
|
+
removeWorktree: (worktree) => this.removeWorktree(worktree),
|
|
955
|
+
launchAgent: (agentId, worktree) => this.launchAgent(agentId, worktree),
|
|
956
|
+
openInEditor: (path) => this.launch(() => editorCommand(path, this.config.editor), path),
|
|
957
|
+
openInTerminal: (path) => this.launch(() => terminalCommand(path, this.config.terminal), path),
|
|
958
|
+
runVerify: () => this.runVerify(),
|
|
959
|
+
remoteMachines: () => this.config.remoteMachines,
|
|
960
|
+
sshTo: (machineId) => this.sshTo(machineId),
|
|
886
961
|
quit: () => void this.quit(),
|
|
887
962
|
};
|
|
888
963
|
// Session services are swapped out on a project switch, so expose them as
|
|
@@ -894,6 +969,8 @@ export class App {
|
|
|
894
969
|
logs: { get: () => this.logs, enumerable: true },
|
|
895
970
|
system: { get: () => this.system, enumerable: true },
|
|
896
971
|
deps: { get: () => this.deps, enumerable: true },
|
|
972
|
+
agents: { get: () => this.agents, enumerable: true },
|
|
973
|
+
environment: { get: () => this.environment, enumerable: true },
|
|
897
974
|
});
|
|
898
975
|
return context;
|
|
899
976
|
}
|
|
@@ -1081,6 +1158,9 @@ export class App {
|
|
|
1081
1158
|
scripts: { available: session.scripts, runs: session.scriptRuns },
|
|
1082
1159
|
git: session.git.getSnapshot(),
|
|
1083
1160
|
dependencies: session.deps.getSnapshot(),
|
|
1161
|
+
agents: session.agents.sessions(),
|
|
1162
|
+
environment: session.environment.getSnapshot(),
|
|
1163
|
+
verify: session.verify,
|
|
1084
1164
|
system: session.system.getMetrics(),
|
|
1085
1165
|
logSources: session.logs.sources(),
|
|
1086
1166
|
projects: [],
|
|
@@ -1159,6 +1239,221 @@ export class App {
|
|
|
1159
1239
|
}), this.session.rootDir);
|
|
1160
1240
|
}
|
|
1161
1241
|
// ---------------------------------------------------------------------
|
|
1242
|
+
// Worktrees, agents, verification, remote
|
|
1243
|
+
// ---------------------------------------------------------------------
|
|
1244
|
+
selectedWorktree() {
|
|
1245
|
+
const state = this.store.getState();
|
|
1246
|
+
if (state.ui.view === "worktrees") {
|
|
1247
|
+
const entry = selectedWorktreeEntry(state);
|
|
1248
|
+
if (entry?.worktree)
|
|
1249
|
+
return entry.worktree;
|
|
1250
|
+
}
|
|
1251
|
+
return state.git.worktrees.find((worktree) => worktree.isCurrent);
|
|
1252
|
+
}
|
|
1253
|
+
async createWorktree() {
|
|
1254
|
+
const snapshot = this.git.getSnapshot();
|
|
1255
|
+
if (!snapshot.isRepository) {
|
|
1256
|
+
this.pushNotification("warn", "Not a git repository");
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const main = snapshot.worktrees.find((worktree) => worktree.isMain);
|
|
1260
|
+
if (!main) {
|
|
1261
|
+
this.pushNotification("warn", "Could not read this repository's worktrees yet");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const branch = (await this.askPrompt("New worktree: branch (created if it does not exist)", "feature/name"))?.trim();
|
|
1265
|
+
if (!branch)
|
|
1266
|
+
return;
|
|
1267
|
+
const current = snapshot.status.branch ?? "HEAD";
|
|
1268
|
+
const base = await this.askPrompt(`Base for ${branch} (empty: ${current})`, current);
|
|
1269
|
+
if (base === null)
|
|
1270
|
+
return;
|
|
1271
|
+
const suggested = defaultWorktreePath(main.path, branch, this.config.worktrees?.directory);
|
|
1272
|
+
const path = await this.askPrompt(`Directory (empty: ${tildify(suggested)})`, tildify(suggested));
|
|
1273
|
+
if (path === null)
|
|
1274
|
+
return;
|
|
1275
|
+
const target = path.trim() === "" ? suggested : resolve(main.path, expandHome(path.trim()));
|
|
1276
|
+
try {
|
|
1277
|
+
this.footer.setStatus(`Creating worktree ${branch}…`);
|
|
1278
|
+
await this.git.createWorktree({ branch, base: base.trim() || undefined, path: target });
|
|
1279
|
+
this.pushNotification("success", `Created worktree ${branch} at ${tildify(target)}`);
|
|
1280
|
+
}
|
|
1281
|
+
catch (error) {
|
|
1282
|
+
const devccError = toDevccError(error, "git");
|
|
1283
|
+
this.pushNotification("error", devccError.message, devccError.detail);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async removeWorktree(worktree) {
|
|
1287
|
+
const busy = [
|
|
1288
|
+
...this.agents.busyPaths(),
|
|
1289
|
+
...this.processes
|
|
1290
|
+
.list()
|
|
1291
|
+
.filter((entry) => isActive(entry.status))
|
|
1292
|
+
.map((entry) => resolve(this.session.rootDir, entry.cwd ?? ".")),
|
|
1293
|
+
// A worktree open as a project in devcc is in use too.
|
|
1294
|
+
...[...this.sessions.keys()].filter((path) => path !== this.session.rootDir),
|
|
1295
|
+
];
|
|
1296
|
+
const blocker = removalBlocker(worktree, busy);
|
|
1297
|
+
if (blocker) {
|
|
1298
|
+
this.pushNotification("warn", blocker);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const confirmed = await this.askConfirm(`Remove worktree ${worktreeLabel(worktree)}?`, `Deletes ${tildify(worktree.path)}. The branch is kept. git refuses if there are uncommitted or untracked changes; devcc never forces it.`);
|
|
1302
|
+
if (!confirmed)
|
|
1303
|
+
return;
|
|
1304
|
+
try {
|
|
1305
|
+
await this.git.removeWorktree(worktree.path, busy);
|
|
1306
|
+
this.pushNotification("success", `Removed worktree ${worktreeLabel(worktree)}`);
|
|
1307
|
+
}
|
|
1308
|
+
catch (error) {
|
|
1309
|
+
const devccError = toDevccError(error, "git");
|
|
1310
|
+
this.pushNotification("error", devccError.message, devccError.detail);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async launchAgent(agentId, worktree) {
|
|
1314
|
+
const adapter = this.agents.get(agentId);
|
|
1315
|
+
if (!adapter) {
|
|
1316
|
+
this.pushNotification("warn", `Unknown agent "${agentId}"`);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const target = worktree ?? this.selectedWorktree();
|
|
1320
|
+
const cwd = target?.path ?? this.session.rootDir;
|
|
1321
|
+
const where = target ? worktreeLabel(target) : basename(cwd);
|
|
1322
|
+
const task = await this.askPrompt(`Task for ${adapter.name} in ${where}`, "Describe what the agent should do");
|
|
1323
|
+
if (task === null || task.trim() === "")
|
|
1324
|
+
return;
|
|
1325
|
+
try {
|
|
1326
|
+
const session = await this.agents.launch(agentId, { cwd, prompt: task, branch: target?.branch });
|
|
1327
|
+
updateUi(this.store, { logSource: session.id });
|
|
1328
|
+
this.pushNotification("success", `${adapter.name} started in ${where}`);
|
|
1329
|
+
}
|
|
1330
|
+
catch (error) {
|
|
1331
|
+
const devccError = toDevccError(error, "process");
|
|
1332
|
+
this.pushNotification("error", devccError.message, devccError.detail);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/** Build and start an external launch; a missing configuration becomes a notification. */
|
|
1336
|
+
async launch(build, cwd) {
|
|
1337
|
+
try {
|
|
1338
|
+
const command = build();
|
|
1339
|
+
await launchDetached(command, cwd);
|
|
1340
|
+
this.logger.log("debug", "launch", "opened", { command: command.command, cwd });
|
|
1341
|
+
}
|
|
1342
|
+
catch (error) {
|
|
1343
|
+
const devccError = toDevccError(error, "command");
|
|
1344
|
+
this.pushNotification("error", devccError.message, devccError.detail);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async sshTo(machineId) {
|
|
1348
|
+
const machine = this.config.remoteMachines.find((entry) => entry.id === machineId);
|
|
1349
|
+
if (!machine) {
|
|
1350
|
+
this.pushNotification("warn", `Unknown remote machine "${machineId}"`);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
await this.launch(() => sshCommand(machine, this.config.terminal), this.session.rootDir);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Run verification through the ProcessManager: each step is a managed task,
|
|
1357
|
+
* so its output streams into Logs and quitting stops it like anything else.
|
|
1358
|
+
*/
|
|
1359
|
+
async runVerify() {
|
|
1360
|
+
const session = this.session;
|
|
1361
|
+
if (session.verify.running) {
|
|
1362
|
+
this.pushNotification("warn", "Verification is already running");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (session.verifyIssues.length > 0) {
|
|
1366
|
+
this.pushNotification("error", `Verify refused: ${session.verifyIssues[0] ?? "invalid verify config"}`, session.verifyIssues.join("\n"));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (session.verifySteps.length === 0) {
|
|
1370
|
+
this.pushNotification("warn", "No verification steps configured", 'Add verify: ["lint", "test"] to .devcc.ts');
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const controller = new AbortController();
|
|
1374
|
+
session.verifyAbort = controller;
|
|
1375
|
+
const publish = (verify) => {
|
|
1376
|
+
session.verify = verify;
|
|
1377
|
+
if (session === this.session)
|
|
1378
|
+
this.store.patch({ verify });
|
|
1379
|
+
};
|
|
1380
|
+
const startedAt = new Date();
|
|
1381
|
+
publish({ configured: session.verifySteps.length, running: true, steps: [], startedAt });
|
|
1382
|
+
this.pushNotification("info", `Verifying: ${session.verifySteps.map((step) => step.name).join(", ")}`);
|
|
1383
|
+
const report = await runVerification(session.verifySteps, this.processStepExecutor(session), {
|
|
1384
|
+
signal: controller.signal,
|
|
1385
|
+
onProgress: (steps) => publish({ ...session.verify, steps: [...steps] }),
|
|
1386
|
+
});
|
|
1387
|
+
session.verifyAbort = null;
|
|
1388
|
+
publish({
|
|
1389
|
+
configured: session.verifySteps.length,
|
|
1390
|
+
running: false,
|
|
1391
|
+
steps: report.steps,
|
|
1392
|
+
startedAt,
|
|
1393
|
+
finishedAt: new Date(),
|
|
1394
|
+
ok: report.ok,
|
|
1395
|
+
});
|
|
1396
|
+
this.pushNotification(report.ok ? "success" : "error", `Verify: ${summariseVerify(report)}`);
|
|
1397
|
+
}
|
|
1398
|
+
processStepExecutor(session) {
|
|
1399
|
+
return async (step, signal) => {
|
|
1400
|
+
const { processes } = session;
|
|
1401
|
+
const id = `verify:${step.id}`;
|
|
1402
|
+
if (processes.isRunning(id))
|
|
1403
|
+
return { exitCode: null, error: "already running" };
|
|
1404
|
+
processes.register({
|
|
1405
|
+
id,
|
|
1406
|
+
name: `verify · ${step.name}`,
|
|
1407
|
+
command: step.command,
|
|
1408
|
+
args: step.args,
|
|
1409
|
+
cwd: step.cwd,
|
|
1410
|
+
env: { NO_COLOR: "1" },
|
|
1411
|
+
kind: "task",
|
|
1412
|
+
});
|
|
1413
|
+
let onChange;
|
|
1414
|
+
const finished = new Promise((resolveFinished) => {
|
|
1415
|
+
let started = false;
|
|
1416
|
+
onChange = (snapshot) => {
|
|
1417
|
+
if (snapshot.id !== id)
|
|
1418
|
+
return;
|
|
1419
|
+
if (snapshot.status === "starting" || snapshot.status === "running")
|
|
1420
|
+
started = true;
|
|
1421
|
+
else if (started && (snapshot.status === "stopped" || snapshot.status === "failed")) {
|
|
1422
|
+
resolveFinished(snapshot);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
processes.on("process:changed", onChange);
|
|
1426
|
+
});
|
|
1427
|
+
const onAbort = () => void processes.stop(id).catch(() => undefined);
|
|
1428
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1429
|
+
try {
|
|
1430
|
+
if (session === this.session)
|
|
1431
|
+
updateUi(this.store, { logSource: id });
|
|
1432
|
+
await processes.start(id);
|
|
1433
|
+
const result = await finished;
|
|
1434
|
+
return {
|
|
1435
|
+
exitCode: result.exitCode ?? null,
|
|
1436
|
+
error: signal.aborted
|
|
1437
|
+
? "cancelled"
|
|
1438
|
+
: result.exitSignal
|
|
1439
|
+
? `terminated by ${result.exitSignal}`
|
|
1440
|
+
: result.exitCode === undefined
|
|
1441
|
+
? result.lastError
|
|
1442
|
+
: undefined,
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
catch (error) {
|
|
1446
|
+
return { exitCode: null, error: errorMessage(error) };
|
|
1447
|
+
}
|
|
1448
|
+
finally {
|
|
1449
|
+
signal.removeEventListener("abort", onAbort);
|
|
1450
|
+
// Also runs when start() throws, so the listener never outlives the step.
|
|
1451
|
+
if (onChange)
|
|
1452
|
+
processes.off("process:changed", onChange);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
// ---------------------------------------------------------------------
|
|
1162
1457
|
// Dependencies
|
|
1163
1458
|
// ---------------------------------------------------------------------
|
|
1164
1459
|
/** Update a single package, gated by a confirmation (it rewrites the lockfile). */
|
|
@@ -1204,6 +1499,8 @@ export class App {
|
|
|
1204
1499
|
this.logger.log("info", "app", "shutting down");
|
|
1205
1500
|
this.footer.setStatus("Shutting down…");
|
|
1206
1501
|
this.stopBackground();
|
|
1502
|
+
for (const session of this.sessions.values())
|
|
1503
|
+
session.verifyAbort?.abort();
|
|
1207
1504
|
// Every open project, not just the visible one.
|
|
1208
1505
|
await Promise.all([...this.sessions.values()].map(async (session) => {
|
|
1209
1506
|
try {
|
|
@@ -1252,4 +1549,16 @@ export class App {
|
|
|
1252
1549
|
return this.logs;
|
|
1253
1550
|
}
|
|
1254
1551
|
}
|
|
1552
|
+
/** The header and System view still read `project.tools`; derive it from environment detection. */
|
|
1553
|
+
function toolVersionsFrom(snapshot, packageManager) {
|
|
1554
|
+
const version = (id) => {
|
|
1555
|
+
const tool = snapshot.tools.find((entry) => entry.id === id);
|
|
1556
|
+
return tool?.installed ? (tool.version ?? "installed") : null;
|
|
1557
|
+
};
|
|
1558
|
+
return { git: version("git"), docker: version("docker"), packageManager: version(packageManager) };
|
|
1559
|
+
}
|
|
1560
|
+
function expandHome(path) {
|
|
1561
|
+
const home = process.env.HOME;
|
|
1562
|
+
return home && (path === "~" || path.startsWith("~/")) ? `${home}${path.slice(1)}` : path;
|
|
1563
|
+
}
|
|
1255
1564
|
//# sourceMappingURL=App.js.map
|