@doriandev/devcc 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +589 -389
- package/bin/devcc.mjs +50 -23
- package/dist/devcc/agents/agents.d.ts +119 -0
- package/dist/devcc/agents/agents.d.ts.map +1 -0
- package/dist/devcc/agents/agents.js +182 -0
- package/dist/devcc/agents/agents.js.map +1 -0
- package/dist/devcc/app/App.d.ts +23 -0
- package/dist/devcc/app/App.d.ts.map +1 -1
- package/dist/devcc/app/App.js +451 -19
- package/dist/devcc/app/App.js.map +1 -1
- package/dist/devcc/app/controlPlaneCommands.d.ts +14 -0
- package/dist/devcc/app/controlPlaneCommands.d.ts.map +1 -0
- package/dist/devcc/app/controlPlaneCommands.js +204 -0
- package/dist/devcc/app/controlPlaneCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -1
- package/dist/devcc/app/keymap.js +25 -3
- package/dist/devcc/app/keymap.js.map +1 -1
- package/dist/devcc/automation.d.ts +52 -0
- package/dist/devcc/automation.d.ts.map +1 -0
- package/dist/devcc/automation.js +339 -0
- package/dist/devcc/automation.js.map +1 -0
- package/dist/devcc/cli.d.ts.map +1 -1
- package/dist/devcc/cli.js +73 -3
- package/dist/devcc/cli.js.map +1 -1
- package/dist/devcc/components/Sidebar.d.ts.map +1 -1
- package/dist/devcc/components/Sidebar.js +1 -0
- package/dist/devcc/components/Sidebar.js.map +1 -1
- package/dist/devcc/components/primitives.d.ts.map +1 -1
- package/dist/devcc/components/primitives.js +3 -0
- package/dist/devcc/components/primitives.js.map +1 -1
- package/dist/devcc/core/commands.d.ts +33 -0
- package/dist/devcc/core/commands.d.ts.map +1 -1
- package/dist/devcc/core/commands.js.map +1 -1
- package/dist/devcc/core/config.d.ts +71 -3
- package/dist/devcc/core/config.d.ts.map +1 -1
- package/dist/devcc/core/config.js +189 -5
- package/dist/devcc/core/config.js.map +1 -1
- package/dist/devcc/core/state.d.ts +19 -1
- package/dist/devcc/core/state.d.ts.map +1 -1
- package/dist/devcc/core/state.js +6 -0
- package/dist/devcc/core/state.js.map +1 -1
- package/dist/devcc/environment/EnvironmentService.d.ts +58 -0
- package/dist/devcc/environment/EnvironmentService.d.ts.map +1 -0
- package/dist/devcc/environment/EnvironmentService.js +159 -0
- package/dist/devcc/environment/EnvironmentService.js.map +1 -0
- package/dist/devcc/environment/integrations.d.ts +52 -0
- package/dist/devcc/environment/integrations.d.ts.map +1 -0
- package/dist/devcc/environment/integrations.js +94 -0
- package/dist/devcc/environment/integrations.js.map +1 -0
- package/dist/devcc/environment/tools.d.ts +51 -0
- package/dist/devcc/environment/tools.d.ts.map +1 -0
- package/dist/devcc/environment/tools.js +99 -0
- package/dist/devcc/environment/tools.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +26 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -1
- package/dist/devcc/git/GitService.js +71 -2
- package/dist/devcc/git/GitService.js.map +1 -1
- package/dist/devcc/git/worktrees.d.ts +88 -0
- package/dist/devcc/git/worktrees.d.ts.map +1 -0
- package/dist/devcc/git/worktrees.js +226 -0
- package/dist/devcc/git/worktrees.js.map +1 -0
- package/dist/devcc/index.d.ts +15 -3
- package/dist/devcc/index.d.ts.map +1 -1
- package/dist/devcc/index.js +14 -2
- package/dist/devcc/index.js.map +1 -1
- package/dist/devcc/init/detect.d.ts +55 -0
- package/dist/devcc/init/detect.d.ts.map +1 -0
- package/dist/devcc/init/detect.js +207 -0
- package/dist/devcc/init/detect.js.map +1 -0
- package/dist/devcc/init/prompt.d.ts +25 -0
- package/dist/devcc/init/prompt.d.ts.map +1 -0
- package/dist/devcc/init/prompt.js +156 -0
- package/dist/devcc/init/prompt.js.map +1 -0
- package/dist/devcc/platform/launch.d.ts +30 -0
- package/dist/devcc/platform/launch.d.ts.map +1 -0
- package/dist/devcc/platform/launch.js +93 -0
- package/dist/devcc/platform/launch.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +10 -2
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -1
- package/dist/devcc/services/ServiceManager.js +18 -4
- package/dist/devcc/services/ServiceManager.js.map +1 -1
- package/dist/devcc/services/node.d.ts +11 -0
- package/dist/devcc/services/node.d.ts.map +1 -1
- package/dist/devcc/services/node.js +20 -18
- package/dist/devcc/services/node.js.map +1 -1
- package/dist/devcc/services/ollama.d.ts +64 -5
- package/dist/devcc/services/ollama.d.ts.map +1 -1
- package/dist/devcc/services/ollama.js +118 -17
- package/dist/devcc/services/ollama.js.map +1 -1
- package/dist/devcc/services/remote.d.ts +90 -0
- package/dist/devcc/services/remote.d.ts.map +1 -0
- package/dist/devcc/services/remote.js +233 -0
- package/dist/devcc/services/remote.js.map +1 -0
- package/dist/devcc/services/types.d.ts +8 -1
- package/dist/devcc/services/types.d.ts.map +1 -1
- package/dist/devcc/services/types.js.map +1 -1
- package/dist/devcc/utils/exec.d.ts +6 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -1
- package/dist/devcc/utils/exec.js +12 -2
- package/dist/devcc/utils/exec.js.map +1 -1
- package/dist/devcc/verify/verify.d.ts +107 -0
- package/dist/devcc/verify/verify.d.ts.map +1 -0
- package/dist/devcc/verify/verify.js +203 -0
- package/dist/devcc/verify/verify.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -1
- package/dist/devcc/views/OverviewView.js +58 -2
- package/dist/devcc/views/OverviewView.js.map +1 -1
- package/dist/devcc/views/ServicesView.d.ts +2 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -1
- package/dist/devcc/views/ServicesView.js +15 -0
- package/dist/devcc/views/ServicesView.js.map +1 -1
- package/dist/devcc/views/SystemView.d.ts +10 -1
- package/dist/devcc/views/SystemView.d.ts.map +1 -1
- package/dist/devcc/views/SystemView.js +127 -19
- package/dist/devcc/views/SystemView.js.map +1 -1
- package/dist/devcc/views/WorktreesView.d.ts +48 -0
- package/dist/devcc/views/WorktreesView.d.ts.map +1 -0
- package/dist/devcc/views/WorktreesView.js +235 -0
- package/dist/devcc/views/WorktreesView.js.map +1 -0
- package/package.json +1 -1
- package/src/devcc/agents/agents.ts +292 -0
- package/src/devcc/app/App.ts +529 -22
- package/src/devcc/app/controlPlaneCommands.ts +227 -0
- package/src/devcc/app/keymap.ts +25 -3
- package/src/devcc/automation.ts +409 -0
- package/src/devcc/cli.ts +77 -3
- package/src/devcc/components/Sidebar.ts +1 -0
- package/src/devcc/components/primitives.ts +3 -0
- package/src/devcc/core/commands.ts +34 -0
- package/src/devcc/core/config.ts +275 -11
- package/src/devcc/core/state.ts +25 -0
- package/src/devcc/environment/EnvironmentService.ts +215 -0
- package/src/devcc/environment/integrations.ts +133 -0
- package/src/devcc/environment/tools.ts +151 -0
- package/src/devcc/git/GitService.ts +104 -2
- package/src/devcc/git/worktrees.ts +256 -0
- package/src/devcc/index.ts +66 -2
- package/src/devcc/init/detect.ts +268 -0
- package/src/devcc/init/prompt.ts +179 -0
- package/src/devcc/platform/launch.ts +125 -0
- package/src/devcc/services/ServiceManager.ts +15 -3
- package/src/devcc/services/node.ts +17 -14
- package/src/devcc/services/ollama.ts +162 -19
- package/src/devcc/services/remote.ts +306 -0
- package/src/devcc/services/types.ts +9 -1
- package/src/devcc/utils/exec.ts +26 -2
- package/src/devcc/verify/verify.ts +316 -0
- package/src/devcc/views/OverviewView.ts +84 -2
- package/src/devcc/views/ServicesView.ts +17 -0
- package/src/devcc/views/SystemView.ts +144 -22
- package/src/devcc/views/WorktreesView.ts +270 -0
package/dist/devcc/app/App.js
CHANGED
|
@@ -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) =>
|
|
171
|
-
id: entry.id,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
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
|