@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,220 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { DevccError } from "../utils/errors.js"
|
|
4
|
+
import { exec } from "../utils/exec.js"
|
|
5
|
+
import type {
|
|
6
|
+
ServiceActionContext,
|
|
7
|
+
ServiceAdapter,
|
|
8
|
+
ServiceState,
|
|
9
|
+
ServiceStatus,
|
|
10
|
+
ServiceStatusItem,
|
|
11
|
+
} from "./types.js"
|
|
12
|
+
|
|
13
|
+
const COMPOSE_FILES = [
|
|
14
|
+
"docker-compose.yml",
|
|
15
|
+
"docker-compose.yaml",
|
|
16
|
+
"compose.yml",
|
|
17
|
+
"compose.yaml",
|
|
18
|
+
"docker/docker-compose.yml",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export function findComposeFile(
|
|
22
|
+
rootDir: string,
|
|
23
|
+
exists: (path: string) => boolean = existsSync,
|
|
24
|
+
): string | null {
|
|
25
|
+
for (const file of COMPOSE_FILES) {
|
|
26
|
+
if (exists(join(rootDir, file))) return file
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ComposePsEntry {
|
|
32
|
+
Name?: string
|
|
33
|
+
Service?: string
|
|
34
|
+
State?: string
|
|
35
|
+
Status?: string
|
|
36
|
+
Health?: string
|
|
37
|
+
Publishers?: { PublishedPort?: number; TargetPort?: number }[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `docker compose ps --format json` emits either a JSON array or one JSON
|
|
42
|
+
* object per line depending on the CLI version. Handle both.
|
|
43
|
+
*/
|
|
44
|
+
export function parseComposePs(output: string): ServiceStatusItem[] {
|
|
45
|
+
const trimmed = output.trim()
|
|
46
|
+
if (trimmed === "") return []
|
|
47
|
+
|
|
48
|
+
const entries: ComposePsEntry[] = []
|
|
49
|
+
if (trimmed.startsWith("[")) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed: unknown = JSON.parse(trimmed)
|
|
52
|
+
if (Array.isArray(parsed)) entries.push(...(parsed as ComposePsEntry[]))
|
|
53
|
+
} catch {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
for (const line of trimmed.split("\n")) {
|
|
58
|
+
if (line.trim() === "") continue
|
|
59
|
+
try {
|
|
60
|
+
entries.push(JSON.parse(line) as ComposePsEntry)
|
|
61
|
+
} catch {
|
|
62
|
+
// Skip malformed lines rather than losing the whole listing.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return entries.map((entry) => {
|
|
68
|
+
const name = entry.Service ?? entry.Name ?? "container"
|
|
69
|
+
const ports = (entry.Publishers ?? [])
|
|
70
|
+
.map((publisher) => publisher.PublishedPort)
|
|
71
|
+
.filter((port): port is number => typeof port === "number" && port > 0)
|
|
72
|
+
const detail = [entry.Status, ports.length > 0 ? `:${[...new Set(ports)].join(",")}` : null]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" ")
|
|
75
|
+
return { name, state: composeState(entry), detail: detail || undefined }
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function composeState(entry: ComposePsEntry): ServiceState {
|
|
80
|
+
const health = entry.Health?.toLowerCase()
|
|
81
|
+
if (health === "unhealthy" || health === "starting") return "degraded"
|
|
82
|
+
switch (entry.State?.toLowerCase()) {
|
|
83
|
+
case "running":
|
|
84
|
+
return "running"
|
|
85
|
+
case "restarting":
|
|
86
|
+
case "paused":
|
|
87
|
+
return "degraded"
|
|
88
|
+
case "exited":
|
|
89
|
+
case "dead":
|
|
90
|
+
case "created":
|
|
91
|
+
return "stopped"
|
|
92
|
+
default:
|
|
93
|
+
return "unknown"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DockerServiceOptions {
|
|
98
|
+
rootDir: string
|
|
99
|
+
timeoutMs?: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Docker / Docker Compose adapter.
|
|
104
|
+
*
|
|
105
|
+
* `stop` runs `docker compose stop` (not `down`): stopping must never remove
|
|
106
|
+
* containers, networks or volumes without an explicit confirmation.
|
|
107
|
+
*/
|
|
108
|
+
export class DockerService implements ServiceAdapter {
|
|
109
|
+
readonly id = "docker"
|
|
110
|
+
readonly name = "Docker Compose"
|
|
111
|
+
readonly category = "container" as const
|
|
112
|
+
|
|
113
|
+
private composeFile: string | null
|
|
114
|
+
private readonly timeoutMs: number
|
|
115
|
+
|
|
116
|
+
constructor(private readonly options: DockerServiceOptions) {
|
|
117
|
+
this.composeFile = findComposeFile(options.rootDir)
|
|
118
|
+
this.timeoutMs = options.timeoutMs ?? 6_000
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get hasCompose(): boolean {
|
|
122
|
+
return this.composeFile !== null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async detect(): Promise<boolean> {
|
|
126
|
+
this.composeFile = findComposeFile(this.options.rootDir)
|
|
127
|
+
const result = await exec("docker", ["--version"], { timeoutMs: 3_000 })
|
|
128
|
+
return result.spawnError === undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getStatus(): Promise<ServiceStatus> {
|
|
132
|
+
const checkedAt = new Date()
|
|
133
|
+
const info = await exec("docker", ["info", "--format", "{{.ServerVersion}}"], {
|
|
134
|
+
cwd: this.options.rootDir,
|
|
135
|
+
timeoutMs: this.timeoutMs,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (!info.ok) {
|
|
139
|
+
return {
|
|
140
|
+
state: "unavailable",
|
|
141
|
+
detail: info.spawnError ? "docker not installed" : "daemon unreachable",
|
|
142
|
+
checkedAt,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const serverVersion = info.stdout.trim()
|
|
147
|
+
if (!this.composeFile) {
|
|
148
|
+
return { state: "running", detail: `daemon ${serverVersion}`, checkedAt }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ps = await exec("docker", ["compose", "ps", "--all", "--format", "json"], {
|
|
152
|
+
cwd: this.options.rootDir,
|
|
153
|
+
timeoutMs: this.timeoutMs,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if (!ps.ok) {
|
|
157
|
+
return { state: "degraded", detail: `daemon ${serverVersion}, compose ps failed`, checkedAt }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const items = parseComposePs(ps.stdout)
|
|
161
|
+
const running = items.filter((item) => item.state === "running").length
|
|
162
|
+
const state: ServiceState =
|
|
163
|
+
items.length === 0 ? "stopped" : running === items.length ? "running" : "degraded"
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
state,
|
|
167
|
+
detail:
|
|
168
|
+
items.length === 0 ? `no containers (${this.composeFile})` : `${running}/${items.length} running`,
|
|
169
|
+
items,
|
|
170
|
+
checkedAt,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async start(): Promise<void> {
|
|
175
|
+
this.requireCompose()
|
|
176
|
+
await this.compose(["up", "--detach"], 120_000)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async stop(): Promise<void> {
|
|
180
|
+
this.requireCompose()
|
|
181
|
+
// `stop`, never `down` - see the class comment.
|
|
182
|
+
await this.compose(["stop"], 60_000)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async restart(): Promise<void> {
|
|
186
|
+
this.requireCompose()
|
|
187
|
+
await this.compose(["restart"], 120_000)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** `docker compose down` removes containers and networks, so it must be confirmed. */
|
|
191
|
+
async down(context: ServiceActionContext, removeVolumes = false): Promise<void> {
|
|
192
|
+
this.requireCompose()
|
|
193
|
+
const message = removeVolumes
|
|
194
|
+
? "This removes containers, networks AND named volumes. Data will be lost."
|
|
195
|
+
: "This removes containers and networks created by compose."
|
|
196
|
+
const confirmed = await context.confirm(
|
|
197
|
+
removeVolumes ? "docker compose down --volumes" : "docker compose down",
|
|
198
|
+
message,
|
|
199
|
+
)
|
|
200
|
+
if (!confirmed) return
|
|
201
|
+
await this.compose(removeVolumes ? ["down", "--volumes"] : ["down"], 120_000)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private requireCompose(): void {
|
|
205
|
+
if (!this.composeFile) {
|
|
206
|
+
throw new DevccError({ scope: "docker", message: "No docker compose file found in this project" })
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async compose(args: string[], timeoutMs: number): Promise<void> {
|
|
211
|
+
const result = await exec("docker", ["compose", ...args], { cwd: this.options.rootDir, timeoutMs })
|
|
212
|
+
if (!result.ok) {
|
|
213
|
+
throw new DevccError({
|
|
214
|
+
scope: "docker",
|
|
215
|
+
message: `docker compose ${args.join(" ")} failed`,
|
|
216
|
+
detail: result.stderr.trim() || result.stdout.trim() || (result.timedOut ? "timed out" : undefined),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { probeTcpPort } from "../utils/exec.js"
|
|
2
|
+
import type { ServiceAdapter, ServiceStatus } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export interface PortServiceOptions {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
port: number
|
|
8
|
+
host?: string
|
|
9
|
+
category?: ServiceAdapter["category"]
|
|
10
|
+
/** When set, a successful HTTP request to this path upgrades the detail line. */
|
|
11
|
+
healthPath?: string
|
|
12
|
+
/** Auto-detected services hide themselves when nothing is listening. */
|
|
13
|
+
onlyWhenListening?: boolean
|
|
14
|
+
timeoutMs?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A generic "is something serving on this port?" adapter.
|
|
19
|
+
*
|
|
20
|
+
* This is how Next.js, Vite, Payload, Typesense and any user-declared HTTP
|
|
21
|
+
* service are represented - devcc observes them, it does not own them, so it
|
|
22
|
+
* deliberately exposes no start/stop actions here. Use a managed process for
|
|
23
|
+
* something devcc should control.
|
|
24
|
+
*/
|
|
25
|
+
export class PortService implements ServiceAdapter {
|
|
26
|
+
readonly id: string
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly category: ServiceAdapter["category"]
|
|
29
|
+
|
|
30
|
+
private readonly host: string
|
|
31
|
+
private readonly port: number
|
|
32
|
+
private readonly healthPath: string | undefined
|
|
33
|
+
private readonly onlyWhenListening: boolean
|
|
34
|
+
private readonly timeoutMs: number
|
|
35
|
+
|
|
36
|
+
constructor(options: PortServiceOptions) {
|
|
37
|
+
this.id = options.id
|
|
38
|
+
this.name = options.name
|
|
39
|
+
this.category = options.category ?? "web"
|
|
40
|
+
this.host = options.host ?? "127.0.0.1"
|
|
41
|
+
this.port = options.port
|
|
42
|
+
this.healthPath = options.healthPath
|
|
43
|
+
this.onlyWhenListening = options.onlyWhenListening ?? false
|
|
44
|
+
this.timeoutMs = options.timeoutMs ?? 1_000
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async detect(): Promise<boolean> {
|
|
48
|
+
if (!this.onlyWhenListening) return true
|
|
49
|
+
return probeTcpPort(this.port, this.host, this.timeoutMs)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getStatus(): Promise<ServiceStatus> {
|
|
53
|
+
const checkedAt = new Date()
|
|
54
|
+
const endpoint = `${this.host}:${this.port}`
|
|
55
|
+
const listening = await probeTcpPort(this.port, this.host, this.timeoutMs)
|
|
56
|
+
if (!listening) return { state: "stopped", endpoint, detail: "not listening", checkedAt }
|
|
57
|
+
|
|
58
|
+
if (!this.healthPath) return { state: "running", endpoint, detail: "listening", checkedAt }
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`http://${endpoint}${this.healthPath}`, {
|
|
62
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
63
|
+
})
|
|
64
|
+
return {
|
|
65
|
+
state: response.ok ? "running" : "degraded",
|
|
66
|
+
endpoint,
|
|
67
|
+
detail: `HTTP ${response.status}`,
|
|
68
|
+
checkedAt,
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
return { state: "degraded", endpoint, detail: "listening, health check failed", checkedAt }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface FrameworkHint {
|
|
77
|
+
/** Dependency name to look for in package.json. */
|
|
78
|
+
dependency: string
|
|
79
|
+
id: string
|
|
80
|
+
name: string
|
|
81
|
+
port: number
|
|
82
|
+
category?: ServiceAdapter["category"]
|
|
83
|
+
healthPath?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Well-known local dev servers and their conventional ports. */
|
|
87
|
+
const FRAMEWORK_HINTS: readonly FrameworkHint[] = [
|
|
88
|
+
{ dependency: "next", id: "next", name: "Next.js", port: 3000 },
|
|
89
|
+
{ dependency: "vite", id: "vite", name: "Vite", port: 5173 },
|
|
90
|
+
{ dependency: "@remix-run/dev", id: "remix", name: "Remix", port: 3000 },
|
|
91
|
+
{ dependency: "nuxt", id: "nuxt", name: "Nuxt", port: 3000 },
|
|
92
|
+
{ dependency: "astro", id: "astro", name: "Astro", port: 4321 },
|
|
93
|
+
{ dependency: "payload", id: "payload", name: "Payload CMS", port: 3000, healthPath: "/admin" },
|
|
94
|
+
{ dependency: "typesense", id: "typesense", name: "Typesense", port: 8108, category: "database" },
|
|
95
|
+
{ dependency: "express", id: "express", name: "Express API", port: 3001, category: "runtime" },
|
|
96
|
+
{ dependency: "fastify", id: "fastify", name: "Fastify API", port: 3001, category: "runtime" },
|
|
97
|
+
{ dependency: "@nestjs/core", id: "nest", name: "NestJS API", port: 3000, category: "runtime" },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
function dependencyNames(packageJson: Record<string, unknown> | null): Set<string> {
|
|
101
|
+
const names = new Set<string>()
|
|
102
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
103
|
+
const value = packageJson?.[field]
|
|
104
|
+
if (typeof value === "object" && value !== null) {
|
|
105
|
+
for (const name of Object.keys(value)) names.add(name)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return names
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build port adapters for frameworks this project actually depends on.
|
|
113
|
+
* Nothing is assumed: a project with none of these gets no adapters.
|
|
114
|
+
*/
|
|
115
|
+
export function detectNodeServices(packageJson: Record<string, unknown> | null): PortService[] {
|
|
116
|
+
const dependencies = dependencyNames(packageJson)
|
|
117
|
+
const seenPorts = new Set<number>()
|
|
118
|
+
const adapters: PortService[] = []
|
|
119
|
+
|
|
120
|
+
for (const hint of FRAMEWORK_HINTS) {
|
|
121
|
+
if (!dependencies.has(hint.dependency)) continue
|
|
122
|
+
if (seenPorts.has(hint.port)) continue
|
|
123
|
+
seenPorts.add(hint.port)
|
|
124
|
+
adapters.push(
|
|
125
|
+
new PortService({
|
|
126
|
+
id: hint.id,
|
|
127
|
+
name: hint.name,
|
|
128
|
+
port: hint.port,
|
|
129
|
+
category: hint.category ?? "web",
|
|
130
|
+
healthPath: hint.healthPath,
|
|
131
|
+
onlyWhenListening: true,
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return adapters
|
|
137
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { probeTcpPort } from "../utils/exec.js"
|
|
2
|
+
import type { ServiceAdapter, ServiceStatus } from "./types.js"
|
|
3
|
+
|
|
4
|
+
const DEFAULT_HOST = "127.0.0.1"
|
|
5
|
+
const DEFAULT_PORT = 11434
|
|
6
|
+
|
|
7
|
+
interface OllamaTagsResponse {
|
|
8
|
+
models?: { name?: string }[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OllamaServiceOptions {
|
|
12
|
+
host?: string
|
|
13
|
+
port?: number
|
|
14
|
+
timeoutMs?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ollama adapter. Uses the HTTP API for a model count when the port is open;
|
|
19
|
+
* falls back to a plain port probe if the API call fails.
|
|
20
|
+
*/
|
|
21
|
+
export class OllamaService implements ServiceAdapter {
|
|
22
|
+
readonly id = "ollama"
|
|
23
|
+
readonly name = "Ollama"
|
|
24
|
+
readonly category = "ai" as const
|
|
25
|
+
|
|
26
|
+
private readonly host: string
|
|
27
|
+
private readonly port: number
|
|
28
|
+
private readonly timeoutMs: number
|
|
29
|
+
|
|
30
|
+
constructor(options: OllamaServiceOptions = {}) {
|
|
31
|
+
const fromEnv = parseHostPort(process.env.OLLAMA_HOST)
|
|
32
|
+
this.host = options.host ?? fromEnv?.host ?? DEFAULT_HOST
|
|
33
|
+
this.port = options.port ?? fromEnv?.port ?? DEFAULT_PORT
|
|
34
|
+
this.timeoutMs = options.timeoutMs ?? 1_500
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async detect(): Promise<boolean> {
|
|
38
|
+
return probeTcpPort(this.port, this.host)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getStatus(): Promise<ServiceStatus> {
|
|
42
|
+
const checkedAt = new Date()
|
|
43
|
+
const endpoint = `${this.host}:${this.port}`
|
|
44
|
+
const listening = await probeTcpPort(this.port, this.host)
|
|
45
|
+
if (!listening) return { state: "stopped", endpoint, detail: "not listening", checkedAt }
|
|
46
|
+
|
|
47
|
+
const models = await this.listModels()
|
|
48
|
+
return {
|
|
49
|
+
state: "running",
|
|
50
|
+
endpoint,
|
|
51
|
+
detail: models === null ? "listening" : `${models.length} model${models.length === 1 ? "" : "s"}`,
|
|
52
|
+
items: models?.slice(0, 6).map((name) => ({ name, state: "running" as const })),
|
|
53
|
+
checkedAt,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async listModels(): Promise<string[] | null> {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`http://${this.host}:${this.port}/api/tags`, {
|
|
60
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
61
|
+
})
|
|
62
|
+
if (!response.ok) return null
|
|
63
|
+
const body = (await response.json()) as OllamaTagsResponse
|
|
64
|
+
return (body.models ?? []).map((model) => model.name ?? "unnamed")
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseHostPort(value: string | undefined): { host: string; port: number } | null {
|
|
72
|
+
if (!value) return null
|
|
73
|
+
const withScheme = value.includes("://") ? value : `http://${value}`
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(withScheme)
|
|
76
|
+
return { host: parsed.hostname || DEFAULT_HOST, port: Number(parsed.port || DEFAULT_PORT) }
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { probeTcpPort } from "../utils/exec.js"
|
|
2
|
+
import type { ServiceAdapter, ServiceStatus } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export interface PostgresServiceOptions {
|
|
5
|
+
host?: string
|
|
6
|
+
port?: number
|
|
7
|
+
/** Only report the service when one of these is true. */
|
|
8
|
+
hints?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Parse `postgres://user:pass@host:5432/db` style URLs for host/port. */
|
|
12
|
+
export function parsePostgresUrl(url: string | undefined): { host: string; port: number } | null {
|
|
13
|
+
if (!url) return null
|
|
14
|
+
try {
|
|
15
|
+
const parsed = new URL(url)
|
|
16
|
+
if (!parsed.protocol.startsWith("postgres")) return null
|
|
17
|
+
return { host: parsed.hostname || "127.0.0.1", port: Number(parsed.port || 5432) }
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* PostgreSQL adapter.
|
|
25
|
+
*
|
|
26
|
+
* Detection is intentionally conservative: we only claim Postgres is relevant
|
|
27
|
+
* when the project mentions it (DATABASE_URL / a pg dependency) or something is
|
|
28
|
+
* actually listening on the port.
|
|
29
|
+
*/
|
|
30
|
+
export class PostgresService implements ServiceAdapter {
|
|
31
|
+
readonly id = "postgres"
|
|
32
|
+
readonly name = "PostgreSQL"
|
|
33
|
+
readonly category = "database" as const
|
|
34
|
+
|
|
35
|
+
private readonly host: string
|
|
36
|
+
private readonly port: number
|
|
37
|
+
|
|
38
|
+
constructor(options: PostgresServiceOptions = {}) {
|
|
39
|
+
const fromEnv = parsePostgresUrl(process.env.DATABASE_URL ?? process.env.POSTGRES_URL)
|
|
40
|
+
this.host = options.host ?? fromEnv?.host ?? "127.0.0.1"
|
|
41
|
+
this.port = options.port ?? fromEnv?.port ?? 5432
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async detect(): Promise<boolean> {
|
|
45
|
+
return probeTcpPort(this.port, this.host)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getStatus(): Promise<ServiceStatus> {
|
|
49
|
+
const listening = await probeTcpPort(this.port, this.host)
|
|
50
|
+
return {
|
|
51
|
+
state: listening ? "running" : "stopped",
|
|
52
|
+
endpoint: `${this.host}:${this.port}`,
|
|
53
|
+
detail: listening ? "accepting connections" : "not listening",
|
|
54
|
+
checkedAt: new Date(),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { probeTcpPort } from "../utils/exec.js"
|
|
2
|
+
import type { ServiceAdapter, ServiceStatus } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export interface RedisServiceOptions {
|
|
5
|
+
host?: string
|
|
6
|
+
port?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseRedisUrl(url: string | undefined): { host: string; port: number } | null {
|
|
10
|
+
if (!url) return null
|
|
11
|
+
try {
|
|
12
|
+
const parsed = new URL(url)
|
|
13
|
+
if (!parsed.protocol.startsWith("redis")) return null
|
|
14
|
+
return { host: parsed.hostname || "127.0.0.1", port: Number(parsed.port || 6379) }
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class RedisService implements ServiceAdapter {
|
|
21
|
+
readonly id = "redis"
|
|
22
|
+
readonly name = "Redis"
|
|
23
|
+
readonly category = "database" as const
|
|
24
|
+
|
|
25
|
+
private readonly host: string
|
|
26
|
+
private readonly port: number
|
|
27
|
+
|
|
28
|
+
constructor(options: RedisServiceOptions = {}) {
|
|
29
|
+
const fromEnv = parseRedisUrl(process.env.REDIS_URL)
|
|
30
|
+
this.host = options.host ?? fromEnv?.host ?? "127.0.0.1"
|
|
31
|
+
this.port = options.port ?? fromEnv?.port ?? 6379
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async detect(): Promise<boolean> {
|
|
35
|
+
return probeTcpPort(this.port, this.host)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getStatus(): Promise<ServiceStatus> {
|
|
39
|
+
const listening = await probeTcpPort(this.port, this.host)
|
|
40
|
+
return {
|
|
41
|
+
state: listening ? "running" : "stopped",
|
|
42
|
+
endpoint: `${this.host}:${this.port}`,
|
|
43
|
+
detail: listening ? "accepting connections" : "not listening",
|
|
44
|
+
checkedAt: new Date(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service adapters describe things the project depends on but that devcc does
|
|
3
|
+
* not necessarily own: Docker, a database, a local model server, a dev server
|
|
4
|
+
* someone started in another terminal.
|
|
5
|
+
*
|
|
6
|
+
* Adapters are detected at runtime. Nothing here assumes a given project has
|
|
7
|
+
* any particular service.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ServiceState = "running" | "stopped" | "degraded" | "unknown" | "unavailable"
|
|
11
|
+
|
|
12
|
+
export interface ServiceStatusItem {
|
|
13
|
+
name: string
|
|
14
|
+
state: ServiceState
|
|
15
|
+
detail?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServiceStatus {
|
|
19
|
+
state: ServiceState
|
|
20
|
+
/** One-line summary shown next to the service name. */
|
|
21
|
+
detail?: string
|
|
22
|
+
/** Where to reach it, e.g. `localhost:5432`. */
|
|
23
|
+
endpoint?: string
|
|
24
|
+
/** Sub-entries, e.g. individual compose containers. */
|
|
25
|
+
items?: ServiceStatusItem[]
|
|
26
|
+
checkedAt: Date
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ServiceActionContext {
|
|
30
|
+
/** Ask the user before doing something destructive. Resolves to the answer. */
|
|
31
|
+
confirm: (title: string, message: string) => Promise<boolean>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ServiceAdapter {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
/** Grouping label shown in the Services view. */
|
|
38
|
+
category: "container" | "database" | "runtime" | "ai" | "web"
|
|
39
|
+
|
|
40
|
+
/** Cheap check: is this service relevant to this project/host at all? */
|
|
41
|
+
detect: () => Promise<boolean>
|
|
42
|
+
getStatus: () => Promise<ServiceStatus>
|
|
43
|
+
|
|
44
|
+
start?: (context: ServiceActionContext) => Promise<void>
|
|
45
|
+
stop?: (context: ServiceActionContext) => Promise<void>
|
|
46
|
+
restart?: (context: ServiceActionContext) => Promise<void>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ServiceSnapshot {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
category: ServiceAdapter["category"]
|
|
53
|
+
status: ServiceStatus
|
|
54
|
+
canStart: boolean
|
|
55
|
+
canStop: boolean
|
|
56
|
+
canRestart: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const UNKNOWN_STATUS: ServiceStatus = { state: "unknown", checkedAt: new Date(0) }
|