@doriandev/devcc 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +589 -389
  2. package/bin/devcc.mjs +50 -23
  3. package/dist/devcc/agents/agents.d.ts +119 -0
  4. package/dist/devcc/agents/agents.d.ts.map +1 -0
  5. package/dist/devcc/agents/agents.js +182 -0
  6. package/dist/devcc/agents/agents.js.map +1 -0
  7. package/dist/devcc/app/App.d.ts +23 -0
  8. package/dist/devcc/app/App.d.ts.map +1 -1
  9. package/dist/devcc/app/App.js +451 -19
  10. package/dist/devcc/app/App.js.map +1 -1
  11. package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
  12. package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
  13. package/dist/devcc/app/controlPlaneCommands.js +204 -0
  14. package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
  15. package/dist/devcc/app/keymap.d.ts.map +1 -1
  16. package/dist/devcc/app/keymap.js +25 -3
  17. package/dist/devcc/app/keymap.js.map +1 -1
  18. package/dist/devcc/automation.d.ts +52 -0
  19. package/dist/devcc/automation.d.ts.map +1 -0
  20. package/dist/devcc/automation.js +339 -0
  21. package/dist/devcc/automation.js.map +1 -0
  22. package/dist/devcc/cli.d.ts.map +1 -1
  23. package/dist/devcc/cli.js +73 -3
  24. package/dist/devcc/cli.js.map +1 -1
  25. package/dist/devcc/components/Sidebar.d.ts.map +1 -1
  26. package/dist/devcc/components/Sidebar.js +1 -0
  27. package/dist/devcc/components/Sidebar.js.map +1 -1
  28. package/dist/devcc/components/primitives.d.ts.map +1 -1
  29. package/dist/devcc/components/primitives.js +3 -0
  30. package/dist/devcc/components/primitives.js.map +1 -1
  31. package/dist/devcc/core/commands.d.ts +33 -0
  32. package/dist/devcc/core/commands.d.ts.map +1 -1
  33. package/dist/devcc/core/commands.js.map +1 -1
  34. package/dist/devcc/core/config.d.ts +71 -3
  35. package/dist/devcc/core/config.d.ts.map +1 -1
  36. package/dist/devcc/core/config.js +189 -5
  37. package/dist/devcc/core/config.js.map +1 -1
  38. package/dist/devcc/core/state.d.ts +19 -1
  39. package/dist/devcc/core/state.d.ts.map +1 -1
  40. package/dist/devcc/core/state.js +6 -0
  41. package/dist/devcc/core/state.js.map +1 -1
  42. package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
  43. package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
  44. package/dist/devcc/environment/EnvironmentService.js +159 -0
  45. package/dist/devcc/environment/EnvironmentService.js.map +1 -0
  46. package/dist/devcc/environment/integrations.d.ts +52 -0
  47. package/dist/devcc/environment/integrations.d.ts.map +1 -0
  48. package/dist/devcc/environment/integrations.js +94 -0
  49. package/dist/devcc/environment/integrations.js.map +1 -0
  50. package/dist/devcc/environment/tools.d.ts +51 -0
  51. package/dist/devcc/environment/tools.d.ts.map +1 -0
  52. package/dist/devcc/environment/tools.js +99 -0
  53. package/dist/devcc/environment/tools.js.map +1 -0
  54. package/dist/devcc/git/GitService.d.ts +26 -0
  55. package/dist/devcc/git/GitService.d.ts.map +1 -1
  56. package/dist/devcc/git/GitService.js +71 -2
  57. package/dist/devcc/git/GitService.js.map +1 -1
  58. package/dist/devcc/git/worktrees.d.ts +88 -0
  59. package/dist/devcc/git/worktrees.d.ts.map +1 -0
  60. package/dist/devcc/git/worktrees.js +226 -0
  61. package/dist/devcc/git/worktrees.js.map +1 -0
  62. package/dist/devcc/index.d.ts +15 -3
  63. package/dist/devcc/index.d.ts.map +1 -1
  64. package/dist/devcc/index.js +14 -2
  65. package/dist/devcc/index.js.map +1 -1
  66. package/dist/devcc/init/detect.d.ts +55 -0
  67. package/dist/devcc/init/detect.d.ts.map +1 -0
  68. package/dist/devcc/init/detect.js +207 -0
  69. package/dist/devcc/init/detect.js.map +1 -0
  70. package/dist/devcc/init/prompt.d.ts +25 -0
  71. package/dist/devcc/init/prompt.d.ts.map +1 -0
  72. package/dist/devcc/init/prompt.js +156 -0
  73. package/dist/devcc/init/prompt.js.map +1 -0
  74. package/dist/devcc/platform/launch.d.ts +30 -0
  75. package/dist/devcc/platform/launch.d.ts.map +1 -0
  76. package/dist/devcc/platform/launch.js +93 -0
  77. package/dist/devcc/platform/launch.js.map +1 -0
  78. package/dist/devcc/services/ServiceManager.d.ts +10 -2
  79. package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
  80. package/dist/devcc/services/ServiceManager.js +18 -4
  81. package/dist/devcc/services/ServiceManager.js.map +1 -1
  82. package/dist/devcc/services/node.d.ts +11 -0
  83. package/dist/devcc/services/node.d.ts.map +1 -1
  84. package/dist/devcc/services/node.js +20 -18
  85. package/dist/devcc/services/node.js.map +1 -1
  86. package/dist/devcc/services/ollama.d.ts +64 -5
  87. package/dist/devcc/services/ollama.d.ts.map +1 -1
  88. package/dist/devcc/services/ollama.js +118 -17
  89. package/dist/devcc/services/ollama.js.map +1 -1
  90. package/dist/devcc/services/remote.d.ts +90 -0
  91. package/dist/devcc/services/remote.d.ts.map +1 -0
  92. package/dist/devcc/services/remote.js +233 -0
  93. package/dist/devcc/services/remote.js.map +1 -0
  94. package/dist/devcc/services/types.d.ts +8 -1
  95. package/dist/devcc/services/types.d.ts.map +1 -1
  96. package/dist/devcc/services/types.js.map +1 -1
  97. package/dist/devcc/utils/exec.d.ts +6 -0
  98. package/dist/devcc/utils/exec.d.ts.map +1 -1
  99. package/dist/devcc/utils/exec.js +12 -2
  100. package/dist/devcc/utils/exec.js.map +1 -1
  101. package/dist/devcc/verify/verify.d.ts +107 -0
  102. package/dist/devcc/verify/verify.d.ts.map +1 -0
  103. package/dist/devcc/verify/verify.js +203 -0
  104. package/dist/devcc/verify/verify.js.map +1 -0
  105. package/dist/devcc/views/OverviewView.d.ts.map +1 -1
  106. package/dist/devcc/views/OverviewView.js +58 -2
  107. package/dist/devcc/views/OverviewView.js.map +1 -1
  108. package/dist/devcc/views/ServicesView.d.ts +2 -0
  109. package/dist/devcc/views/ServicesView.d.ts.map +1 -1
  110. package/dist/devcc/views/ServicesView.js +15 -0
  111. package/dist/devcc/views/ServicesView.js.map +1 -1
  112. package/dist/devcc/views/SystemView.d.ts +10 -1
  113. package/dist/devcc/views/SystemView.d.ts.map +1 -1
  114. package/dist/devcc/views/SystemView.js +127 -19
  115. package/dist/devcc/views/SystemView.js.map +1 -1
  116. package/dist/devcc/views/WorktreesView.d.ts +48 -0
  117. package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
  118. package/dist/devcc/views/WorktreesView.js +235 -0
  119. package/dist/devcc/views/WorktreesView.js.map +1 -0
  120. package/package.json +1 -1
  121. package/src/devcc/agents/agents.ts +292 -0
  122. package/src/devcc/app/App.ts +529 -22
  123. package/src/devcc/app/controlPlaneCommands.ts +227 -0
  124. package/src/devcc/app/keymap.ts +25 -3
  125. package/src/devcc/automation.ts +409 -0
  126. package/src/devcc/cli.ts +77 -3
  127. package/src/devcc/components/Sidebar.ts +1 -0
  128. package/src/devcc/components/primitives.ts +3 -0
  129. package/src/devcc/core/commands.ts +34 -0
  130. package/src/devcc/core/config.ts +275 -11
  131. package/src/devcc/core/state.ts +25 -0
  132. package/src/devcc/environment/EnvironmentService.ts +215 -0
  133. package/src/devcc/environment/integrations.ts +133 -0
  134. package/src/devcc/environment/tools.ts +151 -0
  135. package/src/devcc/git/GitService.ts +104 -2
  136. package/src/devcc/git/worktrees.ts +256 -0
  137. package/src/devcc/index.ts +66 -2
  138. package/src/devcc/init/detect.ts +268 -0
  139. package/src/devcc/init/prompt.ts +179 -0
  140. package/src/devcc/platform/launch.ts +125 -0
  141. package/src/devcc/services/ServiceManager.ts +15 -3
  142. package/src/devcc/services/node.ts +17 -14
  143. package/src/devcc/services/ollama.ts +162 -19
  144. package/src/devcc/services/remote.ts +306 -0
  145. package/src/devcc/services/types.ts +9 -1
  146. package/src/devcc/utils/exec.ts +26 -2
  147. package/src/devcc/verify/verify.ts +316 -0
  148. package/src/devcc/views/OverviewView.ts +84 -2
  149. package/src/devcc/views/ServicesView.ts +17 -0
  150. package/src/devcc/views/SystemView.ts +144 -22
  151. package/src/devcc/views/WorktreesView.ts +270 -0
@@ -1,6 +1,10 @@
1
1
  import { BoxRenderable, createCliRenderer } 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 } from "../components/Footer.js";
6
10
  import { Header } from "../components/Header.js";
@@ -9,28 +13,33 @@ import { Sidebar } from "../components/Sidebar.js";
9
13
  import { CommandPalette } from "../components/CommandPalette.js";
10
14
  import { ProjectPicker } from "../components/ProjectPicker.js";
11
15
  import { CommandRegistry } from "../core/commands.js";
12
- import { loadConfig } from "../core/config.js";
16
+ import { loadConfig, loadConfigFile } from "../core/config.js";
17
+ import { EnvironmentService } from "../environment/EnvironmentService.js";
13
18
  import { Subscriptions } from "../core/events.js";
14
19
  import { createInitialSelection, createInitialState, NARROW_WIDTH, setSelection, shallowArrayEqual, Store, updateUi, VIEW_IDS, } from "../core/state.js";
15
20
  import { DependencyService, updateArgs } from "../deps/DependencyService.js";
16
21
  import { GitService } from "../git/GitService.js";
22
+ import { defaultWorktreePath, removalBlocker, worktreeLabel } from "../git/worktrees.js";
17
23
  import { buildProjectList, defaultSearchDirs, discoverProjects, readRecents, recordRecent, } from "../projects/ProjectRegistry.js";
18
24
  import { LogStore, MAX_LOG_LINES } from "../logs/LogBuffer.js";
19
25
  import { ALL_SOURCES } from "../logs/merge.js";
20
26
  import { isActive } from "../processes/ManagedProcess.js";
27
+ import { editorCommand, launchDetached, sshCommand, terminalCommand, } from "../platform/launch.js";
21
28
  import { ProcessManager } from "../processes/ProcessManager.js";
22
29
  import { DockerService } from "../services/docker.js";
23
30
  import { detectNodeServices, PortService } from "../services/node.js";
24
31
  import { OllamaService } from "../services/ollama.js";
25
32
  import { PostgresService } from "../services/postgres.js";
26
33
  import { RedisService } from "../services/redis.js";
34
+ import { RemoteMachineService, TailscaleService } from "../services/remote.js";
27
35
  import { ServiceManager } from "../services/ServiceManager.js";
28
36
  import { SystemMonitor } from "../system/SystemMonitor.js";
29
37
  import { errorMessage, toDevccError } from "../utils/errors.js";
30
38
  import { createLogger } from "../utils/logger.js";
31
39
  import { detectPackageManager, readPackageJson, readScripts, runScriptArgs } from "../utils/packageManager.js";
32
40
  import { fuzzyFilter } from "../utils/fuzzy.js";
33
- import { findProjectRoot } from "../utils/paths.js";
41
+ import { findProjectRoot, tildify } from "../utils/paths.js";
42
+ import { isVerifyIssue, resolveVerifySteps, runVerification, summariseVerify, } from "../verify/verify.js";
34
43
  import { DependenciesView } from "../views/DependenciesView.js";
35
44
  import { GitView } from "../views/GitView.js";
36
45
  import { LogsView } from "../views/LogsView.js";
@@ -40,7 +49,9 @@ import { ScriptsView, scriptProcessId } from "../views/ScriptsView.js";
40
49
  import { ServicesView } from "../views/ServicesView.js";
41
50
  import { SettingsView } from "../views/SettingsView.js";
42
51
  import { SystemView } from "../views/SystemView.js";
52
+ import { WorktreesView, selectedWorktreeEntry } from "../views/WorktreesView.js";
43
53
  import { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./builtinCommands.js";
54
+ import { createControlPlaneCommands } from "./controlPlaneCommands.js";
44
55
  import { HELP_SECTIONS, VIEW_SHORTCUTS } from "./keymap.js";
45
56
  import { theme } from "./theme.js";
46
57
  const LOG_UPDATE_THROTTLE_MS = 80;
@@ -83,6 +94,12 @@ export class App {
83
94
  get deps() {
84
95
  return this.session.deps;
85
96
  }
97
+ get agents() {
98
+ return this.session.agents;
99
+ }
100
+ get environment() {
101
+ return this.session.environment;
102
+ }
86
103
  get config() {
87
104
  return this.session.config;
88
105
  }
@@ -161,19 +178,32 @@ export class App {
161
178
  logger: logger.child("services"),
162
179
  intervalMs: config.refresh.services,
163
180
  });
181
+ const tailscale = new TailscaleService({
182
+ machines: config.remoteMachines,
183
+ logger: logger.child("tailscale"),
184
+ });
164
185
  services.registerAll([
165
186
  new DockerService({ rootDir }),
166
187
  new PostgresService(),
167
188
  new RedisService(),
168
189
  new OllamaService(),
169
190
  ...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,
191
+ ...config.services.map((entry) => entry.type === "ollama"
192
+ ? new OllamaService({ id: entry.id, name: entry.name, baseUrl: entry.baseUrl })
193
+ : new PortService({
194
+ id: entry.id,
195
+ name: entry.name,
196
+ port: entry.port,
197
+ host: entry.host,
198
+ healthPath: entry.healthPath,
199
+ category: entry.category,
200
+ })),
201
+ tailscale,
202
+ ...config.remoteMachines.map((machine) => new RemoteMachineService({
203
+ machine,
204
+ tailscale: () => tailscale.getLastStatus(),
205
+ service: (id) => services.get(id),
206
+ logger: logger.child("remote"),
177
207
  })),
178
208
  ]);
179
209
  const git = new GitService({ rootDir, logger: logger.child("git") });
@@ -182,12 +212,24 @@ export class App {
182
212
  manager: packageManager.manager,
183
213
  logger: logger.child("deps"),
184
214
  });
215
+ const agents = new AgentManager({
216
+ processes,
217
+ adapters: createAgentAdapters(config.agents),
218
+ logger: logger.child("agents"),
219
+ });
220
+ const environment = new EnvironmentService({ rootDir, logger: logger.child("environment") });
221
+ const scripts = readScripts(packageJson);
222
+ const resolvedVerify = resolveVerifySteps(config.verify, {
223
+ commands: config.commands,
224
+ scripts,
225
+ packageManager: packageManager.manager,
226
+ });
185
227
  const project = {
186
228
  name: config.name ?? (typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir)),
187
229
  rootDir,
188
230
  packageManager,
189
231
  configPath: loaded.filePath,
190
- configIssues: loaded.issues,
232
+ configIssues: [...loaded.issues, ...resolvedVerify.issues],
191
233
  host: system.getHostInfo(),
192
234
  tools: { git: null, docker: null, packageManager: null },
193
235
  };
@@ -204,8 +246,15 @@ export class App {
204
246
  git,
205
247
  system,
206
248
  deps,
249
+ agents,
250
+ environment,
207
251
  project,
208
- scripts: readScripts(packageJson),
252
+ verifySteps: resolvedVerify.steps,
253
+ verify: { configured: resolvedVerify.steps.length, running: false, steps: [] },
254
+ verifyIssues: [...loaded.issues.filter(isVerifyIssue), ...resolvedVerify.issues],
255
+ verifyAbort: null,
256
+ setupAgentSessionId: null,
257
+ scripts,
209
258
  scriptRuns: {},
210
259
  wiring: new Subscriptions(),
211
260
  timers: [],
@@ -217,11 +266,18 @@ export class App {
217
266
  this.commands.registerAll(createBuiltinCommands());
218
267
  this.commands.registerAll(createScriptCommands(this.session.scripts));
219
268
  this.commands.registerAll(createConfigCommands(this.config.commands, (entry) => this.runConfigCommand(entry)));
269
+ this.commands.registerAll(createControlPlaneCommands({
270
+ agents: this.session.agents.list(),
271
+ machines: this.config.remoteMachines,
272
+ }));
220
273
  }
221
274
  reportConfigIssues() {
222
275
  for (const issue of this.session.project.configIssues) {
223
276
  this.pushNotification("warn", `Config: ${issue}`);
224
277
  }
278
+ if (!this.session.project.configPath) {
279
+ this.pushNotification("info", "No .devcc.ts yet: press / and run “Project: set up .devcc.ts”");
280
+ }
225
281
  }
226
282
  // ---------------------------------------------------------------------
227
283
  // UI construction
@@ -267,6 +323,7 @@ export class App {
267
323
  new ServicesView(deps),
268
324
  new ScriptsView(deps),
269
325
  new GitView(deps),
326
+ new WorktreesView(deps),
270
327
  new LogsView(deps),
271
328
  new DependenciesView(deps),
272
329
  new SystemView(deps),
@@ -322,12 +379,12 @@ export class App {
322
379
  */
323
380
  wireSession(session) {
324
381
  const { store } = this;
325
- const { wiring, processes, services, git, system, logs, deps } = session;
382
+ const { wiring, processes, services, git, system, logs, deps, environment } = session;
326
383
  const active = () => this.session === session;
327
384
  const notify = (level, message, detail) => this.pushNotification(level, active() ? message : `[${session.project.name}] ${message}`, detail);
328
385
  const syncProcesses = () => {
329
386
  if (active())
330
- store.patch({ processes: processes.list() });
387
+ store.patch({ processes: processes.list(), agents: session.agents.sessions() });
331
388
  this.refreshSidebarProjects();
332
389
  };
333
390
  processes.on("process:changed", syncProcesses);
@@ -335,6 +392,8 @@ export class App {
335
392
  processes.on("process:exit", (process) => {
336
393
  this.finishScriptRun(session, process.id, process.exitCode ?? 0);
337
394
  syncProcesses();
395
+ if (process.id === session.setupAgentSessionId)
396
+ void this.finishConfigSetup(session, process);
338
397
  });
339
398
  processes.on("process:error", (error) => notify("error", error.message, error.detail));
340
399
  processes.on("process:retry", (managed, delayMs) => {
@@ -362,12 +421,15 @@ export class App {
362
421
  if (active())
363
422
  store.patch({ system: metrics });
364
423
  });
365
- system.on("system:tools", (tools) => {
424
+ wiring.add(() => system.removeAllListeners());
425
+ environment.on("environment:changed", (snapshot) => {
426
+ const tools = toolVersionsFrom(snapshot, session.project.packageManager.manager);
366
427
  session.project = { ...session.project, tools };
367
- if (active())
368
- store.setState((state) => ({ ...state, project: { ...state.project, tools } }));
428
+ if (active()) {
429
+ store.setState((state) => ({ ...state, environment: snapshot, project: { ...state.project, tools } }));
430
+ }
369
431
  });
370
- wiring.add(() => system.removeAllListeners());
432
+ wiring.add(() => environment.removeAllListeners());
371
433
  logs.on("log:sources", (sources) => {
372
434
  if (!active())
373
435
  return;
@@ -411,6 +473,9 @@ export class App {
411
473
  this.subscriptions.add(store.watch((state) => state.services, markDirty, shallowArrayEqual));
412
474
  this.subscriptions.add(store.watch((state) => state.git, markDirty));
413
475
  this.subscriptions.add(store.watch((state) => state.dependencies, markDirty));
476
+ this.subscriptions.add(store.watch((state) => state.agents, markDirty, shallowArrayEqual));
477
+ this.subscriptions.add(store.watch((state) => state.environment, markDirty));
478
+ this.subscriptions.add(store.watch((state) => state.verify, markDirty));
414
479
  this.subscriptions.add(store.watch((state) => state.projects, markDirty, shallowArrayEqual));
415
480
  this.subscriptions.add(store.watch((state) => state.system, markDirty));
416
481
  this.subscriptions.add(store.watch((state) => state.scripts, markDirty));
@@ -427,6 +492,7 @@ export class App {
427
492
  processes: this.processes.list(),
428
493
  services: this.services.list(),
429
494
  logSources: this.logs.sources(),
495
+ verify: this.session.verify,
430
496
  });
431
497
  store.setState((state) => ({
432
498
  ...state,
@@ -461,7 +527,9 @@ export class App {
461
527
  session.timers.push(gitTimer);
462
528
  void session.git.refresh();
463
529
  void session.services.detectAll().then(() => session.services.start());
464
- void session.system.refreshTools(session.project.packageManager.manager);
530
+ // Tool versions change rarely: detect once per project, refresh on request.
531
+ if (!session.environment.getSnapshot().checkedAt)
532
+ void session.environment.refresh();
465
533
  if (!session.processes.list().some((entry) => entry.startedAt)) {
466
534
  // Autostart only the first time a project is opened, never on reactivation.
467
535
  void session.processes.startAutoStart();
@@ -616,6 +684,11 @@ export class App {
616
684
  void this.commands.run("app.refresh", this.context);
617
685
  return;
618
686
  }
687
+ if (key.shift && key.name === "v") {
688
+ key.preventDefault();
689
+ void this.runCommand("verify.run");
690
+ return;
691
+ }
619
692
  const state = this.store.getState();
620
693
  // Views get first refusal so `s`/`r`/`d` mean the right thing per pane.
621
694
  const view = this.views.get(this.activeView);
@@ -872,6 +945,8 @@ export class App {
872
945
  logs: this.logs,
873
946
  system: this.system,
874
947
  deps: this.deps,
948
+ agents: this.agents,
949
+ environment: this.environment,
875
950
  navigate: (view) => updateUi(this.store, { view }),
876
951
  notify: (level, message, detail) => this.pushNotification(level, message, detail),
877
952
  confirm: (title, message) => this.askConfirm(title, message),
@@ -883,6 +958,17 @@ export class App {
883
958
  openProjects: () => this.openProjects(),
884
959
  switchProject: (path) => this.openProject(path),
885
960
  closeProject: (path) => this.closeProject(path),
961
+ selectedWorktree: () => this.selectedWorktree(),
962
+ createWorktree: () => this.createWorktree(),
963
+ removeWorktree: (worktree) => this.removeWorktree(worktree),
964
+ launchAgent: (agentId, worktree) => this.launchAgent(agentId, worktree),
965
+ openInEditor: (path) => this.launch(() => editorCommand(path, this.config.editor), path),
966
+ openInTerminal: (path) => this.launch(() => terminalCommand(path, this.config.terminal), path),
967
+ runVerify: () => this.runVerify(),
968
+ remoteMachines: () => this.config.remoteMachines,
969
+ sshTo: (machineId) => this.sshTo(machineId),
970
+ setUpConfig: (how) => this.setUpConfig(how),
971
+ reloadConfig: () => this.reloadConfig(),
886
972
  quit: () => void this.quit(),
887
973
  };
888
974
  // Session services are swapped out on a project switch, so expose them as
@@ -894,6 +980,8 @@ export class App {
894
980
  logs: { get: () => this.logs, enumerable: true },
895
981
  system: { get: () => this.system, enumerable: true },
896
982
  deps: { get: () => this.deps, enumerable: true },
983
+ agents: { get: () => this.agents, enumerable: true },
984
+ environment: { get: () => this.environment, enumerable: true },
897
985
  });
898
986
  return context;
899
987
  }
@@ -1081,6 +1169,9 @@ export class App {
1081
1169
  scripts: { available: session.scripts, runs: session.scriptRuns },
1082
1170
  git: session.git.getSnapshot(),
1083
1171
  dependencies: session.deps.getSnapshot(),
1172
+ agents: session.agents.sessions(),
1173
+ environment: session.environment.getSnapshot(),
1174
+ verify: session.verify,
1084
1175
  system: session.system.getMetrics(),
1085
1176
  logSources: session.logs.sources(),
1086
1177
  projects: [],
@@ -1159,6 +1250,333 @@ export class App {
1159
1250
  }), this.session.rootDir);
1160
1251
  }
1161
1252
  // ---------------------------------------------------------------------
1253
+ // Worktrees, agents, verification, remote
1254
+ // ---------------------------------------------------------------------
1255
+ selectedWorktree() {
1256
+ const state = this.store.getState();
1257
+ if (state.ui.view === "worktrees") {
1258
+ const entry = selectedWorktreeEntry(state);
1259
+ if (entry?.worktree)
1260
+ return entry.worktree;
1261
+ }
1262
+ return state.git.worktrees.find((worktree) => worktree.isCurrent);
1263
+ }
1264
+ async createWorktree() {
1265
+ const snapshot = this.git.getSnapshot();
1266
+ if (!snapshot.isRepository) {
1267
+ this.pushNotification("warn", "Not a git repository");
1268
+ return;
1269
+ }
1270
+ const main = snapshot.worktrees.find((worktree) => worktree.isMain);
1271
+ if (!main) {
1272
+ this.pushNotification("warn", "Could not read this repository's worktrees yet");
1273
+ return;
1274
+ }
1275
+ const branch = (await this.askPrompt("New worktree: branch (created if it does not exist)", "feature/name"))?.trim();
1276
+ if (!branch)
1277
+ return;
1278
+ const current = snapshot.status.branch ?? "HEAD";
1279
+ const base = await this.askPrompt(`Base for ${branch} (empty: ${current})`, current);
1280
+ if (base === null)
1281
+ return;
1282
+ const suggested = defaultWorktreePath(main.path, branch, this.config.worktrees?.directory);
1283
+ const path = await this.askPrompt(`Directory (empty: ${tildify(suggested)})`, tildify(suggested));
1284
+ if (path === null)
1285
+ return;
1286
+ const target = path.trim() === "" ? suggested : resolve(main.path, expandHome(path.trim()));
1287
+ try {
1288
+ this.footer.setStatus(`Creating worktree ${branch}…`);
1289
+ await this.git.createWorktree({ branch, base: base.trim() || undefined, path: target });
1290
+ this.pushNotification("success", `Created worktree ${branch} at ${tildify(target)}`);
1291
+ }
1292
+ catch (error) {
1293
+ const devccError = toDevccError(error, "git");
1294
+ this.pushNotification("error", devccError.message, devccError.detail);
1295
+ }
1296
+ }
1297
+ async removeWorktree(worktree) {
1298
+ const busy = [
1299
+ ...this.agents.busyPaths(),
1300
+ ...this.processes
1301
+ .list()
1302
+ .filter((entry) => isActive(entry.status))
1303
+ .map((entry) => resolve(this.session.rootDir, entry.cwd ?? ".")),
1304
+ // A worktree open as a project in devcc is in use too.
1305
+ ...[...this.sessions.keys()].filter((path) => path !== this.session.rootDir),
1306
+ ];
1307
+ const blocker = removalBlocker(worktree, busy);
1308
+ if (blocker) {
1309
+ this.pushNotification("warn", blocker);
1310
+ return;
1311
+ }
1312
+ 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.`);
1313
+ if (!confirmed)
1314
+ return;
1315
+ try {
1316
+ await this.git.removeWorktree(worktree.path, busy);
1317
+ this.pushNotification("success", `Removed worktree ${worktreeLabel(worktree)}`);
1318
+ }
1319
+ catch (error) {
1320
+ const devccError = toDevccError(error, "git");
1321
+ this.pushNotification("error", devccError.message, devccError.detail);
1322
+ }
1323
+ }
1324
+ async launchAgent(agentId, worktree) {
1325
+ const adapter = this.agents.get(agentId);
1326
+ if (!adapter) {
1327
+ this.pushNotification("warn", `Unknown agent "${agentId}"`);
1328
+ return;
1329
+ }
1330
+ const target = worktree ?? this.selectedWorktree();
1331
+ const cwd = target?.path ?? this.session.rootDir;
1332
+ const where = target ? worktreeLabel(target) : basename(cwd);
1333
+ const task = await this.askPrompt(`Task for ${adapter.name} in ${where}`, "Describe what the agent should do");
1334
+ if (task === null || task.trim() === "")
1335
+ return;
1336
+ try {
1337
+ const session = await this.agents.launch(agentId, { cwd, prompt: task, branch: target?.branch });
1338
+ updateUi(this.store, { logSource: session.id });
1339
+ this.pushNotification("success", `${adapter.name} started in ${where}`);
1340
+ }
1341
+ catch (error) {
1342
+ const devccError = toDevccError(error, "process");
1343
+ this.pushNotification("error", devccError.message, devccError.detail);
1344
+ }
1345
+ }
1346
+ /** Build and start an external launch; a missing configuration becomes a notification. */
1347
+ async launch(build, cwd) {
1348
+ try {
1349
+ const command = build();
1350
+ await launchDetached(command, cwd);
1351
+ this.logger.log("debug", "launch", "opened", { command: command.command, cwd });
1352
+ }
1353
+ catch (error) {
1354
+ const devccError = toDevccError(error, "command");
1355
+ this.pushNotification("error", devccError.message, devccError.detail);
1356
+ }
1357
+ }
1358
+ async sshTo(machineId) {
1359
+ const machine = this.config.remoteMachines.find((entry) => entry.id === machineId);
1360
+ if (!machine) {
1361
+ this.pushNotification("warn", `Unknown remote machine "${machineId}"`);
1362
+ return;
1363
+ }
1364
+ await this.launch(() => sshCommand(machine, this.config.terminal), this.session.rootDir);
1365
+ }
1366
+ /**
1367
+ * Run verification through the ProcessManager: each step is a managed task,
1368
+ * so its output streams into Logs and quitting stops it like anything else.
1369
+ */
1370
+ async runVerify() {
1371
+ const session = this.session;
1372
+ if (session.verify.running) {
1373
+ this.pushNotification("warn", "Verification is already running");
1374
+ return;
1375
+ }
1376
+ if (session.verifyIssues.length > 0) {
1377
+ this.pushNotification("error", `Verify refused: ${session.verifyIssues[0] ?? "invalid verify config"}`, session.verifyIssues.join("\n"));
1378
+ return;
1379
+ }
1380
+ if (session.verifySteps.length === 0) {
1381
+ this.pushNotification("warn", "No verification steps configured", 'Add verify: ["lint", "test"] to .devcc.ts');
1382
+ return;
1383
+ }
1384
+ const controller = new AbortController();
1385
+ session.verifyAbort = controller;
1386
+ const publish = (verify) => {
1387
+ session.verify = verify;
1388
+ if (session === this.session)
1389
+ this.store.patch({ verify });
1390
+ };
1391
+ const startedAt = new Date();
1392
+ publish({ configured: session.verifySteps.length, running: true, steps: [], startedAt });
1393
+ this.pushNotification("info", `Verifying: ${session.verifySteps.map((step) => step.name).join(", ")}`);
1394
+ const report = await runVerification(session.verifySteps, this.processStepExecutor(session), {
1395
+ signal: controller.signal,
1396
+ onProgress: (steps) => publish({ ...session.verify, steps: [...steps] }),
1397
+ });
1398
+ session.verifyAbort = null;
1399
+ publish({
1400
+ configured: session.verifySteps.length,
1401
+ running: false,
1402
+ steps: report.steps,
1403
+ startedAt,
1404
+ finishedAt: new Date(),
1405
+ ok: report.ok,
1406
+ });
1407
+ this.pushNotification(report.ok ? "success" : "error", `Verify: ${summariseVerify(report)}`);
1408
+ }
1409
+ processStepExecutor(session) {
1410
+ return async (step, signal) => {
1411
+ const { processes } = session;
1412
+ const id = `verify:${step.id}`;
1413
+ if (processes.isRunning(id))
1414
+ return { exitCode: null, error: "already running" };
1415
+ processes.register({
1416
+ id,
1417
+ name: `verify · ${step.name}`,
1418
+ command: step.command,
1419
+ args: step.args,
1420
+ cwd: step.cwd,
1421
+ env: { NO_COLOR: "1" },
1422
+ kind: "task",
1423
+ });
1424
+ let onChange;
1425
+ const finished = new Promise((resolveFinished) => {
1426
+ let started = false;
1427
+ onChange = (snapshot) => {
1428
+ if (snapshot.id !== id)
1429
+ return;
1430
+ if (snapshot.status === "starting" || snapshot.status === "running")
1431
+ started = true;
1432
+ else if (started && (snapshot.status === "stopped" || snapshot.status === "failed")) {
1433
+ resolveFinished(snapshot);
1434
+ }
1435
+ };
1436
+ processes.on("process:changed", onChange);
1437
+ });
1438
+ const onAbort = () => void processes.stop(id).catch(() => undefined);
1439
+ signal.addEventListener("abort", onAbort, { once: true });
1440
+ try {
1441
+ if (session === this.session)
1442
+ updateUi(this.store, { logSource: id });
1443
+ await processes.start(id);
1444
+ const result = await finished;
1445
+ return {
1446
+ exitCode: result.exitCode ?? null,
1447
+ error: signal.aborted
1448
+ ? "cancelled"
1449
+ : result.exitSignal
1450
+ ? `terminated by ${result.exitSignal}`
1451
+ : result.exitCode === undefined
1452
+ ? result.lastError
1453
+ : undefined,
1454
+ };
1455
+ }
1456
+ catch (error) {
1457
+ return { exitCode: null, error: errorMessage(error) };
1458
+ }
1459
+ finally {
1460
+ signal.removeEventListener("abort", onAbort);
1461
+ // Also runs when start() throws, so the listener never outlives the step.
1462
+ if (onChange)
1463
+ processes.off("process:changed", onChange);
1464
+ }
1465
+ };
1466
+ }
1467
+ // ---------------------------------------------------------------------
1468
+ // Project setup (.devcc.ts)
1469
+ // ---------------------------------------------------------------------
1470
+ async setUpConfig(how) {
1471
+ const session = this.session;
1472
+ const facts = detectProject(session.rootDir);
1473
+ if (how === "starter") {
1474
+ if (facts.configPath) {
1475
+ const replace = await this.askConfirm("Replace the existing config with a starter?", `${tildify(facts.configPath)} will be overwritten with what devcc detects. Consider setting it up with an agent instead, which keeps what is still right.`);
1476
+ if (!replace)
1477
+ return;
1478
+ }
1479
+ const target = join(session.rootDir, ".devcc.ts");
1480
+ writeFileSync(target, renderStarterConfig(facts), "utf8");
1481
+ this.pushNotification("success", `Created ${tildify(target)}`);
1482
+ await this.reloadConfig();
1483
+ return;
1484
+ }
1485
+ const adapter = this.agents.get(how);
1486
+ if (!adapter) {
1487
+ this.pushNotification("warn", `Unknown agent "${how}"`);
1488
+ return;
1489
+ }
1490
+ if (session.setupAgentSessionId && session.processes.isRunning(session.setupAgentSessionId)) {
1491
+ this.pushNotification("warn", "An agent is already setting up .devcc.ts");
1492
+ return;
1493
+ }
1494
+ try {
1495
+ const agentSession = await this.agents.launch(how, {
1496
+ cwd: session.rootDir,
1497
+ prompt: buildSetupPrompt(facts, "print"),
1498
+ });
1499
+ session.setupAgentSessionId = agentSession.id;
1500
+ updateUi(this.store, { logSource: agentSession.id });
1501
+ this.pushNotification("info", `${adapter.name} is reading the project; devcc will offer its .devcc.ts when it finishes`);
1502
+ }
1503
+ catch (error) {
1504
+ const devccError = toDevccError(error, "process");
1505
+ this.pushNotification("error", devccError.message, devccError.detail);
1506
+ }
1507
+ }
1508
+ /**
1509
+ * The setup agent exited: take the config it printed, validate it from a
1510
+ * scratch file under .devcc/, and write it only if the user agrees.
1511
+ */
1512
+ async finishConfigSetup(session, agent) {
1513
+ session.setupAgentSessionId = null;
1514
+ const name = session.project.name;
1515
+ if (agent.exitCode !== 0) {
1516
+ this.pushNotification("error", `[${name}] Config setup agent exited with code ${agent.exitCode ?? "?"}; see its logs`);
1517
+ return;
1518
+ }
1519
+ const output = (session.logs.get(agent.id)?.toArray() ?? [])
1520
+ .filter((entry) => entry.stream !== "system")
1521
+ .map((entry) => entry.text)
1522
+ .join("\n");
1523
+ const proposal = extractConfigFromOutput(output);
1524
+ if (!proposal) {
1525
+ this.pushNotification("warn", `[${name}] The agent did not print a .devcc.ts; see its logs`);
1526
+ return;
1527
+ }
1528
+ const scratchDir = join(session.rootDir, ".devcc");
1529
+ const proposedPath = join(scratchDir, "proposed.devcc.ts");
1530
+ mkdirSync(scratchDir, { recursive: true });
1531
+ writeFileSync(proposedPath, proposal, "utf8");
1532
+ const loaded = await loadConfigFile(proposedPath);
1533
+ const problems = configProblems(session.rootDir, loaded);
1534
+ const { config } = loaded;
1535
+ const current = session.project.configPath;
1536
+ const target = current && !current.endsWith(".json") ? current : join(session.rootDir, ".devcc.ts");
1537
+ const summary = [
1538
+ `${config.processes.length} process(es), ${config.commands.length} command(s), verify: ${config.verify.map((entry) => (typeof entry === "string" ? entry : entry.id)).join(", ") || "none"}.`,
1539
+ problems.length > 0 ? `${problems.length} issue(s): ${problems[0] ?? ""}` : "It validates cleanly.",
1540
+ `Review it first in ${tildify(proposedPath)}.`,
1541
+ ].join(" ");
1542
+ const write = await this.askConfirm(`Write ${current ? "the updated" : "the proposed"} ${tildify(target)}?`, summary);
1543
+ if (!write) {
1544
+ this.pushNotification("info", `Kept the proposal at ${tildify(proposedPath)}`);
1545
+ return;
1546
+ }
1547
+ writeFileSync(target, proposal, "utf8");
1548
+ this.pushNotification("success", `Wrote ${tildify(target)}`);
1549
+ if (session === this.session)
1550
+ await this.reloadConfig();
1551
+ else
1552
+ this.pushNotification("info", `Switch to ${name} and reload its config to use it`);
1553
+ }
1554
+ /** Rebuild the active project's session so a changed config takes effect. */
1555
+ async reloadConfig() {
1556
+ const previous = this.session;
1557
+ const running = previous.processes.list().filter((entry) => isActive(entry.status));
1558
+ if (running.length > 0) {
1559
+ const confirmed = await this.askConfirm(`Reload the config and stop ${running.length} running process(es)?`, `${running.map((entry) => entry.name).join(", ")} will be stopped; start them again afterwards.`);
1560
+ if (!confirmed)
1561
+ return;
1562
+ }
1563
+ this.stopSessionBackground(previous);
1564
+ previous.verifyAbort?.abort();
1565
+ await previous.processes.stopAll();
1566
+ previous.wiring.dispose();
1567
+ try {
1568
+ const next = await this.createSession(previous.rootDir);
1569
+ this.sessions.set(next.rootDir, next);
1570
+ this.wireSession(next);
1571
+ this.activateSession(next);
1572
+ this.reportConfigIssues();
1573
+ this.pushNotification("success", `Reloaded ${next.project.configPath ? basename(next.project.configPath) : "config"}`);
1574
+ }
1575
+ catch (error) {
1576
+ this.pushNotification("error", `Could not reload the config: ${errorMessage(error)}`);
1577
+ }
1578
+ }
1579
+ // ---------------------------------------------------------------------
1162
1580
  // Dependencies
1163
1581
  // ---------------------------------------------------------------------
1164
1582
  /** Update a single package, gated by a confirmation (it rewrites the lockfile). */
@@ -1204,6 +1622,8 @@ export class App {
1204
1622
  this.logger.log("info", "app", "shutting down");
1205
1623
  this.footer.setStatus("Shutting down…");
1206
1624
  this.stopBackground();
1625
+ for (const session of this.sessions.values())
1626
+ session.verifyAbort?.abort();
1207
1627
  // Every open project, not just the visible one.
1208
1628
  await Promise.all([...this.sessions.values()].map(async (session) => {
1209
1629
  try {
@@ -1252,4 +1672,16 @@ export class App {
1252
1672
  return this.logs;
1253
1673
  }
1254
1674
  }
1675
+ /** The header and System view still read `project.tools`; derive it from environment detection. */
1676
+ function toolVersionsFrom(snapshot, packageManager) {
1677
+ const version = (id) => {
1678
+ const tool = snapshot.tools.find((entry) => entry.id === id);
1679
+ return tool?.installed ? (tool.version ?? "installed") : null;
1680
+ };
1681
+ return { git: version("git"), docker: version("docker"), packageManager: version(packageManager) };
1682
+ }
1683
+ function expandHome(path) {
1684
+ const home = process.env.HOME;
1685
+ return home && (path === "~" || path.startsWith("~/")) ? `${home}${path.slice(1)}` : path;
1686
+ }
1255
1687
  //# sourceMappingURL=App.js.map