opencode-docker-panel 0.4.8 → 0.5.1
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/CHANGELOG.md +33 -1
- package/README.md +86 -194
- package/README.ru.md +76 -194
- package/dist/commands.js +9 -7
- package/dist/compose.js +36 -14
- package/dist/docker.js +26 -6
- package/dist/logs.js +3 -5
- package/dist/panel.js +232 -116
- package/dist/poll.js +68 -8
- package/dist/runtime.js +67 -0
- package/dist/tui.js +2 -2
- package/package.json +5 -2
package/dist/commands.js
CHANGED
|
@@ -10,12 +10,12 @@ export const ACTION_LABEL = {
|
|
|
10
10
|
down: "Down compose project"
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
/** The name
|
|
13
|
+
/** The name goes after `--` because a container name may start with a dash and docker reads it as a flag */
|
|
14
14
|
export function buildArgs(action, container, target) {
|
|
15
15
|
if (action === "up") {
|
|
16
16
|
if (!target) throw new Error(`container ${container.name} has no resolved compose target`);
|
|
17
|
-
// Both flags are explicit
|
|
18
|
-
// environment, and this way `up` acts on the
|
|
17
|
+
// Both flags are explicit because the project's `name:` can be templated or overridden by the
|
|
18
|
+
// environment, and this way `up` acts on the project the panel is already showing
|
|
19
19
|
return ["compose", "-f", target.file, "-p", target.project, "up", "-d"];
|
|
20
20
|
}
|
|
21
21
|
if (action === "down") {
|
|
@@ -26,7 +26,7 @@ export function buildArgs(action, container, target) {
|
|
|
26
26
|
return [action, "--", container.name];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** Only
|
|
29
|
+
/** Only what docker accepts for the current state, with the destructive action last */
|
|
30
30
|
export function availableActions(container, target) {
|
|
31
31
|
const down = container.composeProject ? ["down"] : [];
|
|
32
32
|
const up = container.composeProject && target ? ["up"] : [];
|
|
@@ -48,9 +48,11 @@ function firstLine(text) {
|
|
|
48
48
|
if (!line) return "";
|
|
49
49
|
return line.length > 80 ? `${line.slice(0, 77)}...` : line;
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Runs an already-built argument list.
|
|
53
|
-
*
|
|
51
|
+
/**
|
|
52
|
+
* Runs an already-built argument list.
|
|
53
|
+
*
|
|
54
|
+
* `up` has no container behind it, so the boundary takes the argv rather than a container the caller
|
|
55
|
+
* would have to invent to satisfy the type.
|
|
54
56
|
*/
|
|
55
57
|
export function runDockerArgs(args) {
|
|
56
58
|
return new Promise(resolve => {
|
package/dist/compose.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
/** compose v2's own search order, so the resolver sees the
|
|
4
|
+
/** compose v2's own search order, so the resolver sees the file the CLI would pick */
|
|
5
5
|
const CANDIDATES = ["compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"];
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compose derives a project name from the directory by lowercasing it, dropping everything outside
|
|
9
|
+
* `[a-z0-9_-]` and then dropping whatever is left before the first alphanumeric. Measured against
|
|
10
|
+
* compose v2: `My.Stack 2` becomes `mystack2`, `A+B` becomes `ab`, `--Lead` becomes `lead`,
|
|
11
|
+
* `Ünïcode` becomes `ncode`, and a directory named `---` fails with "project name must not be empty".
|
|
12
|
+
*
|
|
13
|
+
* The panel passes the name explicitly as `-p`, which replaces that derivation, so the same
|
|
14
|
+
* normalization has to happen here or compose rejects what it would have accepted on its own.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeProject(name) {
|
|
17
|
+
return name.toLowerCase().replace(/[^a-z0-9_-]/g, "").replace(/^[^a-z0-9]+/, "");
|
|
18
|
+
}
|
|
19
|
+
const VALID_PROJECT = /^[a-z0-9][a-z0-9_-]*$/;
|
|
20
|
+
/** An unindented `name:` is top-level by construction, which is why a regex beats a YAML parser here */
|
|
7
21
|
function declaredProject(body) {
|
|
8
|
-
// A UTF-8 BOM makes `^` miss the first line
|
|
9
|
-
//
|
|
22
|
+
// A UTF-8 BOM makes `^` miss the first line and hands back the directory name as the project, and
|
|
23
|
+
// PowerShell writes one
|
|
10
24
|
const match = body.replace(/^\uFEFF/, "").match(/^name:[ \t]*(\S.*?)[ \t]*$/m);
|
|
11
25
|
return match?.[1]?.replace(/^["']|["']$/g, "") ?? "";
|
|
12
26
|
}
|
|
13
27
|
|
|
14
|
-
/**
|
|
15
|
-
* Finds the agent directory's compose file and reports the project it declares, if any.
|
|
16
|
-
*
|
|
28
|
+
/**
|
|
29
|
+
* Finds the agent directory's compose file and reports the project it declares, if any.
|
|
30
|
+
*
|
|
31
|
+
* Read on every call because the caller opens a menu, not the poll loop.
|
|
17
32
|
*/
|
|
18
33
|
export function findComposeFile(agentDir) {
|
|
19
34
|
if (!agentDir) return null;
|
|
@@ -25,21 +40,28 @@ export function findComposeFile(agentDir) {
|
|
|
25
40
|
} catch {
|
|
26
41
|
continue;
|
|
27
42
|
}
|
|
28
|
-
// The first file that exists wins even when its project differs, because that is the
|
|
29
|
-
//
|
|
43
|
+
// The first file that exists wins even when its project differs, because that is the one compose
|
|
44
|
+
// itself would use, and falling through would act on a stack the CLI ignores
|
|
45
|
+
const declared = declaredProject(body);
|
|
46
|
+
const project = declared || normalizeProject(basename(dirname(file)));
|
|
47
|
+
// A declared name compose will reject, or a directory that normalizes to nothing, is a refusal
|
|
48
|
+
// rather than a button that can only fail
|
|
49
|
+
if (!VALID_PROJECT.test(project)) return null;
|
|
30
50
|
return {
|
|
31
51
|
dir: agentDir,
|
|
32
52
|
file,
|
|
33
|
-
project
|
|
53
|
+
project
|
|
34
54
|
};
|
|
35
55
|
}
|
|
36
56
|
return null;
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
/**
|
|
40
|
-
* The compose target for a container
|
|
41
|
-
*
|
|
42
|
-
*
|
|
59
|
+
/**
|
|
60
|
+
* The compose target for a container already part of the project, or null when the agent directory's
|
|
61
|
+
* file belongs to somebody else.
|
|
62
|
+
*
|
|
63
|
+
* `up` runs build and command lines from that file, so an uncertain match has to hide the button
|
|
64
|
+
* rather than point it at the wrong stack.
|
|
43
65
|
*/
|
|
44
66
|
export function resolveComposeTarget(agentDir, project) {
|
|
45
67
|
const target = findComposeFile(agentDir);
|
package/dist/docker.js
CHANGED
|
@@ -5,11 +5,22 @@ const TIMEOUT_MS = 4000;
|
|
|
5
5
|
const MAX_BUFFER = 4 * 1024 * 1024;
|
|
6
6
|
const COMPOSE_PROJECT = "com.docker.compose.project";
|
|
7
7
|
const COMPOSE_SERVICE = "com.docker.compose.service";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Desktop backend owns the named pipe `dockerDesktopLinuxEngine`, and `docker ps` reports it as
|
|
11
|
+
* missing within about a quarter second, while `docker desktop status` needs four seconds to admit
|
|
12
|
+
* the same thing. Measured with Desktop stopped: `docker ps` 257 ms, `docker desktop status` 4.4 s.
|
|
13
|
+
*
|
|
14
|
+
* The wording is the only evidence here, so it has to name the pipe and its absence together: a bare
|
|
15
|
+
* `cannot find the file specified` also covers a missing binary and must not be read as a stopped
|
|
16
|
+
* engine.
|
|
17
|
+
*/
|
|
18
|
+
export function isDesktopPipeMissing(raw) {
|
|
19
|
+
const text = raw.toLowerCase();
|
|
20
|
+
return text.includes("dockerdesktoplinuxengine") && (text.includes("cannot find the file specified") || text.includes("no such file"));
|
|
21
|
+
}
|
|
8
22
|
export function shortReason(raw) {
|
|
9
23
|
const text = raw.toLowerCase();
|
|
10
|
-
if (text.includes("dockerdesktoplinuxengine") || text.includes("cannot find the file specified")) {
|
|
11
|
-
return "docker desktop not running";
|
|
12
|
-
}
|
|
13
24
|
if (text.includes("docker_engine") || text.includes("is the server running")) return "no connection to docker";
|
|
14
25
|
if (text.includes("access is denied") || text.includes("permission denied")) return "no permission to talk to docker";
|
|
15
26
|
const firstLine = raw.split(/\r?\n/).map(line => line.trim()).find(line => line.length > 0);
|
|
@@ -17,7 +28,7 @@ export function shortReason(raw) {
|
|
|
17
28
|
return firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
/** Docker joins labels with commas
|
|
31
|
+
/** Docker joins labels with commas and neither a compose project nor a service name may contain one, so a plain split is enough */
|
|
21
32
|
function composeLabels(raw) {
|
|
22
33
|
let composeProject;
|
|
23
34
|
let composeService;
|
|
@@ -33,7 +44,7 @@ function composeLabels(raw) {
|
|
|
33
44
|
};
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
/** docker reports CreatedAt
|
|
47
|
+
/** docker reports CreatedAt as a unix timestamp in seconds or as a date with a trailing zone name, which Date.parse rejects */
|
|
37
48
|
function parseCreatedAt(raw) {
|
|
38
49
|
const text = String(raw ?? "").trim();
|
|
39
50
|
if (!text) return 0;
|
|
@@ -104,10 +115,19 @@ export function runDockerPs(timeoutMs = TIMEOUT_MS) {
|
|
|
104
115
|
});
|
|
105
116
|
return;
|
|
106
117
|
}
|
|
118
|
+
const raw = String(stderr || error.message);
|
|
119
|
+
if (isDesktopPipeMissing(raw)) {
|
|
120
|
+
resolve({
|
|
121
|
+
kind: "stopped",
|
|
122
|
+
containers: [],
|
|
123
|
+
detail: ""
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
107
127
|
resolve({
|
|
108
128
|
kind: "unavailable",
|
|
109
129
|
containers: [],
|
|
110
|
-
detail: failure.code === "ENOENT" ? "docker not installed" : shortReason(
|
|
130
|
+
detail: failure.code === "ENOENT" ? "docker not installed" : shortReason(raw)
|
|
111
131
|
});
|
|
112
132
|
});
|
|
113
133
|
});
|
package/dist/logs.js
CHANGED
|
@@ -4,12 +4,10 @@ const TAIL = 200;
|
|
|
4
4
|
const TIMEOUT_MS = 4000;
|
|
5
5
|
const MAX_BUFFER = 4 * 1024 * 1024;
|
|
6
6
|
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
7
|
-
|
|
8
|
-
/** One source for both timestamp shapes, so the two patterns cannot drift apart. */
|
|
9
7
|
const STAMP = String.raw`\d{4}-\d{2}-\d{2}T\S+`;
|
|
10
8
|
const TIMESTAMP_ONLY = new RegExp(`^${STAMP}$`);
|
|
11
9
|
const TIMESTAMPED = new RegExp(`^(${STAMP})\\s+(.*)$`);
|
|
12
|
-
/** The terminal would run ANSI sequences
|
|
10
|
+
/** The terminal would run ANSI sequences as control codes, and a lone CR is a terminator, not an overwrite */
|
|
13
11
|
export function sanitize(raw) {
|
|
14
12
|
return raw.replace(ANSI, "").split(/\r\n|\r|\n/).map(line => line.trimEnd()).filter(line => line.length > 0);
|
|
15
13
|
}
|
|
@@ -20,7 +18,7 @@ function level(text) {
|
|
|
20
18
|
return "info";
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
/** docker prefixes every line with an RFC3339 timestamp when asked, so a
|
|
21
|
+
/** docker prefixes every line with an RFC3339 timestamp when asked, so a line splits into time and rest */
|
|
24
22
|
export function parseLogLines(raw) {
|
|
25
23
|
const lines = [];
|
|
26
24
|
for (const line of sanitize(raw)) {
|
|
@@ -44,7 +42,7 @@ export function parseLogLines(raw) {
|
|
|
44
42
|
return lines;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
/** docker interleaves the container's two streams, so the real order only survives in the timestamps
|
|
45
|
+
/** docker interleaves the container's two streams, so the real order only survives in the timestamps */
|
|
48
46
|
export function mergeLogLines(parts) {
|
|
49
47
|
const lines = parts.flatMap(part => parseLogLines(part));
|
|
50
48
|
return lines.sort((a, b) => (a.time || "").localeCompare(b.time || "")).slice(-TAIL);
|