@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,98 @@
|
|
|
1
|
+
import type { ProcessDefinition } from "./ManagedProcess.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dependency ordering for `dependsOn`.
|
|
5
|
+
*
|
|
6
|
+
* Pure so the ordering rules can be tested without spawning anything: the
|
|
7
|
+
* manager only has to execute the waves this produces.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface StartPlan {
|
|
11
|
+
/**
|
|
12
|
+
* Processes to start, grouped into waves. Everything in a wave can start
|
|
13
|
+
* concurrently; wave N+1 waits for wave N to become ready.
|
|
14
|
+
*/
|
|
15
|
+
waves: string[][]
|
|
16
|
+
/** Edges pointing at ids that were never defined. */
|
|
17
|
+
missing: { id: string; dependsOn: string }[]
|
|
18
|
+
/** Ids that form a dependency cycle, if any. Their waves are omitted. */
|
|
19
|
+
cycle: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Group ids into dependency waves (Kahn's algorithm).
|
|
24
|
+
*
|
|
25
|
+
* Unknown dependencies are reported and then ignored, so one typo cannot stop
|
|
26
|
+
* the rest of the project from starting. A cycle is reported rather than
|
|
27
|
+
* guessed at - devcc refuses to pick an arbitrary order.
|
|
28
|
+
*/
|
|
29
|
+
export function planStartOrder(
|
|
30
|
+
definitions: readonly ProcessDefinition[],
|
|
31
|
+
only?: readonly string[],
|
|
32
|
+
): StartPlan {
|
|
33
|
+
const byId = new Map(definitions.map((definition) => [definition.id, definition]))
|
|
34
|
+
const selected = new Set(only ?? definitions.map((definition) => definition.id))
|
|
35
|
+
|
|
36
|
+
// Only consider selected ids that actually exist.
|
|
37
|
+
const ids = [...selected].filter((id) => byId.has(id))
|
|
38
|
+
|
|
39
|
+
const missing: { id: string; dependsOn: string }[] = []
|
|
40
|
+
const dependencies = new Map<string, Set<string>>()
|
|
41
|
+
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const declared = byId.get(id)?.dependsOn ?? []
|
|
44
|
+
const resolved = new Set<string>()
|
|
45
|
+
for (const dependency of declared) {
|
|
46
|
+
if (!byId.has(dependency)) {
|
|
47
|
+
missing.push({ id, dependsOn: dependency })
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
// A dependency outside the selection is still a real edge, but it cannot
|
|
51
|
+
// gate a start we were not asked to perform.
|
|
52
|
+
if (ids.includes(dependency)) resolved.add(dependency)
|
|
53
|
+
}
|
|
54
|
+
dependencies.set(id, resolved)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const waves: string[][] = []
|
|
58
|
+
const placed = new Set<string>()
|
|
59
|
+
|
|
60
|
+
while (placed.size < ids.length) {
|
|
61
|
+
const wave = ids
|
|
62
|
+
.filter((id) => !placed.has(id))
|
|
63
|
+
.filter((id) => [...(dependencies.get(id) ?? [])].every((dependency) => placed.has(dependency)))
|
|
64
|
+
|
|
65
|
+
if (wave.length === 0) {
|
|
66
|
+
// Everything left depends on something unplaced: that is a cycle.
|
|
67
|
+
return { waves, missing, cycle: ids.filter((id) => !placed.has(id)).sort() }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const id of wave) placed.add(id)
|
|
71
|
+
waves.push(wave)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { waves, missing, cycle: [] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ids that must be stopped before `id` can be, i.e. everything depending on it,
|
|
79
|
+
* transitively. Used to stop a stack from the top down.
|
|
80
|
+
*/
|
|
81
|
+
export function dependentsOf(definitions: readonly ProcessDefinition[], id: string): string[] {
|
|
82
|
+
const direct = new Map<string, string[]>()
|
|
83
|
+
for (const definition of definitions) {
|
|
84
|
+
for (const dependency of definition.dependsOn ?? []) {
|
|
85
|
+
direct.set(dependency, [...(direct.get(dependency) ?? []), definition.id])
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const found = new Set<string>()
|
|
90
|
+
const queue = [...(direct.get(id) ?? [])]
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const next = queue.shift()
|
|
93
|
+
if (!next || found.has(next)) continue
|
|
94
|
+
found.add(next)
|
|
95
|
+
queue.push(...(direct.get(next) ?? []))
|
|
96
|
+
}
|
|
97
|
+
return [...found].sort()
|
|
98
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
4
|
+
import { errorMessage } from "../utils/errors.js"
|
|
5
|
+
|
|
6
|
+
export interface ProjectEntry {
|
|
7
|
+
/** Absolute path; the identity of a project. */
|
|
8
|
+
path: string
|
|
9
|
+
name: string
|
|
10
|
+
/** How devcc knows about it. */
|
|
11
|
+
source: "current" | "recent" | "configured" | "discovered"
|
|
12
|
+
/** Last time it was opened in devcc, for recents. */
|
|
13
|
+
lastOpenedAt?: Date
|
|
14
|
+
hasGit: boolean
|
|
15
|
+
hasPackageJson: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Directories that are never worth scanning into. */
|
|
19
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
20
|
+
"node_modules",
|
|
21
|
+
".git",
|
|
22
|
+
".next",
|
|
23
|
+
"dist",
|
|
24
|
+
"build",
|
|
25
|
+
"target",
|
|
26
|
+
"vendor",
|
|
27
|
+
"coverage",
|
|
28
|
+
".turbo",
|
|
29
|
+
".cache",
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
export function isProjectDirectory(path: string, exists: (p: string) => boolean = existsSync): boolean {
|
|
33
|
+
return exists(join(path, "package.json")) || exists(join(path, ".git"))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read `name` from a package.json, falling back to the directory name. */
|
|
37
|
+
export function projectNameFor(path: string): string {
|
|
38
|
+
try {
|
|
39
|
+
const raw: unknown = JSON.parse(readFileSync(join(path, "package.json"), "utf8"))
|
|
40
|
+
const name = typeof raw === "object" && raw !== null ? (raw as { name?: unknown }).name : undefined
|
|
41
|
+
if (typeof name === "string" && name.length > 0) return name
|
|
42
|
+
} catch {
|
|
43
|
+
// Fall through to the directory name.
|
|
44
|
+
}
|
|
45
|
+
return basename(path)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiscoverOptions {
|
|
49
|
+
/** Directories whose immediate children are candidate projects. */
|
|
50
|
+
searchDirs: readonly string[]
|
|
51
|
+
/** Maximum entries to return per search directory. */
|
|
52
|
+
limit?: number
|
|
53
|
+
readdir?: (dir: string) => string[]
|
|
54
|
+
exists?: (path: string) => boolean
|
|
55
|
+
isDirectory?: (path: string) => boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find sibling projects one level below each search directory.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately shallow: a recursive walk of a developer's home directory is slow
|
|
62
|
+
* and finds far more than anyone wants in a picker.
|
|
63
|
+
*/
|
|
64
|
+
export function discoverProjects(options: DiscoverOptions): ProjectEntry[] {
|
|
65
|
+
const readdir = options.readdir ?? ((dir: string) => readdirSync(dir))
|
|
66
|
+
const exists = options.exists ?? existsSync
|
|
67
|
+
const isDirectory = options.isDirectory ?? ((path: string) => statSync(path).isDirectory())
|
|
68
|
+
const limit = options.limit ?? 200
|
|
69
|
+
|
|
70
|
+
const found = new Map<string, ProjectEntry>()
|
|
71
|
+
|
|
72
|
+
for (const searchDir of options.searchDirs) {
|
|
73
|
+
let entries: string[]
|
|
74
|
+
try {
|
|
75
|
+
entries = readdir(searchDir)
|
|
76
|
+
} catch {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (found.size >= limit) break
|
|
82
|
+
if (entry.startsWith(".") || IGNORED_DIRECTORIES.has(entry)) continue
|
|
83
|
+
|
|
84
|
+
const path = join(searchDir, entry)
|
|
85
|
+
try {
|
|
86
|
+
if (!isDirectory(path)) continue
|
|
87
|
+
} catch {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (!isProjectDirectory(path, exists)) continue
|
|
91
|
+
if (found.has(path)) continue
|
|
92
|
+
|
|
93
|
+
found.set(path, {
|
|
94
|
+
path,
|
|
95
|
+
name: entry,
|
|
96
|
+
source: "discovered",
|
|
97
|
+
hasGit: exists(join(path, ".git")),
|
|
98
|
+
hasPackageJson: exists(join(path, "package.json")),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...found.values()]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface RecentsFile {
|
|
107
|
+
projects?: { path?: unknown; lastOpenedAt?: unknown }[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Where the cross-project recents list lives. */
|
|
111
|
+
export function recentsPath(home: string = homedir()): string {
|
|
112
|
+
return join(home, ".devcc", "recent-projects.json")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function readRecents(path: string = recentsPath(), limit = 20): ProjectEntry[] {
|
|
116
|
+
try {
|
|
117
|
+
if (!existsSync(path)) return []
|
|
118
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as RecentsFile
|
|
119
|
+
const entries = Array.isArray(parsed.projects) ? parsed.projects : []
|
|
120
|
+
|
|
121
|
+
return entries
|
|
122
|
+
.map((entry): ProjectEntry | null => {
|
|
123
|
+
const projectPath = typeof entry.path === "string" ? entry.path : null
|
|
124
|
+
if (!projectPath) return null
|
|
125
|
+
const openedAt = typeof entry.lastOpenedAt === "string" ? new Date(entry.lastOpenedAt) : undefined
|
|
126
|
+
return {
|
|
127
|
+
path: projectPath,
|
|
128
|
+
name: basename(projectPath),
|
|
129
|
+
source: "recent" as const,
|
|
130
|
+
lastOpenedAt: openedAt && !Number.isNaN(openedAt.getTime()) ? openedAt : undefined,
|
|
131
|
+
hasGit: existsSync(join(projectPath, ".git")),
|
|
132
|
+
hasPackageJson: existsSync(join(projectPath, "package.json")),
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.filter((entry): entry is ProjectEntry => entry !== null && existsSync(entry.path))
|
|
136
|
+
.slice(0, limit)
|
|
137
|
+
} catch {
|
|
138
|
+
return []
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Record a project as most-recently opened. Failure is never fatal. */
|
|
143
|
+
export function recordRecent(projectPath: string, path: string = recentsPath(), limit = 20): string | null {
|
|
144
|
+
try {
|
|
145
|
+
const existing = readRecents(path, limit).filter((entry) => entry.path !== projectPath)
|
|
146
|
+
const next = [{ path: projectPath, lastOpenedAt: new Date().toISOString() }].concat(
|
|
147
|
+
existing.map((entry) => ({
|
|
148
|
+
path: entry.path,
|
|
149
|
+
lastOpenedAt: (entry.lastOpenedAt ?? new Date()).toISOString(),
|
|
150
|
+
})),
|
|
151
|
+
)
|
|
152
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
153
|
+
writeFileSync(path, `${JSON.stringify({ projects: next.slice(0, limit) }, null, 2)}\n`, "utf8")
|
|
154
|
+
return null
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return errorMessage(error)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface BuildProjectListOptions {
|
|
161
|
+
currentPath: string
|
|
162
|
+
/** Explicit paths from project config. */
|
|
163
|
+
configured?: readonly string[]
|
|
164
|
+
/** Extra directories to scan for siblings. Defaults to the parent directory. */
|
|
165
|
+
searchDirs?: readonly string[]
|
|
166
|
+
recents?: readonly ProjectEntry[]
|
|
167
|
+
discovered?: readonly ProjectEntry[]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Merge the sources into one de-duplicated, ranked list:
|
|
172
|
+
* current project first, then recents, then configured, then discovered.
|
|
173
|
+
*/
|
|
174
|
+
export function buildProjectList(options: BuildProjectListOptions): ProjectEntry[] {
|
|
175
|
+
const current = resolve(options.currentPath)
|
|
176
|
+
const byPath = new Map<string, ProjectEntry>()
|
|
177
|
+
|
|
178
|
+
const add = (entry: ProjectEntry) => {
|
|
179
|
+
const path = resolve(entry.path)
|
|
180
|
+
const existing = byPath.get(path)
|
|
181
|
+
if (existing) {
|
|
182
|
+
// Keep the highest-priority source but carry over a known open time.
|
|
183
|
+
if (entry.lastOpenedAt && !existing.lastOpenedAt) existing.lastOpenedAt = entry.lastOpenedAt
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
byPath.set(path, { ...entry, path })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
add({
|
|
190
|
+
path: current,
|
|
191
|
+
name: projectNameFor(current),
|
|
192
|
+
source: "current",
|
|
193
|
+
hasGit: existsSync(join(current, ".git")),
|
|
194
|
+
hasPackageJson: existsSync(join(current, "package.json")),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
for (const entry of options.recents ?? []) add(entry)
|
|
198
|
+
|
|
199
|
+
for (const configured of options.configured ?? []) {
|
|
200
|
+
const path = resolve(current, configured)
|
|
201
|
+
if (!existsSync(path)) continue
|
|
202
|
+
add({
|
|
203
|
+
path,
|
|
204
|
+
name: basename(path),
|
|
205
|
+
source: "configured",
|
|
206
|
+
hasGit: existsSync(join(path, ".git")),
|
|
207
|
+
hasPackageJson: existsSync(join(path, "package.json")),
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const entry of options.discovered ?? []) add(entry)
|
|
212
|
+
|
|
213
|
+
return [...byPath.values()]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Default search directories: the parent of the current project. */
|
|
217
|
+
export function defaultSearchDirs(currentPath: string): string[] {
|
|
218
|
+
return [dirname(resolve(currentPath))]
|
|
219
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { TypedEmitter } from "../core/events.js"
|
|
2
|
+
import { DevccError, errorMessage, toDevccError } from "../utils/errors.js"
|
|
3
|
+
import { withTimeout } from "../utils/exec.js"
|
|
4
|
+
import type { ScopedLogger } from "../utils/logger.js"
|
|
5
|
+
import type { ServiceActionContext, ServiceAdapter, ServiceSnapshot, ServiceStatus } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export type ServiceManagerEvents = {
|
|
8
|
+
"service:list": [ServiceSnapshot[]]
|
|
9
|
+
"service:changed": [ServiceSnapshot]
|
|
10
|
+
"service:error": [DevccError]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ServiceManagerOptions {
|
|
14
|
+
logger: ScopedLogger
|
|
15
|
+
/** Poll interval for status refreshes. */
|
|
16
|
+
intervalMs?: number
|
|
17
|
+
/** Per-adapter budget; a slow adapter must not stall the others. */
|
|
18
|
+
statusTimeoutMs?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STALLED_STATUS: ServiceStatus = {
|
|
22
|
+
state: "unknown",
|
|
23
|
+
detail: "status check timed out",
|
|
24
|
+
checkedAt: new Date(0),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs detection once at startup (off the critical path) and then polls the
|
|
29
|
+
* adapters that reported themselves relevant.
|
|
30
|
+
*
|
|
31
|
+
* All adapters are queried concurrently with an individual timeout, so one
|
|
32
|
+
* unresponsive daemon cannot freeze the panel.
|
|
33
|
+
*/
|
|
34
|
+
export class ServiceManager extends TypedEmitter<ServiceManagerEvents> {
|
|
35
|
+
private readonly adapters = new Map<string, ServiceAdapter>()
|
|
36
|
+
private readonly detected = new Set<string>()
|
|
37
|
+
private readonly snapshots = new Map<string, ServiceSnapshot>()
|
|
38
|
+
private timer: NodeJS.Timeout | null = null
|
|
39
|
+
private refreshing = false
|
|
40
|
+
private readonly intervalMs: number
|
|
41
|
+
private readonly statusTimeoutMs: number
|
|
42
|
+
|
|
43
|
+
constructor(private readonly options: ServiceManagerOptions) {
|
|
44
|
+
super()
|
|
45
|
+
this.intervalMs = options.intervalMs ?? 5_000
|
|
46
|
+
this.statusTimeoutMs = options.statusTimeoutMs ?? 4_000
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
register(adapter: ServiceAdapter): void {
|
|
50
|
+
this.adapters.set(adapter.id, adapter)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerAll(adapters: readonly ServiceAdapter[]): void {
|
|
54
|
+
for (const adapter of adapters) this.register(adapter)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
list(): ServiceSnapshot[] {
|
|
58
|
+
return [...this.snapshots.values()]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get(id: string): ServiceSnapshot | undefined {
|
|
62
|
+
return this.snapshots.get(id)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getAdapter(id: string): ServiceAdapter | undefined {
|
|
66
|
+
return this.adapters.get(id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Ask every adapter whether it is relevant here. Failures mean "not relevant". */
|
|
70
|
+
async detectAll(): Promise<ServiceSnapshot[]> {
|
|
71
|
+
const results = await Promise.all(
|
|
72
|
+
[...this.adapters.values()].map(async (adapter) => {
|
|
73
|
+
try {
|
|
74
|
+
const relevant = await withTimeout(adapter.detect(), this.statusTimeoutMs, false)
|
|
75
|
+
return { adapter, relevant }
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.options.logger.debug("service detect failed", { id: adapter.id, error: errorMessage(error) })
|
|
78
|
+
return { adapter, relevant: false }
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
this.detected.clear()
|
|
84
|
+
for (const { adapter, relevant } of results) {
|
|
85
|
+
if (relevant) this.detected.add(adapter.id)
|
|
86
|
+
else this.snapshots.delete(adapter.id)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await this.refresh()
|
|
90
|
+
return this.list()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
start(): void {
|
|
94
|
+
if (this.timer) return
|
|
95
|
+
this.timer = setInterval(() => void this.refresh(), this.intervalMs)
|
|
96
|
+
this.timer.unref?.()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
stop(): void {
|
|
100
|
+
if (!this.timer) return
|
|
101
|
+
clearInterval(this.timer)
|
|
102
|
+
this.timer = null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Refresh all detected adapters concurrently. Overlapping calls are skipped. */
|
|
106
|
+
async refresh(): Promise<ServiceSnapshot[]> {
|
|
107
|
+
if (this.refreshing) return this.list()
|
|
108
|
+
this.refreshing = true
|
|
109
|
+
try {
|
|
110
|
+
const targets = [...this.detected]
|
|
111
|
+
.map((id) => this.adapters.get(id))
|
|
112
|
+
.filter((a): a is ServiceAdapter => !!a)
|
|
113
|
+
|
|
114
|
+
await Promise.all(
|
|
115
|
+
targets.map(async (adapter) => {
|
|
116
|
+
const status = await withTimeout(
|
|
117
|
+
adapter.getStatus().catch((error: unknown) => {
|
|
118
|
+
this.options.logger.debug("service status failed", {
|
|
119
|
+
id: adapter.id,
|
|
120
|
+
error: errorMessage(error),
|
|
121
|
+
})
|
|
122
|
+
return {
|
|
123
|
+
state: "unknown",
|
|
124
|
+
detail: errorMessage(error),
|
|
125
|
+
checkedAt: new Date(),
|
|
126
|
+
} satisfies ServiceStatus
|
|
127
|
+
}),
|
|
128
|
+
this.statusTimeoutMs,
|
|
129
|
+
{ ...STALLED_STATUS, checkedAt: new Date() },
|
|
130
|
+
)
|
|
131
|
+
this.setSnapshot(adapter, status)
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
this.emit("service:list", this.list())
|
|
136
|
+
return this.list()
|
|
137
|
+
} finally {
|
|
138
|
+
this.refreshing = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async runAction(
|
|
143
|
+
id: string,
|
|
144
|
+
action: "start" | "stop" | "restart",
|
|
145
|
+
context: ServiceActionContext,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const adapter = this.adapters.get(id)
|
|
148
|
+
if (!adapter) throw new DevccError({ scope: "service", message: `Unknown service "${id}"` })
|
|
149
|
+
|
|
150
|
+
const handler = adapter[action]
|
|
151
|
+
if (!handler) {
|
|
152
|
+
throw new DevccError({ scope: "service", message: `${adapter.name} does not support ${action}` })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await handler.call(adapter, context)
|
|
157
|
+
this.options.logger.info("service action ok", { id, action })
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const devccError = toDevccError(error, "service", `${adapter.name} ${action} failed`)
|
|
160
|
+
this.emit("service:error", devccError)
|
|
161
|
+
throw devccError
|
|
162
|
+
} finally {
|
|
163
|
+
await this.refresh()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private setSnapshot(adapter: ServiceAdapter, status: ServiceStatus): void {
|
|
168
|
+
const snapshot: ServiceSnapshot = {
|
|
169
|
+
id: adapter.id,
|
|
170
|
+
name: adapter.name,
|
|
171
|
+
category: adapter.category,
|
|
172
|
+
status,
|
|
173
|
+
canStart: typeof adapter.start === "function",
|
|
174
|
+
canStop: typeof adapter.stop === "function",
|
|
175
|
+
canRestart: typeof adapter.restart === "function",
|
|
176
|
+
}
|
|
177
|
+
this.snapshots.set(adapter.id, snapshot)
|
|
178
|
+
this.emit("service:changed", snapshot)
|
|
179
|
+
}
|
|
180
|
+
}
|