@doriandev/devcc 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +556 -386
  2. package/bin/devcc.mjs +50 -23
  3. package/dist/devcc/agents/agents.d.ts +112 -0
  4. package/dist/devcc/agents/agents.d.ts.map +1 -0
  5. package/dist/devcc/agents/agents.js +179 -0
  6. package/dist/devcc/agents/agents.js.map +1 -0
  7. package/dist/devcc/app/App.d.ts +15 -0
  8. package/dist/devcc/app/App.d.ts.map +1 -1
  9. package/dist/devcc/app/App.js +326 -17
  10. package/dist/devcc/app/App.js.map +1 -1
  11. package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
  12. package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
  13. package/dist/devcc/app/controlPlaneCommands.js +179 -0
  14. package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
  15. package/dist/devcc/app/keymap.d.ts.map +1 -1
  16. package/dist/devcc/app/keymap.js +24 -3
  17. package/dist/devcc/app/keymap.js.map +1 -1
  18. package/dist/devcc/automation.d.ts +31 -0
  19. package/dist/devcc/automation.d.ts.map +1 -0
  20. package/dist/devcc/automation.js +202 -0
  21. package/dist/devcc/automation.js.map +1 -0
  22. package/dist/devcc/cli.d.ts.map +1 -1
  23. package/dist/devcc/cli.js +46 -3
  24. package/dist/devcc/cli.js.map +1 -1
  25. package/dist/devcc/components/Sidebar.d.ts.map +1 -1
  26. package/dist/devcc/components/Sidebar.js +1 -0
  27. package/dist/devcc/components/Sidebar.js.map +1 -1
  28. package/dist/devcc/components/primitives.d.ts.map +1 -1
  29. package/dist/devcc/components/primitives.js +3 -0
  30. package/dist/devcc/components/primitives.js.map +1 -1
  31. package/dist/devcc/core/commands.d.ts +25 -0
  32. package/dist/devcc/core/commands.d.ts.map +1 -1
  33. package/dist/devcc/core/commands.js.map +1 -1
  34. package/dist/devcc/core/config.d.ts +69 -3
  35. package/dist/devcc/core/config.d.ts.map +1 -1
  36. package/dist/devcc/core/config.js +185 -5
  37. package/dist/devcc/core/config.js.map +1 -1
  38. package/dist/devcc/core/state.d.ts +19 -1
  39. package/dist/devcc/core/state.d.ts.map +1 -1
  40. package/dist/devcc/core/state.js +6 -0
  41. package/dist/devcc/core/state.js.map +1 -1
  42. package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
  43. package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
  44. package/dist/devcc/environment/EnvironmentService.js +159 -0
  45. package/dist/devcc/environment/EnvironmentService.js.map +1 -0
  46. package/dist/devcc/environment/integrations.d.ts +52 -0
  47. package/dist/devcc/environment/integrations.d.ts.map +1 -0
  48. package/dist/devcc/environment/integrations.js +94 -0
  49. package/dist/devcc/environment/integrations.js.map +1 -0
  50. package/dist/devcc/environment/tools.d.ts +51 -0
  51. package/dist/devcc/environment/tools.d.ts.map +1 -0
  52. package/dist/devcc/environment/tools.js +99 -0
  53. package/dist/devcc/environment/tools.js.map +1 -0
  54. package/dist/devcc/git/GitService.d.ts +26 -0
  55. package/dist/devcc/git/GitService.d.ts.map +1 -1
  56. package/dist/devcc/git/GitService.js +71 -2
  57. package/dist/devcc/git/GitService.js.map +1 -1
  58. package/dist/devcc/git/worktrees.d.ts +88 -0
  59. package/dist/devcc/git/worktrees.d.ts.map +1 -0
  60. package/dist/devcc/git/worktrees.js +226 -0
  61. package/dist/devcc/git/worktrees.js.map +1 -0
  62. package/dist/devcc/index.d.ts +13 -3
  63. package/dist/devcc/index.d.ts.map +1 -1
  64. package/dist/devcc/index.js +11 -1
  65. package/dist/devcc/index.js.map +1 -1
  66. package/dist/devcc/platform/launch.d.ts +30 -0
  67. package/dist/devcc/platform/launch.d.ts.map +1 -0
  68. package/dist/devcc/platform/launch.js +93 -0
  69. package/dist/devcc/platform/launch.js.map +1 -0
  70. package/dist/devcc/services/ServiceManager.d.ts +10 -2
  71. package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
  72. package/dist/devcc/services/ServiceManager.js +18 -4
  73. package/dist/devcc/services/ServiceManager.js.map +1 -1
  74. package/dist/devcc/services/ollama.d.ts +64 -5
  75. package/dist/devcc/services/ollama.d.ts.map +1 -1
  76. package/dist/devcc/services/ollama.js +118 -17
  77. package/dist/devcc/services/ollama.js.map +1 -1
  78. package/dist/devcc/services/remote.d.ts +90 -0
  79. package/dist/devcc/services/remote.d.ts.map +1 -0
  80. package/dist/devcc/services/remote.js +233 -0
  81. package/dist/devcc/services/remote.js.map +1 -0
  82. package/dist/devcc/services/types.d.ts +8 -1
  83. package/dist/devcc/services/types.d.ts.map +1 -1
  84. package/dist/devcc/services/types.js.map +1 -1
  85. package/dist/devcc/utils/exec.d.ts +6 -0
  86. package/dist/devcc/utils/exec.d.ts.map +1 -1
  87. package/dist/devcc/utils/exec.js +12 -2
  88. package/dist/devcc/utils/exec.js.map +1 -1
  89. package/dist/devcc/verify/verify.d.ts +107 -0
  90. package/dist/devcc/verify/verify.d.ts.map +1 -0
  91. package/dist/devcc/verify/verify.js +203 -0
  92. package/dist/devcc/verify/verify.js.map +1 -0
  93. package/dist/devcc/views/OverviewView.d.ts.map +1 -1
  94. package/dist/devcc/views/OverviewView.js +55 -1
  95. package/dist/devcc/views/OverviewView.js.map +1 -1
  96. package/dist/devcc/views/ServicesView.d.ts +2 -0
  97. package/dist/devcc/views/ServicesView.d.ts.map +1 -1
  98. package/dist/devcc/views/ServicesView.js +15 -0
  99. package/dist/devcc/views/ServicesView.js.map +1 -1
  100. package/dist/devcc/views/SystemView.d.ts +10 -1
  101. package/dist/devcc/views/SystemView.d.ts.map +1 -1
  102. package/dist/devcc/views/SystemView.js +127 -19
  103. package/dist/devcc/views/SystemView.js.map +1 -1
  104. package/dist/devcc/views/WorktreesView.d.ts +48 -0
  105. package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
  106. package/dist/devcc/views/WorktreesView.js +235 -0
  107. package/dist/devcc/views/WorktreesView.js.map +1 -0
  108. package/package.json +1 -1
  109. package/src/devcc/agents/agents.ts +282 -0
  110. package/src/devcc/app/App.ts +376 -20
  111. package/src/devcc/app/controlPlaneCommands.ts +197 -0
  112. package/src/devcc/app/keymap.ts +24 -3
  113. package/src/devcc/automation.ts +242 -0
  114. package/src/devcc/cli.ts +45 -3
  115. package/src/devcc/components/Sidebar.ts +1 -0
  116. package/src/devcc/components/primitives.ts +3 -0
  117. package/src/devcc/core/commands.ts +26 -0
  118. package/src/devcc/core/config.ts +271 -11
  119. package/src/devcc/core/state.ts +25 -0
  120. package/src/devcc/environment/EnvironmentService.ts +215 -0
  121. package/src/devcc/environment/integrations.ts +133 -0
  122. package/src/devcc/environment/tools.ts +151 -0
  123. package/src/devcc/git/GitService.ts +104 -2
  124. package/src/devcc/git/worktrees.ts +256 -0
  125. package/src/devcc/index.ts +57 -2
  126. package/src/devcc/platform/launch.ts +125 -0
  127. package/src/devcc/services/ServiceManager.ts +15 -3
  128. package/src/devcc/services/ollama.ts +162 -19
  129. package/src/devcc/services/remote.ts +306 -0
  130. package/src/devcc/services/types.ts +9 -1
  131. package/src/devcc/utils/exec.ts +26 -2
  132. package/src/devcc/verify/verify.ts +316 -0
  133. package/src/devcc/views/OverviewView.ts +76 -1
  134. package/src/devcc/views/ServicesView.ts +17 -0
  135. package/src/devcc/views/SystemView.ts +144 -22
  136. package/src/devcc/views/WorktreesView.ts +270 -0
@@ -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) => new PortService({
171
- id: entry.id,
172
- name: entry.name,
173
- port: entry.port,
174
- host: entry.host,
175
- healthPath: entry.healthPath,
176
- category: entry.category,
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
- scripts: readScripts(packageJson),
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
- system.on("system:tools", (tools) => {
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(() => system.removeAllListeners());
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
- void session.system.refreshTools(session.project.packageManager.manager);
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