@doriandev/devcc 0.1.2
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/LICENSE +15 -0
- package/README.md +659 -0
- package/bin/devcc.mjs +66 -0
- package/dist/devcc/app/App.d.ts +178 -0
- package/dist/devcc/app/App.d.ts.map +1 -0
- package/dist/devcc/app/App.js +1255 -0
- package/dist/devcc/app/App.js.map +1 -0
- package/dist/devcc/app/builtinCommands.d.ts +17 -0
- package/dist/devcc/app/builtinCommands.d.ts.map +1 -0
- package/dist/devcc/app/builtinCommands.js +389 -0
- package/dist/devcc/app/builtinCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts +21 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -0
- package/dist/devcc/app/keymap.js +162 -0
- package/dist/devcc/app/keymap.js.map +1 -0
- package/dist/devcc/app/theme.d.ts +49 -0
- package/dist/devcc/app/theme.d.ts.map +1 -0
- package/dist/devcc/app/theme.js +68 -0
- package/dist/devcc/app/theme.js.map +1 -0
- package/dist/devcc/cli.d.ts +3 -0
- package/dist/devcc/cli.d.ts.map +1 -0
- package/dist/devcc/cli.js +113 -0
- package/dist/devcc/cli.js.map +1 -0
- package/dist/devcc/components/CommandPalette.d.ts +26 -0
- package/dist/devcc/components/CommandPalette.d.ts.map +1 -0
- package/dist/devcc/components/CommandPalette.js +113 -0
- package/dist/devcc/components/CommandPalette.js.map +1 -0
- package/dist/devcc/components/ConfirmDialog.d.ts +31 -0
- package/dist/devcc/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/devcc/components/ConfirmDialog.js +147 -0
- package/dist/devcc/components/ConfirmDialog.js.map +1 -0
- package/dist/devcc/components/Footer.d.ts +22 -0
- package/dist/devcc/components/Footer.d.ts.map +1 -0
- package/dist/devcc/components/Footer.js +72 -0
- package/dist/devcc/components/Footer.js.map +1 -0
- package/dist/devcc/components/Header.d.ts +12 -0
- package/dist/devcc/components/Header.d.ts.map +1 -0
- package/dist/devcc/components/Header.js +84 -0
- package/dist/devcc/components/Header.js.map +1 -0
- package/dist/devcc/components/HelpOverlay.d.ts +18 -0
- package/dist/devcc/components/HelpOverlay.d.ts.map +1 -0
- package/dist/devcc/components/HelpOverlay.js +48 -0
- package/dist/devcc/components/HelpOverlay.js.map +1 -0
- package/dist/devcc/components/Overlay.d.ts +31 -0
- package/dist/devcc/components/Overlay.d.ts.map +1 -0
- package/dist/devcc/components/Overlay.js +60 -0
- package/dist/devcc/components/Overlay.js.map +1 -0
- package/dist/devcc/components/ProjectPicker.d.ts +26 -0
- package/dist/devcc/components/ProjectPicker.d.ts.map +1 -0
- package/dist/devcc/components/ProjectPicker.js +114 -0
- package/dist/devcc/components/ProjectPicker.js.map +1 -0
- package/dist/devcc/components/Sidebar.d.ts +42 -0
- package/dist/devcc/components/Sidebar.d.ts.map +1 -0
- package/dist/devcc/components/Sidebar.js +127 -0
- package/dist/devcc/components/Sidebar.js.map +1 -0
- package/dist/devcc/components/StatusBadge.d.ts +16 -0
- package/dist/devcc/components/StatusBadge.d.ts.map +1 -0
- package/dist/devcc/components/StatusBadge.js +30 -0
- package/dist/devcc/components/StatusBadge.js.map +1 -0
- package/dist/devcc/components/primitives.d.ts +62 -0
- package/dist/devcc/components/primitives.d.ts.map +1 -0
- package/dist/devcc/components/primitives.js +169 -0
- package/dist/devcc/components/primitives.js.map +1 -0
- package/dist/devcc/components/styled.d.ts +16 -0
- package/dist/devcc/components/styled.d.ts.map +1 -0
- package/dist/devcc/components/styled.js +35 -0
- package/dist/devcc/components/styled.js.map +1 -0
- package/dist/devcc/core/commands.d.ts +89 -0
- package/dist/devcc/core/commands.d.ts.map +1 -0
- package/dist/devcc/core/commands.js +68 -0
- package/dist/devcc/core/commands.js.map +1 -0
- package/dist/devcc/core/config.d.ts +81 -0
- package/dist/devcc/core/config.d.ts.map +1 -0
- package/dist/devcc/core/config.js +313 -0
- package/dist/devcc/core/config.js.map +1 -0
- package/dist/devcc/core/events.d.ts +21 -0
- package/dist/devcc/core/events.d.ts.map +1 -0
- package/dist/devcc/core/events.js +27 -0
- package/dist/devcc/core/events.js.map +1 -0
- package/dist/devcc/core/state.d.ts +104 -0
- package/dist/devcc/core/state.d.ts.map +1 -0
- package/dist/devcc/core/state.js +139 -0
- package/dist/devcc/core/state.js.map +1 -0
- package/dist/devcc/deps/DependencyService.d.ts +50 -0
- package/dist/devcc/deps/DependencyService.d.ts.map +1 -0
- package/dist/devcc/deps/DependencyService.js +109 -0
- package/dist/devcc/deps/DependencyService.js.map +1 -0
- package/dist/devcc/deps/parse.d.ts +49 -0
- package/dist/devcc/deps/parse.d.ts.map +1 -0
- package/dist/devcc/deps/parse.js +158 -0
- package/dist/devcc/deps/parse.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +60 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -0
- package/dist/devcc/git/GitService.js +160 -0
- package/dist/devcc/git/GitService.js.map +1 -0
- package/dist/devcc/git/parse.d.ts +57 -0
- package/dist/devcc/git/parse.d.ts.map +1 -0
- package/dist/devcc/git/parse.js +174 -0
- package/dist/devcc/git/parse.js.map +1 -0
- package/dist/devcc/index.d.ts +37 -0
- package/dist/devcc/index.d.ts.map +1 -0
- package/dist/devcc/index.js +36 -0
- package/dist/devcc/index.js.map +1 -0
- package/dist/devcc/logs/LogBuffer.d.ts +86 -0
- package/dist/devcc/logs/LogBuffer.d.ts.map +1 -0
- package/dist/devcc/logs/LogBuffer.js +200 -0
- package/dist/devcc/logs/LogBuffer.js.map +1 -0
- package/dist/devcc/logs/merge.d.ts +36 -0
- package/dist/devcc/logs/merge.d.ts.map +1 -0
- package/dist/devcc/logs/merge.js +71 -0
- package/dist/devcc/logs/merge.js.map +1 -0
- package/dist/devcc/processes/ManagedProcess.d.ts +129 -0
- package/dist/devcc/processes/ManagedProcess.d.ts.map +1 -0
- package/dist/devcc/processes/ManagedProcess.js +80 -0
- package/dist/devcc/processes/ManagedProcess.js.map +1 -0
- package/dist/devcc/processes/ProcessManager.d.ts +112 -0
- package/dist/devcc/processes/ProcessManager.d.ts.map +1 -0
- package/dist/devcc/processes/ProcessManager.js +525 -0
- package/dist/devcc/processes/ProcessManager.js.map +1 -0
- package/dist/devcc/processes/graph.d.ts +35 -0
- package/dist/devcc/processes/graph.d.ts.map +1 -0
- package/dist/devcc/processes/graph.js +68 -0
- package/dist/devcc/processes/graph.js.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts +52 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.js +177 -0
- package/dist/devcc/projects/ProjectRegistry.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +48 -0
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -0
- package/dist/devcc/services/ServiceManager.js +146 -0
- package/dist/devcc/services/ServiceManager.js.map +1 -0
- package/dist/devcc/services/docker.d.ts +37 -0
- package/dist/devcc/services/docker.d.ts.map +1 -0
- package/dist/devcc/services/docker.js +179 -0
- package/dist/devcc/services/docker.js.map +1 -0
- package/dist/devcc/services/node.d.ts +40 -0
- package/dist/devcc/services/node.d.ts.map +1 -0
- package/dist/devcc/services/node.js +107 -0
- package/dist/devcc/services/node.js.map +1 -0
- package/dist/devcc/services/ollama.d.ts +27 -0
- package/dist/devcc/services/ollama.d.ts.map +1 -0
- package/dist/devcc/services/ollama.js +66 -0
- package/dist/devcc/services/ollama.js.map +1 -0
- package/dist/devcc/services/postgres.d.ts +30 -0
- package/dist/devcc/services/postgres.d.ts.map +1 -0
- package/dist/devcc/services/postgres.js +47 -0
- package/dist/devcc/services/postgres.js.map +1 -0
- package/dist/devcc/services/redis.d.ts +20 -0
- package/dist/devcc/services/redis.d.ts.map +1 -0
- package/dist/devcc/services/redis.js +39 -0
- package/dist/devcc/services/redis.js.map +1 -0
- package/dist/devcc/services/types.d.ts +51 -0
- package/dist/devcc/services/types.d.ts.map +1 -0
- package/dist/devcc/services/types.js +10 -0
- package/dist/devcc/services/types.js.map +1 -0
- package/dist/devcc/system/SystemMonitor.d.ts +87 -0
- package/dist/devcc/system/SystemMonitor.d.ts.map +1 -0
- package/dist/devcc/system/SystemMonitor.js +142 -0
- package/dist/devcc/system/SystemMonitor.js.map +1 -0
- package/dist/devcc/utils/errors.d.ts +31 -0
- package/dist/devcc/utils/errors.d.ts.map +1 -0
- package/dist/devcc/utils/errors.js +52 -0
- package/dist/devcc/utils/errors.js.map +1 -0
- package/dist/devcc/utils/exec.d.ts +41 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -0
- package/dist/devcc/utils/exec.js +112 -0
- package/dist/devcc/utils/exec.js.map +1 -0
- package/dist/devcc/utils/format.d.ts +18 -0
- package/dist/devcc/utils/format.d.ts.map +1 -0
- package/dist/devcc/utils/format.js +85 -0
- package/dist/devcc/utils/format.js.map +1 -0
- package/dist/devcc/utils/fuzzy.d.ts +12 -0
- package/dist/devcc/utils/fuzzy.d.ts.map +1 -0
- package/dist/devcc/utils/fuzzy.js +20 -0
- package/dist/devcc/utils/fuzzy.js.map +1 -0
- package/dist/devcc/utils/logger.d.ts +41 -0
- package/dist/devcc/utils/logger.d.ts.map +1 -0
- package/dist/devcc/utils/logger.js +117 -0
- package/dist/devcc/utils/logger.js.map +1 -0
- package/dist/devcc/utils/packageManager.d.ts +39 -0
- package/dist/devcc/utils/packageManager.d.ts.map +1 -0
- package/dist/devcc/utils/packageManager.js +85 -0
- package/dist/devcc/utils/packageManager.js.map +1 -0
- package/dist/devcc/utils/paths.d.ts +12 -0
- package/dist/devcc/utils/paths.d.ts.map +1 -0
- package/dist/devcc/utils/paths.js +39 -0
- package/dist/devcc/utils/paths.js.map +1 -0
- package/dist/devcc/views/DependenciesView.d.ts +34 -0
- package/dist/devcc/views/DependenciesView.d.ts.map +1 -0
- package/dist/devcc/views/DependenciesView.js +170 -0
- package/dist/devcc/views/DependenciesView.js.map +1 -0
- package/dist/devcc/views/GitView.d.ts +29 -0
- package/dist/devcc/views/GitView.d.ts.map +1 -0
- package/dist/devcc/views/GitView.js +226 -0
- package/dist/devcc/views/GitView.js.map +1 -0
- package/dist/devcc/views/LogsView.d.ts +39 -0
- package/dist/devcc/views/LogsView.d.ts.map +1 -0
- package/dist/devcc/views/LogsView.js +249 -0
- package/dist/devcc/views/LogsView.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts +16 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -0
- package/dist/devcc/views/OverviewView.js +135 -0
- package/dist/devcc/views/OverviewView.js.map +1 -0
- package/dist/devcc/views/ProcessesView.d.ts +26 -0
- package/dist/devcc/views/ProcessesView.d.ts.map +1 -0
- package/dist/devcc/views/ProcessesView.js +161 -0
- package/dist/devcc/views/ProcessesView.js.map +1 -0
- package/dist/devcc/views/ScriptsView.d.ts +30 -0
- package/dist/devcc/views/ScriptsView.d.ts.map +1 -0
- package/dist/devcc/views/ScriptsView.js +168 -0
- package/dist/devcc/views/ScriptsView.js.map +1 -0
- package/dist/devcc/views/ServicesView.d.ts +28 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -0
- package/dist/devcc/views/ServicesView.js +153 -0
- package/dist/devcc/views/ServicesView.js.map +1 -0
- package/dist/devcc/views/SettingsView.d.ts +20 -0
- package/dist/devcc/views/SettingsView.d.ts.map +1 -0
- package/dist/devcc/views/SettingsView.js +114 -0
- package/dist/devcc/views/SettingsView.js.map +1 -0
- package/dist/devcc/views/SystemView.d.ts +13 -0
- package/dist/devcc/views/SystemView.d.ts.map +1 -0
- package/dist/devcc/views/SystemView.js +82 -0
- package/dist/devcc/views/SystemView.js.map +1 -0
- package/dist/devcc/views/View.d.ts +35 -0
- package/dist/devcc/views/View.d.ts.map +1 -0
- package/dist/devcc/views/View.js +42 -0
- package/dist/devcc/views/View.js.map +1 -0
- package/package.json +79 -0
- package/src/devcc/app/App.ts +1463 -0
- package/src/devcc/app/builtinCommands.ts +429 -0
- package/src/devcc/app/keymap.ts +167 -0
- package/src/devcc/app/theme.ts +83 -0
- package/src/devcc/cli.ts +122 -0
- package/src/devcc/components/CommandPalette.ts +136 -0
- package/src/devcc/components/ConfirmDialog.ts +174 -0
- package/src/devcc/components/Footer.ts +85 -0
- package/src/devcc/components/Header.ts +93 -0
- package/src/devcc/components/HelpOverlay.ts +62 -0
- package/src/devcc/components/Overlay.ts +91 -0
- package/src/devcc/components/ProjectPicker.ts +138 -0
- package/src/devcc/components/Sidebar.ts +163 -0
- package/src/devcc/components/StatusBadge.ts +36 -0
- package/src/devcc/components/primitives.ts +213 -0
- package/src/devcc/components/styled.ts +44 -0
- package/src/devcc/core/commands.ts +140 -0
- package/src/devcc/core/config.ts +405 -0
- package/src/devcc/core/events.ts +41 -0
- package/src/devcc/core/state.ts +236 -0
- package/src/devcc/deps/DependencyService.ts +152 -0
- package/src/devcc/deps/parse.ts +210 -0
- package/src/devcc/git/GitService.ts +205 -0
- package/src/devcc/git/parse.ts +203 -0
- package/src/devcc/index.ts +115 -0
- package/src/devcc/logs/LogBuffer.ts +237 -0
- package/src/devcc/logs/merge.ts +87 -0
- package/src/devcc/processes/ManagedProcess.ts +195 -0
- package/src/devcc/processes/ProcessManager.ts +625 -0
- package/src/devcc/processes/graph.ts +98 -0
- package/src/devcc/projects/ProjectRegistry.ts +219 -0
- package/src/devcc/services/ServiceManager.ts +180 -0
- package/src/devcc/services/docker.ts +220 -0
- package/src/devcc/services/node.ts +137 -0
- package/src/devcc/services/ollama.ts +80 -0
- package/src/devcc/services/postgres.ts +57 -0
- package/src/devcc/services/redis.ts +47 -0
- package/src/devcc/services/types.ts +59 -0
- package/src/devcc/system/SystemMonitor.ts +207 -0
- package/src/devcc/utils/errors.ts +64 -0
- package/src/devcc/utils/exec.ts +165 -0
- package/src/devcc/utils/format.ts +83 -0
- package/src/devcc/utils/fuzzy.ts +44 -0
- package/src/devcc/utils/logger.ts +140 -0
- package/src/devcc/utils/packageManager.ts +112 -0
- package/src/devcc/utils/paths.ts +39 -0
- package/src/devcc/views/DependenciesView.ts +206 -0
- package/src/devcc/views/GitView.ts +259 -0
- package/src/devcc/views/LogsView.ts +300 -0
- package/src/devcc/views/OverviewView.ts +187 -0
- package/src/devcc/views/ProcessesView.ts +177 -0
- package/src/devcc/views/ScriptsView.ts +187 -0
- package/src/devcc/views/ServicesView.ts +188 -0
- package/src/devcc/views/SettingsView.ts +146 -0
- package/src/devcc/views/SystemView.ts +110 -0
- package/src/devcc/views/View.ts +63 -0
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
import { BoxRenderable, createCliRenderer } from "@opentui/core";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { ConfirmDialog, PromptDialog } from "../components/ConfirmDialog.js";
|
|
5
|
+
import { Footer } from "../components/Footer.js";
|
|
6
|
+
import { Header } from "../components/Header.js";
|
|
7
|
+
import { HelpOverlay } from "../components/HelpOverlay.js";
|
|
8
|
+
import { Sidebar } from "../components/Sidebar.js";
|
|
9
|
+
import { CommandPalette } from "../components/CommandPalette.js";
|
|
10
|
+
import { ProjectPicker } from "../components/ProjectPicker.js";
|
|
11
|
+
import { CommandRegistry } from "../core/commands.js";
|
|
12
|
+
import { loadConfig } from "../core/config.js";
|
|
13
|
+
import { Subscriptions } from "../core/events.js";
|
|
14
|
+
import { createInitialSelection, createInitialState, NARROW_WIDTH, setSelection, shallowArrayEqual, Store, updateUi, VIEW_IDS, } from "../core/state.js";
|
|
15
|
+
import { DependencyService, updateArgs } from "../deps/DependencyService.js";
|
|
16
|
+
import { GitService } from "../git/GitService.js";
|
|
17
|
+
import { buildProjectList, defaultSearchDirs, discoverProjects, readRecents, recordRecent, } from "../projects/ProjectRegistry.js";
|
|
18
|
+
import { LogStore, MAX_LOG_LINES } from "../logs/LogBuffer.js";
|
|
19
|
+
import { ALL_SOURCES } from "../logs/merge.js";
|
|
20
|
+
import { isActive } from "../processes/ManagedProcess.js";
|
|
21
|
+
import { ProcessManager } from "../processes/ProcessManager.js";
|
|
22
|
+
import { DockerService } from "../services/docker.js";
|
|
23
|
+
import { detectNodeServices, PortService } from "../services/node.js";
|
|
24
|
+
import { OllamaService } from "../services/ollama.js";
|
|
25
|
+
import { PostgresService } from "../services/postgres.js";
|
|
26
|
+
import { RedisService } from "../services/redis.js";
|
|
27
|
+
import { ServiceManager } from "../services/ServiceManager.js";
|
|
28
|
+
import { SystemMonitor } from "../system/SystemMonitor.js";
|
|
29
|
+
import { errorMessage, toDevccError } from "../utils/errors.js";
|
|
30
|
+
import { createLogger } from "../utils/logger.js";
|
|
31
|
+
import { detectPackageManager, readPackageJson, readScripts, runScriptArgs } from "../utils/packageManager.js";
|
|
32
|
+
import { fuzzyFilter } from "../utils/fuzzy.js";
|
|
33
|
+
import { findProjectRoot } from "../utils/paths.js";
|
|
34
|
+
import { DependenciesView } from "../views/DependenciesView.js";
|
|
35
|
+
import { GitView } from "../views/GitView.js";
|
|
36
|
+
import { LogsView } from "../views/LogsView.js";
|
|
37
|
+
import { OverviewView } from "../views/OverviewView.js";
|
|
38
|
+
import { ProcessesView } from "../views/ProcessesView.js";
|
|
39
|
+
import { ScriptsView, scriptProcessId } from "../views/ScriptsView.js";
|
|
40
|
+
import { ServicesView } from "../views/ServicesView.js";
|
|
41
|
+
import { SettingsView } from "../views/SettingsView.js";
|
|
42
|
+
import { SystemView } from "../views/SystemView.js";
|
|
43
|
+
import { createBuiltinCommands, createConfigCommands, createScriptCommands } from "./builtinCommands.js";
|
|
44
|
+
import { HELP_SECTIONS, VIEW_SHORTCUTS } from "./keymap.js";
|
|
45
|
+
import { theme } from "./theme.js";
|
|
46
|
+
const LOG_UPDATE_THROTTLE_MS = 80;
|
|
47
|
+
export class App {
|
|
48
|
+
options;
|
|
49
|
+
renderer;
|
|
50
|
+
/** Torn down only when the app exits. */
|
|
51
|
+
subscriptions = new Subscriptions();
|
|
52
|
+
/** App-level timers (clock, uptime tick). */
|
|
53
|
+
timers = [];
|
|
54
|
+
views = new Map();
|
|
55
|
+
store;
|
|
56
|
+
/** Every open project, keyed by root directory. All keep their processes alive. */
|
|
57
|
+
sessions = new Map();
|
|
58
|
+
/** The project the views are showing. */
|
|
59
|
+
session;
|
|
60
|
+
/** Stable across project switches; repopulated per session. */
|
|
61
|
+
commands = new CommandRegistry();
|
|
62
|
+
context;
|
|
63
|
+
// Session services are reached through getters so the ~60 call sites below
|
|
64
|
+
// keep working unchanged when the session is swapped out.
|
|
65
|
+
get logger() {
|
|
66
|
+
return this.session.logger;
|
|
67
|
+
}
|
|
68
|
+
get logs() {
|
|
69
|
+
return this.session.logs;
|
|
70
|
+
}
|
|
71
|
+
get processes() {
|
|
72
|
+
return this.session.processes;
|
|
73
|
+
}
|
|
74
|
+
get services() {
|
|
75
|
+
return this.session.services;
|
|
76
|
+
}
|
|
77
|
+
get git() {
|
|
78
|
+
return this.session.git;
|
|
79
|
+
}
|
|
80
|
+
get system() {
|
|
81
|
+
return this.session.system;
|
|
82
|
+
}
|
|
83
|
+
get deps() {
|
|
84
|
+
return this.session.deps;
|
|
85
|
+
}
|
|
86
|
+
get config() {
|
|
87
|
+
return this.session.config;
|
|
88
|
+
}
|
|
89
|
+
header;
|
|
90
|
+
footer;
|
|
91
|
+
sidebar;
|
|
92
|
+
contentBox;
|
|
93
|
+
bodyBox;
|
|
94
|
+
palette;
|
|
95
|
+
projectPicker;
|
|
96
|
+
confirmDialog;
|
|
97
|
+
promptDialog;
|
|
98
|
+
helpOverlay;
|
|
99
|
+
activeView = "overview";
|
|
100
|
+
viewDirty = false;
|
|
101
|
+
logUpdateTimer = null;
|
|
102
|
+
notificationSeq = 0;
|
|
103
|
+
switching = false;
|
|
104
|
+
shuttingDown = false;
|
|
105
|
+
exitResolver = null;
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.options = options;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Bootstrap services, build the UI and start background polling.
|
|
111
|
+
* Returns as soon as the first frame is ready; slower work continues behind it.
|
|
112
|
+
*/
|
|
113
|
+
async start() {
|
|
114
|
+
await this.bootstrap();
|
|
115
|
+
await this.mount();
|
|
116
|
+
this.startAppTimers();
|
|
117
|
+
this.startSessionBackground(this.session);
|
|
118
|
+
}
|
|
119
|
+
/** Boot the dashboard and resolve when the user quits. */
|
|
120
|
+
async run() {
|
|
121
|
+
await this.start();
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
this.exitResolver = resolve;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------
|
|
127
|
+
// Bootstrap
|
|
128
|
+
// ---------------------------------------------------------------------
|
|
129
|
+
async bootstrap() {
|
|
130
|
+
const rootDir = this.options.cwd ?? findProjectRoot();
|
|
131
|
+
this.session = await this.createSession(rootDir);
|
|
132
|
+
this.sessions.set(this.session.rootDir, this.session);
|
|
133
|
+
this.store = new Store(createInitialState(this.session.project, 80, 24));
|
|
134
|
+
this.store.patch({ scripts: { available: this.session.scripts, runs: {} } });
|
|
135
|
+
this.context = this.createCommandContext();
|
|
136
|
+
this.registerSessionCommands();
|
|
137
|
+
this.reportConfigIssues();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build every project-scoped service for `rootDir`.
|
|
141
|
+
*
|
|
142
|
+
* Nothing here touches the renderer or the store, which is what makes it
|
|
143
|
+
* reusable for switching projects at runtime.
|
|
144
|
+
*/
|
|
145
|
+
async createSession(rootDir) {
|
|
146
|
+
const packageJson = readPackageJson(rootDir);
|
|
147
|
+
const packageManager = detectPackageManager({
|
|
148
|
+
rootDir,
|
|
149
|
+
packageJson,
|
|
150
|
+
userAgent: process.env.npm_config_user_agent,
|
|
151
|
+
});
|
|
152
|
+
const loaded = await loadConfig(rootDir);
|
|
153
|
+
const config = loaded.config;
|
|
154
|
+
const logger = createLogger(rootDir, config.logLevel);
|
|
155
|
+
logger.log("info", "app", "opening project", { rootDir, packageManager: packageManager.manager });
|
|
156
|
+
const logs = new LogStore(config.maxLogLines ?? MAX_LOG_LINES);
|
|
157
|
+
const system = new SystemMonitor({ rootDir, intervalMs: config.refresh.system });
|
|
158
|
+
const processes = new ProcessManager({ logs, logger: logger.child("processes"), rootDir });
|
|
159
|
+
processes.registerAll(config.processes);
|
|
160
|
+
const services = new ServiceManager({
|
|
161
|
+
logger: logger.child("services"),
|
|
162
|
+
intervalMs: config.refresh.services,
|
|
163
|
+
});
|
|
164
|
+
services.registerAll([
|
|
165
|
+
new DockerService({ rootDir }),
|
|
166
|
+
new PostgresService(),
|
|
167
|
+
new RedisService(),
|
|
168
|
+
new OllamaService(),
|
|
169
|
+
...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,
|
|
177
|
+
})),
|
|
178
|
+
]);
|
|
179
|
+
const git = new GitService({ rootDir, logger: logger.child("git") });
|
|
180
|
+
const deps = new DependencyService({
|
|
181
|
+
rootDir,
|
|
182
|
+
manager: packageManager.manager,
|
|
183
|
+
logger: logger.child("deps"),
|
|
184
|
+
});
|
|
185
|
+
const project = {
|
|
186
|
+
name: config.name ?? (typeof packageJson?.name === "string" ? packageJson.name : basename(rootDir)),
|
|
187
|
+
rootDir,
|
|
188
|
+
packageManager,
|
|
189
|
+
configPath: loaded.filePath,
|
|
190
|
+
configIssues: loaded.issues,
|
|
191
|
+
host: system.getHostInfo(),
|
|
192
|
+
tools: { git: null, docker: null, packageManager: null },
|
|
193
|
+
};
|
|
194
|
+
const recentError = recordRecent(rootDir);
|
|
195
|
+
if (recentError)
|
|
196
|
+
logger.log("debug", "app", "could not record recent project", recentError);
|
|
197
|
+
return {
|
|
198
|
+
rootDir,
|
|
199
|
+
config,
|
|
200
|
+
logger,
|
|
201
|
+
logs,
|
|
202
|
+
processes,
|
|
203
|
+
services,
|
|
204
|
+
git,
|
|
205
|
+
system,
|
|
206
|
+
deps,
|
|
207
|
+
project,
|
|
208
|
+
scripts: readScripts(packageJson),
|
|
209
|
+
scriptRuns: {},
|
|
210
|
+
wiring: new Subscriptions(),
|
|
211
|
+
timers: [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Rebuild the command registry for the current session. */
|
|
215
|
+
registerSessionCommands() {
|
|
216
|
+
this.commands.clear();
|
|
217
|
+
this.commands.registerAll(createBuiltinCommands());
|
|
218
|
+
this.commands.registerAll(createScriptCommands(this.session.scripts));
|
|
219
|
+
this.commands.registerAll(createConfigCommands(this.config.commands, (entry) => this.runConfigCommand(entry)));
|
|
220
|
+
}
|
|
221
|
+
reportConfigIssues() {
|
|
222
|
+
for (const issue of this.session.project.configIssues) {
|
|
223
|
+
this.pushNotification("warn", `Config: ${issue}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------
|
|
227
|
+
// UI construction
|
|
228
|
+
// ---------------------------------------------------------------------
|
|
229
|
+
async mount() {
|
|
230
|
+
this.renderer =
|
|
231
|
+
this.options.renderer ??
|
|
232
|
+
(await createCliRenderer({
|
|
233
|
+
// Ctrl+C is handled by the app so processes get a chance to stop.
|
|
234
|
+
exitOnCtrlC: false,
|
|
235
|
+
targetFps: 30,
|
|
236
|
+
backgroundColor: theme.background,
|
|
237
|
+
}));
|
|
238
|
+
const root = new BoxRenderable(this.renderer, {
|
|
239
|
+
width: "100%",
|
|
240
|
+
height: "100%",
|
|
241
|
+
flexDirection: "column",
|
|
242
|
+
backgroundColor: theme.background,
|
|
243
|
+
});
|
|
244
|
+
this.renderer.root.add(root);
|
|
245
|
+
this.header = new Header(this.renderer);
|
|
246
|
+
this.footer = new Footer(this.renderer);
|
|
247
|
+
this.sidebar = new Sidebar(this.renderer);
|
|
248
|
+
this.bodyBox = new BoxRenderable(this.renderer, {
|
|
249
|
+
flexGrow: 1,
|
|
250
|
+
flexDirection: "row",
|
|
251
|
+
backgroundColor: "transparent",
|
|
252
|
+
});
|
|
253
|
+
this.contentBox = new BoxRenderable(this.renderer, {
|
|
254
|
+
flexGrow: 1,
|
|
255
|
+
flexDirection: "column",
|
|
256
|
+
backgroundColor: "transparent",
|
|
257
|
+
});
|
|
258
|
+
this.bodyBox.add(this.sidebar.box);
|
|
259
|
+
this.bodyBox.add(this.contentBox);
|
|
260
|
+
root.add(this.header.box);
|
|
261
|
+
root.add(this.bodyBox);
|
|
262
|
+
root.add(this.footer.box);
|
|
263
|
+
const deps = { ctx: this.renderer, app: this.context, commands: this.commands };
|
|
264
|
+
const views = [
|
|
265
|
+
new OverviewView(deps),
|
|
266
|
+
new ProcessesView(deps),
|
|
267
|
+
new ServicesView(deps),
|
|
268
|
+
new ScriptsView(deps),
|
|
269
|
+
new GitView(deps),
|
|
270
|
+
new LogsView(deps),
|
|
271
|
+
new DependenciesView(deps),
|
|
272
|
+
new SystemView(deps),
|
|
273
|
+
new SettingsView(deps),
|
|
274
|
+
];
|
|
275
|
+
for (const view of views) {
|
|
276
|
+
this.views.set(view.id, view);
|
|
277
|
+
this.contentBox.add(view.box);
|
|
278
|
+
}
|
|
279
|
+
this.palette = new CommandPalette(this.renderer);
|
|
280
|
+
this.projectPicker = new ProjectPicker(this.renderer);
|
|
281
|
+
this.confirmDialog = new ConfirmDialog(this.renderer);
|
|
282
|
+
this.promptDialog = new PromptDialog(this.renderer);
|
|
283
|
+
this.helpOverlay = new HelpOverlay(this.renderer, HELP_SECTIONS);
|
|
284
|
+
for (const overlay of this.overlays()) {
|
|
285
|
+
this.renderer.root.add(overlay.box);
|
|
286
|
+
}
|
|
287
|
+
this.handleResize(this.renderer.width, this.renderer.height);
|
|
288
|
+
this.setView("overview");
|
|
289
|
+
this.wireEvents();
|
|
290
|
+
this.renderAll();
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------------
|
|
293
|
+
// Event wiring
|
|
294
|
+
// ---------------------------------------------------------------------
|
|
295
|
+
wireEvents() {
|
|
296
|
+
// --- Renderer -------------------------------------------------------
|
|
297
|
+
const onResize = () => this.handleResize(this.renderer.width, this.renderer.height);
|
|
298
|
+
this.renderer.on("resize", onResize);
|
|
299
|
+
this.subscriptions.add(() => this.renderer.off("resize", onResize));
|
|
300
|
+
const onKey = (key) => this.handleKey(key);
|
|
301
|
+
this.renderer.keyInput.on("keypress", onKey);
|
|
302
|
+
this.subscriptions.add(() => this.renderer.keyInput.off("keypress", onKey));
|
|
303
|
+
this.wireSession(this.session);
|
|
304
|
+
this.wireStoreToUi();
|
|
305
|
+
// --- Signals ---------------------------------------------------------
|
|
306
|
+
const onSignal = (signal) => {
|
|
307
|
+
this.logger.log("info", "app", "received signal", { signal });
|
|
308
|
+
void this.quit();
|
|
309
|
+
};
|
|
310
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
311
|
+
process.on(signal, onSignal);
|
|
312
|
+
this.subscriptions.add(() => process.off(signal, onSignal));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Subscribe to one session's backends.
|
|
317
|
+
*
|
|
318
|
+
* Every open project is wired, so background projects keep tracking their
|
|
319
|
+
* processes and capturing logs. Only the *active* project mirrors its state
|
|
320
|
+
* into the store; the others surface nothing but notifications, prefixed
|
|
321
|
+
* with the project name so you know where a crash happened.
|
|
322
|
+
*/
|
|
323
|
+
wireSession(session) {
|
|
324
|
+
const { store } = this;
|
|
325
|
+
const { wiring, processes, services, git, system, logs, deps } = session;
|
|
326
|
+
const active = () => this.session === session;
|
|
327
|
+
const notify = (level, message, detail) => this.pushNotification(level, active() ? message : `[${session.project.name}] ${message}`, detail);
|
|
328
|
+
const syncProcesses = () => {
|
|
329
|
+
if (active())
|
|
330
|
+
store.patch({ processes: processes.list() });
|
|
331
|
+
this.refreshSidebarProjects();
|
|
332
|
+
};
|
|
333
|
+
processes.on("process:changed", syncProcesses);
|
|
334
|
+
processes.on("process:list", syncProcesses);
|
|
335
|
+
processes.on("process:exit", (process) => {
|
|
336
|
+
this.finishScriptRun(session, process.id, process.exitCode ?? 0);
|
|
337
|
+
syncProcesses();
|
|
338
|
+
});
|
|
339
|
+
processes.on("process:error", (error) => notify("error", error.message, error.detail));
|
|
340
|
+
processes.on("process:retry", (managed, delayMs) => {
|
|
341
|
+
notify("warn", `${managed.name} crashed; restarting in ${Math.round(delayMs / 100) / 10}s (attempt ${managed.crashRestarts})`);
|
|
342
|
+
syncProcesses();
|
|
343
|
+
});
|
|
344
|
+
processes.on("process:ready", (managed) => {
|
|
345
|
+
if (managed.readinessTimedOut)
|
|
346
|
+
notify("warn", `${managed.name}: readiness check timed out; reported as running`);
|
|
347
|
+
});
|
|
348
|
+
wiring.add(() => processes.removeAllListeners());
|
|
349
|
+
services.on("service:list", (list) => {
|
|
350
|
+
if (active())
|
|
351
|
+
store.patch({ services: list });
|
|
352
|
+
});
|
|
353
|
+
services.on("service:error", (error) => notify("error", error.message, error.detail));
|
|
354
|
+
wiring.add(() => services.removeAllListeners());
|
|
355
|
+
git.on("git:changed", (snapshot) => {
|
|
356
|
+
if (active())
|
|
357
|
+
store.patch({ git: snapshot });
|
|
358
|
+
});
|
|
359
|
+
git.on("git:error", (error) => notify("error", error.message, error.detail));
|
|
360
|
+
wiring.add(() => git.removeAllListeners());
|
|
361
|
+
system.on("system:metrics", (metrics) => {
|
|
362
|
+
if (active())
|
|
363
|
+
store.patch({ system: metrics });
|
|
364
|
+
});
|
|
365
|
+
system.on("system:tools", (tools) => {
|
|
366
|
+
session.project = { ...session.project, tools };
|
|
367
|
+
if (active())
|
|
368
|
+
store.setState((state) => ({ ...state, project: { ...state.project, tools } }));
|
|
369
|
+
});
|
|
370
|
+
wiring.add(() => system.removeAllListeners());
|
|
371
|
+
logs.on("log:sources", (sources) => {
|
|
372
|
+
if (!active())
|
|
373
|
+
return;
|
|
374
|
+
store.setState((state) => ({
|
|
375
|
+
...state,
|
|
376
|
+
logSources: sources,
|
|
377
|
+
ui: { ...state.ui, logSource: state.ui.logSource ?? sources[0]?.id ?? null },
|
|
378
|
+
}));
|
|
379
|
+
});
|
|
380
|
+
logs.on("log:append", (id) => {
|
|
381
|
+
if (!active() || this.activeView !== "logs")
|
|
382
|
+
return;
|
|
383
|
+
const current = store.getState().ui.logSource;
|
|
384
|
+
if (current === id || current === ALL_SOURCES)
|
|
385
|
+
this.scheduleLogUpdate();
|
|
386
|
+
});
|
|
387
|
+
logs.on("log:clear", () => {
|
|
388
|
+
if (active())
|
|
389
|
+
this.markViewDirty();
|
|
390
|
+
});
|
|
391
|
+
wiring.add(() => logs.removeAllListeners());
|
|
392
|
+
deps.on("deps:changed", (snapshot) => {
|
|
393
|
+
if (active())
|
|
394
|
+
store.patch({ dependencies: snapshot });
|
|
395
|
+
});
|
|
396
|
+
deps.on("deps:error", (error) => notify("error", error.message, error.detail));
|
|
397
|
+
wiring.add(() => deps.removeAllListeners());
|
|
398
|
+
}
|
|
399
|
+
/** Store -> UI watchers. App-level: they outlive any single project. */
|
|
400
|
+
wireStoreToUi() {
|
|
401
|
+
const { store } = this;
|
|
402
|
+
this.subscriptions.add(store.watch((state) => state.ui.view, (view) => this.setView(view)));
|
|
403
|
+
this.subscriptions.add(store.watch((state) => state.ui.clock, () => this.header.update(store.getState())));
|
|
404
|
+
this.subscriptions.add(store.watch((state) => state.ui.sidebarVisible, (visible) => {
|
|
405
|
+
this.sidebar.box.visible = visible;
|
|
406
|
+
}));
|
|
407
|
+
this.subscriptions.add(store.watch((state) => state.notifications[0], (notification) => this.showNotification(notification)));
|
|
408
|
+
// Domain slices that force the active view to re-render.
|
|
409
|
+
const markDirty = () => this.markViewDirty();
|
|
410
|
+
this.subscriptions.add(store.watch((state) => state.processes, markDirty, shallowArrayEqual));
|
|
411
|
+
this.subscriptions.add(store.watch((state) => state.services, markDirty, shallowArrayEqual));
|
|
412
|
+
this.subscriptions.add(store.watch((state) => state.git, markDirty));
|
|
413
|
+
this.subscriptions.add(store.watch((state) => state.dependencies, markDirty));
|
|
414
|
+
this.subscriptions.add(store.watch((state) => state.projects, markDirty, shallowArrayEqual));
|
|
415
|
+
this.subscriptions.add(store.watch((state) => state.system, markDirty));
|
|
416
|
+
this.subscriptions.add(store.watch((state) => state.scripts, markDirty));
|
|
417
|
+
this.subscriptions.add(store.watch((state) => state.logSources, markDirty, shallowArrayEqual));
|
|
418
|
+
this.subscriptions.add(store.watch((state) => state.notifications, markDirty, shallowArrayEqual));
|
|
419
|
+
this.subscriptions.add(store.watch((state) => state.ui.selection, markDirty));
|
|
420
|
+
this.subscriptions.add(store.watch((state) => state.ui.logSource, markDirty));
|
|
421
|
+
this.subscriptions.add(store.watch((state) => state.ui.logFollow, markDirty));
|
|
422
|
+
this.subscriptions.add(store.watch((state) => state.ui.width, markDirty));
|
|
423
|
+
this.subscriptions.add(store.watch((state) => state.ui.height, markDirty));
|
|
424
|
+
// Definitions registered during bootstrap predate these listeners, so seed
|
|
425
|
+
// the store with the current snapshots once.
|
|
426
|
+
store.patch({
|
|
427
|
+
processes: this.processes.list(),
|
|
428
|
+
services: this.services.list(),
|
|
429
|
+
logSources: this.logs.sources(),
|
|
430
|
+
});
|
|
431
|
+
store.setState((state) => ({
|
|
432
|
+
...state,
|
|
433
|
+
ui: { ...state.ui, logSource: state.ui.logSource ?? this.logs.sources()[0]?.id ?? null },
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Kick off background work after the first frame.
|
|
438
|
+
* Nothing here blocks the UI: the dashboard is usable while it runs.
|
|
439
|
+
*/
|
|
440
|
+
startAppTimers() {
|
|
441
|
+
const clockTimer = setInterval(() => updateUi(this.store, { clock: new Date() }), this.config.refresh.clock);
|
|
442
|
+
// Processes report their own state changes; this timer only refreshes the
|
|
443
|
+
// uptime column.
|
|
444
|
+
const uptimeTimer = setInterval(() => {
|
|
445
|
+
if (this.activeView === "processes" || this.activeView === "overview")
|
|
446
|
+
this.markViewDirty();
|
|
447
|
+
}, 1_000);
|
|
448
|
+
this.timers = [clockTimer, uptimeTimer];
|
|
449
|
+
for (const timer of this.timers)
|
|
450
|
+
timer.unref?.();
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Pollers for one project. Only the active project polls git, services and
|
|
454
|
+
* system metrics - background projects merely keep their processes alive.
|
|
455
|
+
*/
|
|
456
|
+
startSessionBackground(session) {
|
|
457
|
+
this.stopSessionBackground(session);
|
|
458
|
+
session.system.start();
|
|
459
|
+
const gitTimer = setInterval(() => void session.git.refresh(), session.config.refresh.git);
|
|
460
|
+
gitTimer.unref?.();
|
|
461
|
+
session.timers.push(gitTimer);
|
|
462
|
+
void session.git.refresh();
|
|
463
|
+
void session.services.detectAll().then(() => session.services.start());
|
|
464
|
+
void session.system.refreshTools(session.project.packageManager.manager);
|
|
465
|
+
if (!session.processes.list().some((entry) => entry.startedAt)) {
|
|
466
|
+
// Autostart only the first time a project is opened, never on reactivation.
|
|
467
|
+
void session.processes.startAutoStart();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
stopSessionBackground(session) {
|
|
471
|
+
for (const timer of session.timers)
|
|
472
|
+
clearInterval(timer);
|
|
473
|
+
session.timers = [];
|
|
474
|
+
session.system.stop();
|
|
475
|
+
session.services.stop();
|
|
476
|
+
}
|
|
477
|
+
// ---------------------------------------------------------------------
|
|
478
|
+
// Rendering
|
|
479
|
+
// ---------------------------------------------------------------------
|
|
480
|
+
renderAll() {
|
|
481
|
+
const state = this.store.getState();
|
|
482
|
+
this.header.update(state);
|
|
483
|
+
this.footer.setWidth(state.ui.width);
|
|
484
|
+
this.sidebar.setActive(state.ui.view);
|
|
485
|
+
this.updateFooterHints();
|
|
486
|
+
this.views.get(this.activeView)?.update(state);
|
|
487
|
+
}
|
|
488
|
+
markViewDirty() {
|
|
489
|
+
if (this.viewDirty)
|
|
490
|
+
return;
|
|
491
|
+
this.viewDirty = true;
|
|
492
|
+
queueMicrotask(() => {
|
|
493
|
+
this.viewDirty = false;
|
|
494
|
+
this.views.get(this.activeView)?.update(this.store.getState());
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/** Log output can arrive in bursts; coalesce redraws. */
|
|
498
|
+
scheduleLogUpdate() {
|
|
499
|
+
if (this.logUpdateTimer)
|
|
500
|
+
return;
|
|
501
|
+
this.logUpdateTimer = setTimeout(() => {
|
|
502
|
+
this.logUpdateTimer = null;
|
|
503
|
+
this.views.get(this.activeView)?.update(this.store.getState());
|
|
504
|
+
}, LOG_UPDATE_THROTTLE_MS);
|
|
505
|
+
this.logUpdateTimer.unref?.();
|
|
506
|
+
}
|
|
507
|
+
setView(view) {
|
|
508
|
+
if (this.views.size === 0)
|
|
509
|
+
return;
|
|
510
|
+
const previous = this.views.get(this.activeView);
|
|
511
|
+
if (previous && this.activeView !== view) {
|
|
512
|
+
previous.box.visible = false;
|
|
513
|
+
previous.onDeactivate();
|
|
514
|
+
}
|
|
515
|
+
this.activeView = view;
|
|
516
|
+
const next = this.views.get(view);
|
|
517
|
+
if (next) {
|
|
518
|
+
next.box.visible = true;
|
|
519
|
+
next.onActivate(this.store.getState());
|
|
520
|
+
next.update(this.store.getState());
|
|
521
|
+
}
|
|
522
|
+
this.sidebar.setActive(view);
|
|
523
|
+
this.updateFooterHints();
|
|
524
|
+
}
|
|
525
|
+
updateFooterHints() {
|
|
526
|
+
const view = this.views.get(this.activeView);
|
|
527
|
+
const hints = view?.hints() ?? [];
|
|
528
|
+
this.footer.setHints([...hints, { keys: "q", label: "quit" }]);
|
|
529
|
+
}
|
|
530
|
+
overlays() {
|
|
531
|
+
return [this.palette, this.projectPicker, this.confirmDialog, this.promptDialog, this.helpOverlay];
|
|
532
|
+
}
|
|
533
|
+
/** Stop every app-level timer and every session's pollers. */
|
|
534
|
+
stopBackground() {
|
|
535
|
+
for (const timer of this.timers)
|
|
536
|
+
clearInterval(timer);
|
|
537
|
+
this.timers = [];
|
|
538
|
+
if (this.logUpdateTimer) {
|
|
539
|
+
clearTimeout(this.logUpdateTimer);
|
|
540
|
+
this.logUpdateTimer = null;
|
|
541
|
+
}
|
|
542
|
+
for (const session of this.sessions.values())
|
|
543
|
+
this.stopSessionBackground(session);
|
|
544
|
+
}
|
|
545
|
+
handleResize(width, height) {
|
|
546
|
+
const sidebarVisible = width >= NARROW_WIDTH;
|
|
547
|
+
updateUi(this.store, { width, height, sidebarVisible });
|
|
548
|
+
// Applied directly as well as through the watcher: the first resize happens
|
|
549
|
+
// while the UI is still being assembled, before the watchers exist.
|
|
550
|
+
this.sidebar.box.visible = sidebarVisible;
|
|
551
|
+
this.footer.setWidth(width);
|
|
552
|
+
for (const overlay of this.overlays()) {
|
|
553
|
+
overlay.layout(width, height);
|
|
554
|
+
}
|
|
555
|
+
this.header.update(this.store.getState());
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------------
|
|
558
|
+
// Input
|
|
559
|
+
// ---------------------------------------------------------------------
|
|
560
|
+
handleKey(key) {
|
|
561
|
+
if (this.shuttingDown)
|
|
562
|
+
return;
|
|
563
|
+
// Ctrl+C always means "quit cleanly".
|
|
564
|
+
if (key.ctrl && key.name === "c") {
|
|
565
|
+
key.preventDefault();
|
|
566
|
+
key.stopPropagation();
|
|
567
|
+
void this.quit();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (this.confirmDialog.pending) {
|
|
571
|
+
this.handleConfirmKey(key);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (this.promptDialog.pending) {
|
|
575
|
+
this.handlePromptKey(key);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (this.projectPicker.visible) {
|
|
579
|
+
this.handleProjectPickerKey(key);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (this.palette.visible) {
|
|
583
|
+
this.handlePaletteKey(key);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (this.helpOverlay.visible) {
|
|
587
|
+
key.preventDefault();
|
|
588
|
+
if (key.name === "escape" || key.name === "q" || key.name === "?" || key.name === "f1")
|
|
589
|
+
this.closeOverlays();
|
|
590
|
+
else if (key.name === "j" || key.name === "down")
|
|
591
|
+
this.helpOverlay.scroll(1);
|
|
592
|
+
else if (key.name === "k" || key.name === "up")
|
|
593
|
+
this.helpOverlay.scroll(-1);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// A few bindings are global no matter which pane is focused. Shifted letters
|
|
597
|
+
// arrive as the lowercase name plus `shift`, so match on that.
|
|
598
|
+
// ctrl+k is the manual; the palette is `/`.
|
|
599
|
+
if (key.ctrl && key.name === "k") {
|
|
600
|
+
key.preventDefault();
|
|
601
|
+
this.openHelp();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (key.ctrl && key.name === "p") {
|
|
605
|
+
key.preventDefault();
|
|
606
|
+
this.openProjectPicker();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key.ctrl && key.name === "o") {
|
|
610
|
+
key.preventDefault();
|
|
611
|
+
this.cycleProject(key.shift ? -1 : 1);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (key.shift && key.name === "r") {
|
|
615
|
+
key.preventDefault();
|
|
616
|
+
void this.commands.run("app.refresh", this.context);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const state = this.store.getState();
|
|
620
|
+
// Views get first refusal so `s`/`r`/`d` mean the right thing per pane.
|
|
621
|
+
const view = this.views.get(this.activeView);
|
|
622
|
+
if (view?.handleKey(key, state)) {
|
|
623
|
+
key.preventDefault();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
switch (key.name) {
|
|
627
|
+
case "q":
|
|
628
|
+
key.preventDefault();
|
|
629
|
+
void this.quit();
|
|
630
|
+
return;
|
|
631
|
+
case "?":
|
|
632
|
+
case "f1":
|
|
633
|
+
key.preventDefault();
|
|
634
|
+
this.openHelp();
|
|
635
|
+
return;
|
|
636
|
+
case "/":
|
|
637
|
+
key.preventDefault();
|
|
638
|
+
this.openPalette();
|
|
639
|
+
return;
|
|
640
|
+
case "escape":
|
|
641
|
+
key.preventDefault();
|
|
642
|
+
this.closeOverlays();
|
|
643
|
+
return;
|
|
644
|
+
case "tab":
|
|
645
|
+
key.preventDefault();
|
|
646
|
+
this.cycleView(key.shift ? -1 : 1);
|
|
647
|
+
return;
|
|
648
|
+
case "left":
|
|
649
|
+
case "h":
|
|
650
|
+
key.preventDefault();
|
|
651
|
+
this.cycleView(-1);
|
|
652
|
+
return;
|
|
653
|
+
case "right":
|
|
654
|
+
key.preventDefault();
|
|
655
|
+
this.cycleView(1);
|
|
656
|
+
return;
|
|
657
|
+
case "up":
|
|
658
|
+
key.preventDefault();
|
|
659
|
+
this.moveSelection(-1);
|
|
660
|
+
return;
|
|
661
|
+
case "down":
|
|
662
|
+
key.preventDefault();
|
|
663
|
+
this.moveSelection(1);
|
|
664
|
+
return;
|
|
665
|
+
default:
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
const shortcutView = VIEW_SHORTCUTS[key.name];
|
|
669
|
+
if (shortcutView && !key.ctrl && !key.meta) {
|
|
670
|
+
key.preventDefault();
|
|
671
|
+
updateUi(this.store, { view: shortcutView });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
handleConfirmKey(key) {
|
|
675
|
+
key.preventDefault();
|
|
676
|
+
key.stopPropagation();
|
|
677
|
+
if (key.name === "y" || key.name === "return")
|
|
678
|
+
this.confirmDialog.resolve(true);
|
|
679
|
+
else if (key.name === "n" || key.name === "escape" || key.name === "q")
|
|
680
|
+
this.confirmDialog.resolve(false);
|
|
681
|
+
}
|
|
682
|
+
handlePromptKey(key) {
|
|
683
|
+
if (key.name === "escape") {
|
|
684
|
+
key.preventDefault();
|
|
685
|
+
key.stopPropagation();
|
|
686
|
+
this.promptDialog.resolve(null);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (key.name === "return") {
|
|
690
|
+
key.preventDefault();
|
|
691
|
+
key.stopPropagation();
|
|
692
|
+
this.promptDialog.submit();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
// Everything else belongs to the focused input.
|
|
696
|
+
}
|
|
697
|
+
handlePaletteKey(key) {
|
|
698
|
+
switch (key.name) {
|
|
699
|
+
case "escape":
|
|
700
|
+
key.preventDefault();
|
|
701
|
+
key.stopPropagation();
|
|
702
|
+
this.closeOverlays();
|
|
703
|
+
return;
|
|
704
|
+
case "return": {
|
|
705
|
+
key.preventDefault();
|
|
706
|
+
key.stopPropagation();
|
|
707
|
+
const selected = this.palette.selected;
|
|
708
|
+
this.closeOverlays();
|
|
709
|
+
if (selected)
|
|
710
|
+
void this.runCommand(selected.id);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
case "up":
|
|
714
|
+
key.preventDefault();
|
|
715
|
+
key.stopPropagation();
|
|
716
|
+
this.palette.move(-1);
|
|
717
|
+
return;
|
|
718
|
+
case "down":
|
|
719
|
+
key.preventDefault();
|
|
720
|
+
key.stopPropagation();
|
|
721
|
+
this.palette.move(1);
|
|
722
|
+
return;
|
|
723
|
+
default:
|
|
724
|
+
if (key.ctrl && (key.name === "n" || key.name === "p")) {
|
|
725
|
+
key.preventDefault();
|
|
726
|
+
key.stopPropagation();
|
|
727
|
+
this.palette.move(key.name === "n" ? 1 : -1);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// Let the input consume the key, then re-filter.
|
|
731
|
+
setTimeout(() => this.refreshPalette(), 0);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
handleProjectPickerKey(key) {
|
|
735
|
+
switch (key.name) {
|
|
736
|
+
case "escape":
|
|
737
|
+
key.preventDefault();
|
|
738
|
+
key.stopPropagation();
|
|
739
|
+
this.closeOverlays();
|
|
740
|
+
return;
|
|
741
|
+
case "return": {
|
|
742
|
+
key.preventDefault();
|
|
743
|
+
key.stopPropagation();
|
|
744
|
+
const selected = this.projectPicker.selected;
|
|
745
|
+
this.closeOverlays();
|
|
746
|
+
if (selected)
|
|
747
|
+
void this.openProject(selected.path);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
case "up":
|
|
751
|
+
key.preventDefault();
|
|
752
|
+
key.stopPropagation();
|
|
753
|
+
this.projectPicker.move(-1);
|
|
754
|
+
return;
|
|
755
|
+
case "down":
|
|
756
|
+
key.preventDefault();
|
|
757
|
+
key.stopPropagation();
|
|
758
|
+
this.projectPicker.move(1);
|
|
759
|
+
return;
|
|
760
|
+
default:
|
|
761
|
+
if (key.ctrl && (key.name === "n" || key.name === "p")) {
|
|
762
|
+
key.preventDefault();
|
|
763
|
+
key.stopPropagation();
|
|
764
|
+
this.projectPicker.move(key.name === "n" ? 1 : -1);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
setTimeout(() => this.refreshProjectPicker(), 0);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
cycleView(direction) {
|
|
771
|
+
const index = VIEW_IDS.indexOf(this.store.getState().ui.view);
|
|
772
|
+
const next = (index + direction + VIEW_IDS.length) % VIEW_IDS.length;
|
|
773
|
+
const view = VIEW_IDS[next];
|
|
774
|
+
if (view)
|
|
775
|
+
updateUi(this.store, { view });
|
|
776
|
+
}
|
|
777
|
+
moveSelection(delta) {
|
|
778
|
+
const state = this.store.getState();
|
|
779
|
+
const current = state.ui.selection[state.ui.view];
|
|
780
|
+
setSelection(this.store, state.ui.view, Math.max(0, current + delta));
|
|
781
|
+
}
|
|
782
|
+
// ---------------------------------------------------------------------
|
|
783
|
+
// Overlays
|
|
784
|
+
// ---------------------------------------------------------------------
|
|
785
|
+
openPalette() {
|
|
786
|
+
updateUi(this.store, { overlay: "palette" });
|
|
787
|
+
this.palette.open();
|
|
788
|
+
this.refreshPalette();
|
|
789
|
+
}
|
|
790
|
+
/** Scanning the filesystem is quick but not free, so do it on open. */
|
|
791
|
+
openProjectPicker() {
|
|
792
|
+
const projects = this.listProjects();
|
|
793
|
+
this.store.patch({ projects });
|
|
794
|
+
updateUi(this.store, { overlay: "projects" });
|
|
795
|
+
this.projectPicker.open(this.session.rootDir);
|
|
796
|
+
this.refreshProjectPicker();
|
|
797
|
+
}
|
|
798
|
+
refreshProjectPicker() {
|
|
799
|
+
if (!this.projectPicker.visible)
|
|
800
|
+
return;
|
|
801
|
+
const projects = this.store.getState().projects;
|
|
802
|
+
const query = this.projectPicker.query;
|
|
803
|
+
this.projectPicker.setResults(query.trim() === ""
|
|
804
|
+
? projects
|
|
805
|
+
: fuzzyFilter(projects, query, (entry) => `${entry.name} ${entry.path}`).map((m) => m.item));
|
|
806
|
+
}
|
|
807
|
+
openHelp() {
|
|
808
|
+
updateUi(this.store, { overlay: "help" });
|
|
809
|
+
this.helpOverlay.show();
|
|
810
|
+
}
|
|
811
|
+
refreshPalette() {
|
|
812
|
+
if (!this.palette.visible)
|
|
813
|
+
return;
|
|
814
|
+
this.palette.setResults(this.commands.search(this.palette.query, this.context));
|
|
815
|
+
}
|
|
816
|
+
closeOverlays() {
|
|
817
|
+
this.palette.close();
|
|
818
|
+
this.projectPicker.close();
|
|
819
|
+
this.helpOverlay.hide();
|
|
820
|
+
updateUi(this.store, { overlay: "none" });
|
|
821
|
+
}
|
|
822
|
+
async runCommand(id) {
|
|
823
|
+
try {
|
|
824
|
+
await this.commands.run(id, this.context);
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
const devccError = toDevccError(error, "command", `Command "${id}" failed`);
|
|
828
|
+
this.pushNotification("error", devccError.message, devccError.detail);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// ---------------------------------------------------------------------
|
|
832
|
+
// Notifications
|
|
833
|
+
// ---------------------------------------------------------------------
|
|
834
|
+
pushNotification(level, message, detail) {
|
|
835
|
+
const notification = {
|
|
836
|
+
id: (this.notificationSeq += 1),
|
|
837
|
+
level,
|
|
838
|
+
message,
|
|
839
|
+
detail,
|
|
840
|
+
createdAt: new Date(),
|
|
841
|
+
};
|
|
842
|
+
this.logger.log(level === "error" ? "error" : level === "warn" ? "warn" : "info", "ui", message, detail);
|
|
843
|
+
this.store.setState((state) => ({
|
|
844
|
+
...state,
|
|
845
|
+
notifications: [notification, ...state.notifications].slice(0, 200),
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
showNotification(notification) {
|
|
849
|
+
if (!notification) {
|
|
850
|
+
this.footer.setStatus(null);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const prefix = notification.level === "error" || notification.level === "warn" ? "⚠ " : "";
|
|
854
|
+
this.footer.setStatus(`${prefix}${notification.message}`, notification.level === "error" ? "error" : notification.level === "success" ? "success" : "info");
|
|
855
|
+
}
|
|
856
|
+
// ---------------------------------------------------------------------
|
|
857
|
+
// Command context
|
|
858
|
+
// ---------------------------------------------------------------------
|
|
859
|
+
/**
|
|
860
|
+
* The context handed to commands and views.
|
|
861
|
+
*
|
|
862
|
+
* Built once and kept stable: session services are read through getters, so a
|
|
863
|
+
* project switch swaps the backends underneath every existing view without
|
|
864
|
+
* rebuilding the UI.
|
|
865
|
+
*/
|
|
866
|
+
createCommandContext() {
|
|
867
|
+
const context = {
|
|
868
|
+
store: this.store,
|
|
869
|
+
processes: this.processes,
|
|
870
|
+
services: this.services,
|
|
871
|
+
git: this.git,
|
|
872
|
+
logs: this.logs,
|
|
873
|
+
system: this.system,
|
|
874
|
+
deps: this.deps,
|
|
875
|
+
navigate: (view) => updateUi(this.store, { view }),
|
|
876
|
+
notify: (level, message, detail) => this.pushNotification(level, message, detail),
|
|
877
|
+
confirm: (title, message) => this.askConfirm(title, message),
|
|
878
|
+
prompt: (title, placeholder) => this.askPrompt(title, placeholder),
|
|
879
|
+
showHelp: () => this.openHelp(),
|
|
880
|
+
runScript: (name) => this.runScript(name),
|
|
881
|
+
updateDependency: (name, version) => this.updateDependency(name, version),
|
|
882
|
+
listProjects: () => this.listProjects(),
|
|
883
|
+
openProjects: () => this.openProjects(),
|
|
884
|
+
switchProject: (path) => this.openProject(path),
|
|
885
|
+
closeProject: (path) => this.closeProject(path),
|
|
886
|
+
quit: () => void this.quit(),
|
|
887
|
+
};
|
|
888
|
+
// Session services are swapped out on a project switch, so expose them as
|
|
889
|
+
// getters rather than snapshots taken at construction time.
|
|
890
|
+
Object.defineProperties(context, {
|
|
891
|
+
processes: { get: () => this.processes, enumerable: true },
|
|
892
|
+
services: { get: () => this.services, enumerable: true },
|
|
893
|
+
git: { get: () => this.git, enumerable: true },
|
|
894
|
+
logs: { get: () => this.logs, enumerable: true },
|
|
895
|
+
system: { get: () => this.system, enumerable: true },
|
|
896
|
+
deps: { get: () => this.deps, enumerable: true },
|
|
897
|
+
});
|
|
898
|
+
return context;
|
|
899
|
+
}
|
|
900
|
+
async askConfirm(title, message) {
|
|
901
|
+
updateUi(this.store, { overlay: "confirm" });
|
|
902
|
+
const answer = await this.confirmDialog.ask(title, message);
|
|
903
|
+
updateUi(this.store, { overlay: "none" });
|
|
904
|
+
return answer;
|
|
905
|
+
}
|
|
906
|
+
async askPrompt(title, placeholder) {
|
|
907
|
+
updateUi(this.store, { overlay: "prompt" });
|
|
908
|
+
const answer = await this.promptDialog.ask(title, placeholder);
|
|
909
|
+
updateUi(this.store, { overlay: "none" });
|
|
910
|
+
return answer;
|
|
911
|
+
}
|
|
912
|
+
// ---------------------------------------------------------------------
|
|
913
|
+
// Scripts and config commands
|
|
914
|
+
// ---------------------------------------------------------------------
|
|
915
|
+
/** Run a package.json script as a managed one-shot process. */
|
|
916
|
+
async runScript(name) {
|
|
917
|
+
const state = this.store.getState();
|
|
918
|
+
const script = state.scripts.available.find((entry) => entry.name === name);
|
|
919
|
+
if (!script) {
|
|
920
|
+
this.pushNotification("warn", `No script named "${name}"`);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const id = scriptProcessId(name);
|
|
924
|
+
const manager = state.project.packageManager.manager;
|
|
925
|
+
if (this.processes.isRunning(id)) {
|
|
926
|
+
this.pushNotification("warn", `${name} is already running`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.processes.register({
|
|
930
|
+
id,
|
|
931
|
+
name: `${manager} run ${name}`,
|
|
932
|
+
command: manager,
|
|
933
|
+
args: runScriptArgs(manager, name),
|
|
934
|
+
cwd: state.project.rootDir,
|
|
935
|
+
kind: "task",
|
|
936
|
+
});
|
|
937
|
+
const session = this.session;
|
|
938
|
+
session.scriptRuns = {
|
|
939
|
+
...session.scriptRuns,
|
|
940
|
+
[name]: { scriptName: name, processId: id, startedAt: new Date(), status: "running" },
|
|
941
|
+
};
|
|
942
|
+
this.store.setState((current) => ({
|
|
943
|
+
...current,
|
|
944
|
+
scripts: { ...current.scripts, runs: session.scriptRuns },
|
|
945
|
+
ui: { ...current.ui, logSource: id },
|
|
946
|
+
}));
|
|
947
|
+
try {
|
|
948
|
+
await this.processes.start(id);
|
|
949
|
+
this.pushNotification("info", `Running ${manager} run ${name}`);
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
this.finishScriptRun(session, id, 1);
|
|
953
|
+
this.pushNotification("error", errorMessage(error));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
finishScriptRun(session, processId, exitCode) {
|
|
957
|
+
const entry = Object.values(session.scriptRuns).find((run) => run.processId === processId);
|
|
958
|
+
if (!entry || entry.status !== "running")
|
|
959
|
+
return;
|
|
960
|
+
session.scriptRuns = {
|
|
961
|
+
...session.scriptRuns,
|
|
962
|
+
[entry.scriptName]: {
|
|
963
|
+
...entry,
|
|
964
|
+
finishedAt: new Date(),
|
|
965
|
+
exitCode,
|
|
966
|
+
status: exitCode === 0 ? "succeeded" : "failed",
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
if (session === this.session) {
|
|
970
|
+
this.store.setState((state) => ({ ...state, scripts: { ...state.scripts, runs: session.scriptRuns } }));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
async runConfigCommand(entry) {
|
|
974
|
+
const id = `command:${entry.id}`;
|
|
975
|
+
if (this.processes.isRunning(id)) {
|
|
976
|
+
this.pushNotification("warn", `${entry.title} is already running`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
this.processes.register({
|
|
980
|
+
id,
|
|
981
|
+
name: entry.title,
|
|
982
|
+
command: entry.command,
|
|
983
|
+
args: entry.args,
|
|
984
|
+
cwd: entry.cwd,
|
|
985
|
+
kind: "task",
|
|
986
|
+
});
|
|
987
|
+
updateUi(this.store, { logSource: id });
|
|
988
|
+
try {
|
|
989
|
+
await this.processes.start(id);
|
|
990
|
+
this.pushNotification("info", `Running ${entry.title}`);
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
this.pushNotification("error", errorMessage(error));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// ---------------------------------------------------------------------
|
|
997
|
+
// Projects
|
|
998
|
+
// ---------------------------------------------------------------------
|
|
999
|
+
/** Current project first, then recents, config entries and sibling directories. */
|
|
1000
|
+
listProjects() {
|
|
1001
|
+
const current = this.session.rootDir;
|
|
1002
|
+
const searchDirs = [
|
|
1003
|
+
...this.config.projectSearchDirs.map((dir) => resolve(current, dir)),
|
|
1004
|
+
...defaultSearchDirs(current),
|
|
1005
|
+
];
|
|
1006
|
+
return buildProjectList({
|
|
1007
|
+
currentPath: current,
|
|
1008
|
+
configured: this.config.projects,
|
|
1009
|
+
recents: [...this.openProjects().filter((entry) => entry.path !== current), ...readRecents()],
|
|
1010
|
+
discovered: discoverProjects({ searchDirs }),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/** Open projects in the order they were opened. */
|
|
1014
|
+
openProjects() {
|
|
1015
|
+
return [...this.sessions.values()].map((session) => ({
|
|
1016
|
+
path: session.rootDir,
|
|
1017
|
+
name: session.project.name,
|
|
1018
|
+
source: session === this.session ? "current" : "recent",
|
|
1019
|
+
hasGit: session.git.getSnapshot().isRepository,
|
|
1020
|
+
hasPackageJson: session.scripts.length > 0 || existsSync(join(session.rootDir, "package.json")),
|
|
1021
|
+
}));
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Open a project, or bring an already-open one to the front.
|
|
1025
|
+
*
|
|
1026
|
+
* Opening never stops anything: the previous project keeps running in the
|
|
1027
|
+
* background with its processes and log capture intact. The renderer, views
|
|
1028
|
+
* and store are shared; only the session behind them changes.
|
|
1029
|
+
*/
|
|
1030
|
+
async openProject(path) {
|
|
1031
|
+
const target = resolve(path);
|
|
1032
|
+
if (this.switching || this.shuttingDown)
|
|
1033
|
+
return;
|
|
1034
|
+
if (target === this.session.rootDir) {
|
|
1035
|
+
this.pushNotification("info", "Already on that project");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const existing = this.sessions.get(target);
|
|
1039
|
+
if (existing) {
|
|
1040
|
+
this.activateSession(existing);
|
|
1041
|
+
this.pushNotification("info", `Switched to ${existing.project.name}`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (!existsSync(target)) {
|
|
1045
|
+
this.pushNotification("error", `No such directory: ${target}`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.switching = true;
|
|
1049
|
+
try {
|
|
1050
|
+
this.footer.setStatus(`Opening ${basename(target)}…`);
|
|
1051
|
+
const session = await this.createSession(target);
|
|
1052
|
+
this.sessions.set(target, session);
|
|
1053
|
+
this.wireSession(session);
|
|
1054
|
+
this.activateSession(session);
|
|
1055
|
+
this.reportConfigIssues();
|
|
1056
|
+
this.pushNotification("success", `Opened ${session.project.name} (${this.sessions.size} open)`);
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
this.pushNotification("error", `Could not open project: ${errorMessage(error)}`);
|
|
1060
|
+
}
|
|
1061
|
+
finally {
|
|
1062
|
+
this.switching = false;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/** Point the views at `session` and mirror its current state into the store. */
|
|
1066
|
+
activateSession(session) {
|
|
1067
|
+
const previous = this.session;
|
|
1068
|
+
if (previous !== session) {
|
|
1069
|
+
this.stopSessionBackground(previous);
|
|
1070
|
+
previous.logger.log("info", "app", "deactivated", { next: session.project.name });
|
|
1071
|
+
}
|
|
1072
|
+
this.session = session;
|
|
1073
|
+
this.registerSessionCommands();
|
|
1074
|
+
// Everything the store holds for a project is derivable from the session,
|
|
1075
|
+
// so switching is a snapshot copy rather than a rebuild.
|
|
1076
|
+
this.store.setState((state) => ({
|
|
1077
|
+
...state,
|
|
1078
|
+
project: session.project,
|
|
1079
|
+
processes: session.processes.list(),
|
|
1080
|
+
services: session.services.list(),
|
|
1081
|
+
scripts: { available: session.scripts, runs: session.scriptRuns },
|
|
1082
|
+
git: session.git.getSnapshot(),
|
|
1083
|
+
dependencies: session.deps.getSnapshot(),
|
|
1084
|
+
system: session.system.getMetrics(),
|
|
1085
|
+
logSources: session.logs.sources(),
|
|
1086
|
+
projects: [],
|
|
1087
|
+
ui: {
|
|
1088
|
+
...state.ui,
|
|
1089
|
+
selection: createInitialSelection(),
|
|
1090
|
+
logSource: session.logs.sources()[0]?.id ?? null,
|
|
1091
|
+
logFollow: true,
|
|
1092
|
+
overlay: "none",
|
|
1093
|
+
},
|
|
1094
|
+
}));
|
|
1095
|
+
this.startSessionBackground(session);
|
|
1096
|
+
this.refreshSidebarProjects();
|
|
1097
|
+
this.renderAll();
|
|
1098
|
+
session.logger.log("info", "app", "activated", { openProjects: this.sessions.size });
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Close a project, stopping anything devcc started there (confirmed).
|
|
1102
|
+
* The last open project cannot be closed - quit instead.
|
|
1103
|
+
*/
|
|
1104
|
+
async closeProject(path) {
|
|
1105
|
+
const target = resolve(path ?? this.session.rootDir);
|
|
1106
|
+
const session = this.sessions.get(target);
|
|
1107
|
+
if (!session) {
|
|
1108
|
+
this.pushNotification("warn", "That project is not open");
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (this.sessions.size === 1) {
|
|
1112
|
+
this.pushNotification("warn", "Cannot close the only open project; press q to quit");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const running = session.processes.list().filter((entry) => isActive(entry.status));
|
|
1116
|
+
if (running.length > 0) {
|
|
1117
|
+
const confirmed = await this.askConfirm(`Close ${session.project.name} and stop ${running.length} running process(es)?`, `${running.map((entry) => entry.name).join(", ")} will be stopped.`);
|
|
1118
|
+
if (!confirmed)
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
this.footer.setStatus(`Closing ${session.project.name}…`);
|
|
1122
|
+
this.stopSessionBackground(session);
|
|
1123
|
+
await session.processes.stopAll();
|
|
1124
|
+
session.wiring.dispose();
|
|
1125
|
+
this.sessions.delete(target);
|
|
1126
|
+
session.logger.log("info", "app", "closed project");
|
|
1127
|
+
if (session === this.session) {
|
|
1128
|
+
const next = [...this.sessions.values()][0];
|
|
1129
|
+
if (next)
|
|
1130
|
+
this.activateSession(next);
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
this.refreshSidebarProjects();
|
|
1134
|
+
}
|
|
1135
|
+
this.pushNotification("info", `Closed ${session.project.name}`);
|
|
1136
|
+
}
|
|
1137
|
+
/** `ctrl+o`: bring the next open project to the front. */
|
|
1138
|
+
cycleProject(direction) {
|
|
1139
|
+
const sessions = [...this.sessions.values()];
|
|
1140
|
+
if (sessions.length < 2) {
|
|
1141
|
+
this.pushNotification("info", "Only one project is open; ctrl+p opens another");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const index = sessions.indexOf(this.session);
|
|
1145
|
+
const next = sessions[(index + direction + sessions.length) % sessions.length];
|
|
1146
|
+
if (next)
|
|
1147
|
+
this.activateSession(next);
|
|
1148
|
+
}
|
|
1149
|
+
/** Feed the rail the per-project running counts. */
|
|
1150
|
+
refreshSidebarProjects() {
|
|
1151
|
+
this.sidebar.setProjects([...this.sessions.values()].map((session) => {
|
|
1152
|
+
const list = session.processes.list();
|
|
1153
|
+
return {
|
|
1154
|
+
path: session.rootDir,
|
|
1155
|
+
name: session.project.name,
|
|
1156
|
+
running: list.filter((entry) => isActive(entry.status)).length,
|
|
1157
|
+
attention: list.some((entry) => entry.status === "failed"),
|
|
1158
|
+
};
|
|
1159
|
+
}), this.session.rootDir);
|
|
1160
|
+
}
|
|
1161
|
+
// ---------------------------------------------------------------------
|
|
1162
|
+
// Dependencies
|
|
1163
|
+
// ---------------------------------------------------------------------
|
|
1164
|
+
/** Update a single package, gated by a confirmation (it rewrites the lockfile). */
|
|
1165
|
+
async updateDependency(name, version) {
|
|
1166
|
+
const manager = this.session.project.packageManager.manager;
|
|
1167
|
+
const args = updateArgs(manager, name);
|
|
1168
|
+
const confirmed = await this.askConfirm(`Update ${name} to ${version}?`, `Runs \`${manager} ${args.join(" ")}\`, which rewrites package.json and the lockfile.`);
|
|
1169
|
+
if (!confirmed)
|
|
1170
|
+
return;
|
|
1171
|
+
const id = `deps:${name}`;
|
|
1172
|
+
if (this.processes.isRunning(id)) {
|
|
1173
|
+
this.pushNotification("warn", `${name} is already being updated`);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this.processes.register({
|
|
1177
|
+
id,
|
|
1178
|
+
name: `${manager} update ${name}`,
|
|
1179
|
+
command: manager,
|
|
1180
|
+
args,
|
|
1181
|
+
cwd: this.session.rootDir,
|
|
1182
|
+
kind: "task",
|
|
1183
|
+
});
|
|
1184
|
+
updateUi(this.store, { logSource: id });
|
|
1185
|
+
try {
|
|
1186
|
+
await this.processes.start(id);
|
|
1187
|
+
this.pushNotification("info", `Updating ${name}…`);
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
this.pushNotification("error", errorMessage(error));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// ---------------------------------------------------------------------
|
|
1194
|
+
// Shutdown
|
|
1195
|
+
// ---------------------------------------------------------------------
|
|
1196
|
+
/**
|
|
1197
|
+
* Graceful shutdown: stop timers and listeners, stop the processes devcc
|
|
1198
|
+
* launched (never anything merely detected), then hand the terminal back.
|
|
1199
|
+
*/
|
|
1200
|
+
async quit() {
|
|
1201
|
+
if (this.shuttingDown)
|
|
1202
|
+
return;
|
|
1203
|
+
this.shuttingDown = true;
|
|
1204
|
+
this.logger.log("info", "app", "shutting down");
|
|
1205
|
+
this.footer.setStatus("Shutting down…");
|
|
1206
|
+
this.stopBackground();
|
|
1207
|
+
// Every open project, not just the visible one.
|
|
1208
|
+
await Promise.all([...this.sessions.values()].map(async (session) => {
|
|
1209
|
+
try {
|
|
1210
|
+
await session.processes.stopAll();
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
session.logger.log("error", "app", "failed to stop processes", errorMessage(error));
|
|
1214
|
+
}
|
|
1215
|
+
session.wiring.dispose();
|
|
1216
|
+
}));
|
|
1217
|
+
this.subscriptions.dispose();
|
|
1218
|
+
for (const view of this.views.values())
|
|
1219
|
+
view.dispose();
|
|
1220
|
+
this.views.clear();
|
|
1221
|
+
try {
|
|
1222
|
+
this.renderer.destroy();
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
this.logger.log("error", "app", "renderer teardown failed", errorMessage(error));
|
|
1226
|
+
}
|
|
1227
|
+
this.exitResolver?.();
|
|
1228
|
+
this.exitResolver = null;
|
|
1229
|
+
}
|
|
1230
|
+
/** Test hook: the store the UI is bound to. */
|
|
1231
|
+
getStore() {
|
|
1232
|
+
return this.store;
|
|
1233
|
+
}
|
|
1234
|
+
/** Test hook: open/activate a project without driving the picker overlay. */
|
|
1235
|
+
async switchProjectForTest(path) {
|
|
1236
|
+
await this.openProject(path);
|
|
1237
|
+
}
|
|
1238
|
+
/** Test hook: close a project without the confirmation dialog when nothing runs. */
|
|
1239
|
+
async closeProjectForTest(path) {
|
|
1240
|
+
await this.closeProject(path);
|
|
1241
|
+
}
|
|
1242
|
+
/** Test hook: dependency-ordered start of the active project's processes. */
|
|
1243
|
+
async startAllForTest(ids) {
|
|
1244
|
+
await this.processes.startAll(ids);
|
|
1245
|
+
}
|
|
1246
|
+
/** Test hook: root directories of every open project. */
|
|
1247
|
+
openProjectPaths() {
|
|
1248
|
+
return [...this.sessions.keys()];
|
|
1249
|
+
}
|
|
1250
|
+
/** Test hook: the log store backing the Logs view. */
|
|
1251
|
+
getLogs() {
|
|
1252
|
+
return this.logs;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
//# sourceMappingURL=App.js.map
|