opencode-docker-panel 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 same file the CLI would pick. */
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
- /** An unindented `name:` is top-level by construction, which is why a regex beats a YAML parser here. */
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, which would hand back the directory name as if it
9
- // were the project's. PowerShell and several editors write one, so strip it before matching.
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
28
  /**
15
- * Finds the agent directory's compose file and reports the project it declares, if any. The file is
16
- * read on every call because the call comes from opening a menu, not from the poll loop.
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 file
29
- // compose itself would use. Falling through would act on a stack the CLI ignores.
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: declaredProject(body) || basename(dirname(file))
53
+ project
34
54
  };
35
55
  }
36
56
  return null;
37
57
  }
38
58
 
39
59
  /**
40
- * The compose target for a container that is already part of the project, or null when the agent
41
- * directory's file belongs to somebody else. `up` runs build and command lines from that file, so
42
- * an uncertain match has to hide the button rather than point it at the wrong stack.
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, and neither a compose project nor a service name may contain one, so a plain split is enough. */
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 either as a unix timestamp in seconds or as a date with a trailing zone name, which Date.parse rejects. */
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(String(stderr || error.message))
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 from a log as control codes, and a lone CR is a line terminator, not an overwrite. */
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 log line splits into a time and the rest. */
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);