@intentius/chant-lexicon-docker 0.1.4 → 0.1.8
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/integrity.json +15 -15
- package/dist/manifest.json +1 -1
- package/package.json +7 -7
- package/src/codegen/docs-cli.ts +1 -1
- package/src/codegen/generate-cli.ts +1 -1
- package/src/codegen/generate.test.ts +3 -3
- package/src/codegen/naming.test.ts +1 -1
- package/src/codegen/package.ts +2 -5
- package/src/coverage.test.ts +1 -1
- package/src/default-labels.test.ts +1 -1
- package/src/import/generator.test.ts +1 -1
- package/src/import/parser.test.ts +2 -2
- package/src/import/roundtrip.test.ts +2 -2
- package/src/index.ts +1 -1
- package/src/interpolation.test.ts +1 -1
- package/src/lint/post-synth/post-synth.test.ts +1 -1
- package/src/lint/rules/rules.test.ts +1 -1
- package/src/list-artifacts.test.ts +141 -0
- package/src/list-artifacts.ts +164 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/hover.test.ts +1 -1
- package/src/package-cli.ts +2 -2
- package/src/plugin.test.ts +3 -3
- package/src/plugin.ts +12 -9
- package/src/serializer.test.ts +1 -1
- package/src/validate-cli.ts +2 -2
- package/src/validate.test.ts +1 -1
- package/src/variables.test.ts +1 -1
package/dist/integrity.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"algorithm": "
|
|
2
|
+
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
5
|
-
"meta.json": "
|
|
6
|
-
"types/index.d.ts": "
|
|
7
|
-
"rules/no-latest-tag.ts": "
|
|
8
|
-
"rules/
|
|
9
|
-
"rules/
|
|
10
|
-
"rules/
|
|
11
|
-
"rules/
|
|
12
|
-
"rules/
|
|
13
|
-
"rules/
|
|
14
|
-
"rules/
|
|
15
|
-
"skills/chant-docker.md": "
|
|
16
|
-
"skills/chant-docker-patterns.md": "
|
|
4
|
+
"manifest.json": "87023f1c5a044cffac469511ec4fa7f58262f6b74e2382dddca406d47eec1ed0",
|
|
5
|
+
"meta.json": "e39b7b9e67e980b070486b5cc6908b1b1bc16e3360c3356dd0923cf4db80133e",
|
|
6
|
+
"types/index.d.ts": "f0bb7df2a41a10f7cdb14869dd3b23d3a3f944f2b58ee953bf252f430e0cbbd9",
|
|
7
|
+
"rules/no-latest-tag.ts": "efd060453d8ca3d5b85c3972906c6650b6f63eb5f67cb6cdb0f16a41cad91228",
|
|
8
|
+
"rules/apt-no-recommends.ts": "579ea54435b1cbd59088994aaaf7b249a42064facd74b62163b302fe31e769e0",
|
|
9
|
+
"rules/docker-helpers.ts": "c76ea1294b630df086a23b24c0435c9daac8cdf21dd371a8aabf74e9984e6b13",
|
|
10
|
+
"rules/no-latest-image.ts": "3e49602822e0645173a961b9c0686b669e846957486781a881693dbe04790b88",
|
|
11
|
+
"rules/no-root-user.ts": "fb47adc8523e07a8307253dcffeeccd7528e655bf737ef7e313e76a3df2c9a2d",
|
|
12
|
+
"rules/prefer-copy.ts": "b422a77b94023f7356d6b6326b6552c459cfbd282468f38214e19a105a59b742",
|
|
13
|
+
"rules/ssh-port-exposed.ts": "1df86b498787cbcc5ac8995c7099956576d9d105b4da71b0c225790be585c4c9",
|
|
14
|
+
"rules/unused-volume.ts": "ffcfc731782a4443a2eb34b1c0f6230b094c18c6ad104e4b81074bb799a3ce40",
|
|
15
|
+
"skills/chant-docker.md": "3ff0708e3c76e245f202948949c768464563af2b60cc9aa2ca52f494ef8357f1",
|
|
16
|
+
"skills/chant-docker-patterns.md": "ad1b0196d8150b2df579a699ff107f604340c799c04f3460ebf65003e287b12a"
|
|
17
17
|
},
|
|
18
|
-
"composite": "
|
|
18
|
+
"composite": "ff82c863447932fee4a358399636de04932adb25894772f0e0fd03941ed152d6"
|
|
19
19
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-docker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Docker lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"./types": "./dist/types/index.d.ts"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"generate": "
|
|
41
|
-
"bundle": "
|
|
42
|
-
"validate": "
|
|
43
|
-
"docs": "
|
|
44
|
-
"prepack": "
|
|
40
|
+
"generate": "tsx src/codegen/generate-cli.ts",
|
|
41
|
+
"bundle": "tsx src/package-cli.ts",
|
|
42
|
+
"validate": "tsx src/validate-cli.ts",
|
|
43
|
+
"docs": "tsx src/codegen/docs-cli.ts",
|
|
44
|
+
"prepack": "npm run generate && npm run bundle && npm run validate"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@intentius/chant": "
|
|
47
|
+
"@intentius/chant": "*",
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
package/src/codegen/docs-cli.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { describe, test, expect,
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { generate, writeGeneratedFiles } from "./generate";
|
|
3
3
|
import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { tmpdir } from "os";
|
|
6
6
|
|
|
7
7
|
// Mock the network fetches so tests run offline and fast
|
|
8
|
-
mock
|
|
8
|
+
vi.mock("../spec/fetch-compose", () => ({
|
|
9
9
|
fetchComposeSpec: async () => Buffer.from("{}"),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
mock
|
|
12
|
+
vi.mock("../spec/fetch-engine", () => ({
|
|
13
13
|
fetchEngineApi: async () => Buffer.from("{}"),
|
|
14
14
|
}));
|
|
15
15
|
|
package/src/codegen/package.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Docker lexicon packaging — delegates to core packagePipeline.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createRequire } from "module";
|
|
6
5
|
import { readFileSync } from "fs";
|
|
7
|
-
|
|
6
|
+
import { dockerPlugin } from "../plugin";
|
|
8
7
|
import { join, dirname } from "path";
|
|
9
8
|
import { fileURLToPath } from "url";
|
|
10
9
|
import type { IntrinsicDef } from "@intentius/chant/lexicon";
|
|
@@ -52,9 +51,7 @@ export async function packageLexicon(opts: PackageOptions = {}): Promise<Package
|
|
|
52
51
|
|
|
53
52
|
srcDir: pkgDir,
|
|
54
53
|
|
|
55
|
-
collectSkills: () => {
|
|
56
|
-
const { dockerPlugin } = require("../plugin");
|
|
57
|
-
const skillDefs = dockerPlugin.skills?.() ?? [];
|
|
54
|
+
collectSkills: () => { const skillDefs = dockerPlugin.skills?.() ?? [];
|
|
58
55
|
return collectSkills(skillDefs);
|
|
59
56
|
},
|
|
60
57
|
|
package/src/coverage.test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { DockerParser, DockerfileParser } from "./parser";
|
|
5
5
|
|
|
6
6
|
const testdata = (file: string) =>
|
|
7
|
-
readFileSync(join(import.meta.
|
|
7
|
+
readFileSync(join(import.meta.dirname, "testdata", file), "utf8");
|
|
8
8
|
|
|
9
9
|
describe("DockerParser — services", () => {
|
|
10
10
|
test("image extracted correctly", () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { DockerParser, DockerfileParser } from "./parser";
|
|
5
5
|
import { DockerGenerator } from "./generator";
|
|
6
6
|
|
|
7
7
|
const testdata = (file: string) =>
|
|
8
|
-
readFileSync(join(import.meta.
|
|
8
|
+
readFileSync(join(import.meta.dirname, "testdata", file), "utf8");
|
|
9
9
|
|
|
10
10
|
describe("roundtrip: parse → generate", () => {
|
|
11
11
|
test("simple.yaml → Service + Volume constructors", () => {
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotation
|
|
|
17
17
|
export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels";
|
|
18
18
|
export type { DefaultLabels, DefaultAnnotations } from "./default-labels";
|
|
19
19
|
|
|
20
|
-
// Generated entities — populated by `
|
|
20
|
+
// Generated entities — populated by `npm run generate`
|
|
21
21
|
export * from "./generated/index";
|
|
22
22
|
|
|
23
23
|
// Composites (to be added in Tier 2)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const execMock = vi.fn();
|
|
4
|
+
vi.mock("node:child_process", async () => {
|
|
5
|
+
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
|
6
|
+
return { ...actual, exec: (cmd: string, cb: (err: Error | null, out: { stdout: string; stderr: string }) => void) => {
|
|
7
|
+
Promise.resolve(execMock(cmd)).then(
|
|
8
|
+
(out) => cb(null, out),
|
|
9
|
+
(err) => cb(err as Error, { stdout: "", stderr: "" }),
|
|
10
|
+
);
|
|
11
|
+
} };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { listArtifacts } = await import("./list-artifacts");
|
|
15
|
+
|
|
16
|
+
describe("docker listArtifacts", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
execMock.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("happy path: containers + images + networks all reported", async () => {
|
|
22
|
+
execMock.mockImplementation((cmd: string) => {
|
|
23
|
+
if (cmd.startsWith("docker ps")) {
|
|
24
|
+
return {
|
|
25
|
+
stdout: [
|
|
26
|
+
JSON.stringify({ Names: "web", ID: "abc123", Image: "nginx:latest", State: "running", Status: "Up 5 minutes" }),
|
|
27
|
+
JSON.stringify({ Names: "db", ID: "def456", Image: "postgres:16", State: "running", Status: "Up 2 hours" }),
|
|
28
|
+
].join("\n"),
|
|
29
|
+
stderr: "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (cmd.startsWith("docker image ls")) {
|
|
33
|
+
return {
|
|
34
|
+
stdout: [
|
|
35
|
+
JSON.stringify({ ID: "img1", Repository: "nginx", Tag: "latest", Size: "187MB", CreatedAt: "2026-04-01" }),
|
|
36
|
+
JSON.stringify({ ID: "img2", Repository: "postgres", Tag: "16", Size: "412MB", CreatedAt: "2026-03-20" }),
|
|
37
|
+
].join("\n"),
|
|
38
|
+
stderr: "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (cmd.startsWith("docker network ls")) {
|
|
42
|
+
return {
|
|
43
|
+
stdout: [
|
|
44
|
+
JSON.stringify({ ID: "net1", Name: "bridge", Driver: "bridge", Scope: "local" }),
|
|
45
|
+
].join("\n"),
|
|
46
|
+
stderr: "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`unexpected cmd: ${cmd}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
53
|
+
|
|
54
|
+
expect(Object.keys(result).sort()).toEqual([
|
|
55
|
+
"container/db",
|
|
56
|
+
"container/web",
|
|
57
|
+
"image/nginx:latest",
|
|
58
|
+
"image/postgres:16",
|
|
59
|
+
"network/bridge",
|
|
60
|
+
]);
|
|
61
|
+
expect(result["container/web"]).toMatchObject({
|
|
62
|
+
type: "Docker::Container",
|
|
63
|
+
physicalId: "abc123",
|
|
64
|
+
status: "running",
|
|
65
|
+
});
|
|
66
|
+
expect(result["image/nginx:latest"]).toMatchObject({ type: "Docker::Image", physicalId: "img1" });
|
|
67
|
+
expect(result["network/bridge"]).toMatchObject({ type: "Docker::Network", physicalId: "net1" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("daemon unreachable on all queries → returns {}", async () => {
|
|
71
|
+
execMock.mockImplementation(() => { throw new Error("Cannot connect to the Docker daemon"); });
|
|
72
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
73
|
+
expect(result).toEqual({});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("per-query failure isolation: containers fail → still get images + networks", async () => {
|
|
77
|
+
execMock.mockImplementation((cmd: string) => {
|
|
78
|
+
if (cmd.startsWith("docker ps")) {
|
|
79
|
+
throw new Error("ps failed");
|
|
80
|
+
}
|
|
81
|
+
if (cmd.startsWith("docker image ls")) {
|
|
82
|
+
return { stdout: JSON.stringify({ ID: "img1", Repository: "alpine", Tag: "3.20", Size: "8MB" }), stderr: "" };
|
|
83
|
+
}
|
|
84
|
+
if (cmd.startsWith("docker network ls")) {
|
|
85
|
+
return { stdout: JSON.stringify({ ID: "net1", Name: "host", Driver: "host", Scope: "local" }), stderr: "" };
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`unexpected cmd: ${cmd}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(result).filter((k) => k.startsWith("container/"))).toEqual([]);
|
|
93
|
+
expect(result["image/alpine:3.20"]).toBeDefined();
|
|
94
|
+
expect(result["network/host"]).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("container Status surfaces in attributes.fullStatus", async () => {
|
|
98
|
+
execMock.mockImplementation((cmd: string) => {
|
|
99
|
+
if (cmd.startsWith("docker ps")) {
|
|
100
|
+
return { stdout: JSON.stringify({ Names: "web", ID: "x", Image: "i", State: "running", Status: "Up 5 minutes" }), stderr: "" };
|
|
101
|
+
}
|
|
102
|
+
return { stdout: "", stderr: "" };
|
|
103
|
+
});
|
|
104
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
105
|
+
expect(result["container/web"].attributes).toMatchObject({ fullStatus: "Up 5 minutes" });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("dangling images (Repository=<none>) are skipped", async () => {
|
|
109
|
+
execMock.mockImplementation((cmd: string) => {
|
|
110
|
+
if (cmd.startsWith("docker image ls")) {
|
|
111
|
+
return {
|
|
112
|
+
stdout: [
|
|
113
|
+
JSON.stringify({ ID: "img1", Repository: "<none>", Tag: "<none>", Size: "8MB" }),
|
|
114
|
+
JSON.stringify({ ID: "img2", Repository: "nginx", Tag: "latest", Size: "187MB" }),
|
|
115
|
+
].join("\n"),
|
|
116
|
+
stderr: "",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { stdout: "", stderr: "" };
|
|
120
|
+
});
|
|
121
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
122
|
+
expect(Object.keys(result).filter((k) => k.startsWith("image/"))).toEqual(["image/nginx:latest"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("malformed NDJSON line is skipped, others still parsed", async () => {
|
|
126
|
+
execMock.mockImplementation((cmd: string) => {
|
|
127
|
+
if (cmd.startsWith("docker ps")) {
|
|
128
|
+
return {
|
|
129
|
+
stdout: [
|
|
130
|
+
"this is not json",
|
|
131
|
+
JSON.stringify({ Names: "web", ID: "x", State: "running", Status: "Up", Image: "i" }),
|
|
132
|
+
].join("\n"),
|
|
133
|
+
stderr: "",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { stdout: "", stderr: "" };
|
|
137
|
+
});
|
|
138
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
139
|
+
expect(result["container/web"]).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live introspection of a Docker host via three independent CLI queries.
|
|
3
|
+
*
|
|
4
|
+
* The Docker lexicon's chant entities describe Compose / Dockerfile
|
|
5
|
+
* authoring primitives. The runtime concept (running containers, local
|
|
6
|
+
* images, networks) is created by `docker compose up` / `docker run` /
|
|
7
|
+
* `docker network create` outside chant's entity model.
|
|
8
|
+
*
|
|
9
|
+
* docker ps --format '{{json .}}' → running containers
|
|
10
|
+
* docker image ls --format '{{json .}}' → local images
|
|
11
|
+
* docker network ls --format '{{json .}}' → networks
|
|
12
|
+
*
|
|
13
|
+
* Output is one JSON object per line (NDJSON), not a JSON array. Daemon
|
|
14
|
+
* unreachable on any query → that query returns nothing; other queries
|
|
15
|
+
* still proceed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { exec } from "node:child_process";
|
|
19
|
+
import { promisify } from "node:util";
|
|
20
|
+
import type { ArtifactMetadata } from "@intentius/chant/lexicon";
|
|
21
|
+
|
|
22
|
+
const execAsync = promisify(exec);
|
|
23
|
+
|
|
24
|
+
interface DockerContainer {
|
|
25
|
+
Names?: string;
|
|
26
|
+
ID?: string;
|
|
27
|
+
Image?: string;
|
|
28
|
+
Command?: string;
|
|
29
|
+
State?: string; // "running" | "exited" | "created" | ...
|
|
30
|
+
Status?: string; // "Up 5 minutes" | "Exited (0) 1 hour ago" | ...
|
|
31
|
+
Ports?: string;
|
|
32
|
+
Mounts?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DockerImage {
|
|
36
|
+
ID?: string;
|
|
37
|
+
Repository?: string;
|
|
38
|
+
Tag?: string;
|
|
39
|
+
CreatedAt?: string;
|
|
40
|
+
Size?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DockerNetwork {
|
|
44
|
+
ID?: string;
|
|
45
|
+
Name?: string;
|
|
46
|
+
Driver?: string;
|
|
47
|
+
Scope?: string;
|
|
48
|
+
CreatedAt?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseNdjson<T>(stdout: string): T[] {
|
|
52
|
+
const out: T[] = [];
|
|
53
|
+
for (const line of stdout.split("\n")) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed) continue;
|
|
56
|
+
try {
|
|
57
|
+
out.push(JSON.parse(trimmed));
|
|
58
|
+
} catch {
|
|
59
|
+
// skip malformed lines
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
|
66
|
+
const out: Record<string, unknown> = {};
|
|
67
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
68
|
+
if (v !== undefined && v !== "") out[k] = v;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function listContainers(): Promise<Record<string, ArtifactMetadata>> {
|
|
74
|
+
const result: Record<string, ArtifactMetadata> = {};
|
|
75
|
+
try {
|
|
76
|
+
const { stdout } = await execAsync("docker ps --format '{{json .}}'");
|
|
77
|
+
const containers = parseNdjson<DockerContainer>(stdout);
|
|
78
|
+
for (const c of containers) {
|
|
79
|
+
const name = c.Names;
|
|
80
|
+
if (!name) continue;
|
|
81
|
+
result[`container/${name}`] = {
|
|
82
|
+
type: "Docker::Container",
|
|
83
|
+
physicalId: c.ID,
|
|
84
|
+
status: c.State ?? c.Status ?? "PRESENT",
|
|
85
|
+
attributes: pruneUndefined({
|
|
86
|
+
image: c.Image,
|
|
87
|
+
command: c.Command,
|
|
88
|
+
ports: c.Ports,
|
|
89
|
+
mounts: c.Mounts,
|
|
90
|
+
fullStatus: c.Status,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Docker daemon unreachable — return empty, don't fail the lexicon
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function listImages(): Promise<Record<string, ArtifactMetadata>> {
|
|
101
|
+
const result: Record<string, ArtifactMetadata> = {};
|
|
102
|
+
try {
|
|
103
|
+
const { stdout } = await execAsync("docker image ls --format '{{json .}}'");
|
|
104
|
+
const images = parseNdjson<DockerImage>(stdout);
|
|
105
|
+
for (const img of images) {
|
|
106
|
+
if (!img.Repository || img.Repository === "<none>") continue;
|
|
107
|
+
const tag = img.Tag && img.Tag !== "<none>" ? img.Tag : "latest";
|
|
108
|
+
const key = `image/${img.Repository}:${tag}`;
|
|
109
|
+
result[key] = {
|
|
110
|
+
type: "Docker::Image",
|
|
111
|
+
physicalId: img.ID,
|
|
112
|
+
status: "PRESENT",
|
|
113
|
+
lastUpdated: img.CreatedAt,
|
|
114
|
+
attributes: pruneUndefined({
|
|
115
|
+
repository: img.Repository,
|
|
116
|
+
tag,
|
|
117
|
+
size: img.Size,
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Docker daemon unreachable
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function listNetworks(): Promise<Record<string, ArtifactMetadata>> {
|
|
128
|
+
const result: Record<string, ArtifactMetadata> = {};
|
|
129
|
+
try {
|
|
130
|
+
const { stdout } = await execAsync("docker network ls --format '{{json .}}'");
|
|
131
|
+
const networks = parseNdjson<DockerNetwork>(stdout);
|
|
132
|
+
for (const net of networks) {
|
|
133
|
+
const name = net.Name;
|
|
134
|
+
if (!name) continue;
|
|
135
|
+
result[`network/${name}`] = {
|
|
136
|
+
type: "Docker::Network",
|
|
137
|
+
physicalId: net.ID,
|
|
138
|
+
status: "PRESENT",
|
|
139
|
+
lastUpdated: net.CreatedAt,
|
|
140
|
+
attributes: pruneUndefined({
|
|
141
|
+
driver: net.Driver,
|
|
142
|
+
scope: net.Scope,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Docker daemon unreachable
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listArtifacts(_options: {
|
|
153
|
+
environment: string;
|
|
154
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
155
|
+
}): Promise<Record<string, ArtifactMetadata>> {
|
|
156
|
+
// Three independent queries — failure of one doesn't block the others.
|
|
157
|
+
const [containers, images, networks] = await Promise.all([
|
|
158
|
+
listContainers(),
|
|
159
|
+
listImages(),
|
|
160
|
+
listNetworks(),
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
return { ...containers, ...images, ...networks };
|
|
164
|
+
}
|
package/src/lsp/hover.test.ts
CHANGED
package/src/package-cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
* Thin entry point for `
|
|
3
|
+
* Thin entry point for `npm run bundle` in lexicon-docker.
|
|
4
4
|
*/
|
|
5
5
|
import { generate, writeGeneratedFiles } from "./codegen/generate";
|
|
6
6
|
import { packageLexicon } from "./codegen/package";
|
package/src/plugin.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { dockerPlugin } from "./plugin";
|
|
3
3
|
|
|
4
4
|
describe("dockerPlugin", () => {
|
|
@@ -105,13 +105,13 @@ describe("dockerPlugin", () => {
|
|
|
105
105
|
test("provides MCP tools", () => {
|
|
106
106
|
const tools = dockerPlugin.mcpTools!();
|
|
107
107
|
expect(tools.length).toBe(1);
|
|
108
|
-
expect(tools[0].name).toBe("diff");
|
|
108
|
+
expect(tools[0].name).toBe("docker:diff");
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test("provides MCP resources", () => {
|
|
112
112
|
const resources = dockerPlugin.mcpResources!();
|
|
113
113
|
expect(resources.length).toBe(2);
|
|
114
|
-
expect(resources[0].uri).toBe("resource-catalog");
|
|
114
|
+
expect(resources[0].uri).toBe("docker:resource-catalog");
|
|
115
115
|
expect(resources[1].uri).toBe("examples/basic-app");
|
|
116
116
|
});
|
|
117
117
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -5,22 +5,24 @@
|
|
|
5
5
|
* and code generation for Docker Compose and Dockerfile resources.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createRequire } from "module";
|
|
9
8
|
import type { LexiconPlugin, IntrinsicDef, InitTemplateSet } from "@intentius/chant/lexicon";
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
9
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
12
10
|
import { discoverPostSynthChecks } from "@intentius/chant/lint/discover";
|
|
13
11
|
import { createSkillsLoader, createDiffTool, createCatalogResource } from "@intentius/chant/lexicon-plugin-helpers";
|
|
14
12
|
import { join, dirname } from "path";
|
|
15
13
|
import { fileURLToPath } from "url";
|
|
16
14
|
import { dockerSerializer } from "./serializer";
|
|
15
|
+
import { noLatestTagRule } from "./lint/rules/no-latest-tag";
|
|
16
|
+
import { dockerCompletions } from "./lsp/completions";
|
|
17
|
+
import { dockerHover } from "./lsp/hover";
|
|
18
|
+
import { DockerParser } from "./import/parser";
|
|
19
|
+
import { DockerGenerator } from "./import/generator";
|
|
17
20
|
|
|
18
21
|
export const dockerPlugin: LexiconPlugin = {
|
|
19
22
|
name: "docker",
|
|
20
23
|
serializer: dockerSerializer,
|
|
21
24
|
|
|
22
25
|
lintRules(): LintRule[] {
|
|
23
|
-
const { noLatestTagRule } = require("./lint/rules/no-latest-tag");
|
|
24
26
|
return [noLatestTagRule];
|
|
25
27
|
},
|
|
26
28
|
|
|
@@ -94,22 +96,18 @@ export const api = new Service({
|
|
|
94
96
|
},
|
|
95
97
|
|
|
96
98
|
completionProvider(ctx: import("@intentius/chant/lsp/types").CompletionContext) {
|
|
97
|
-
const { dockerCompletions } = require("./lsp/completions");
|
|
98
99
|
return dockerCompletions(ctx);
|
|
99
100
|
},
|
|
100
101
|
|
|
101
102
|
hoverProvider(ctx: import("@intentius/chant/lsp/types").HoverContext) {
|
|
102
|
-
const { dockerHover } = require("./lsp/hover");
|
|
103
103
|
return dockerHover(ctx);
|
|
104
104
|
},
|
|
105
105
|
|
|
106
106
|
templateParser() {
|
|
107
|
-
const { DockerParser } = require("./import/parser");
|
|
108
107
|
return new DockerParser();
|
|
109
108
|
},
|
|
110
109
|
|
|
111
110
|
templateGenerator() {
|
|
112
|
-
const { DockerGenerator } = require("./import/generator");
|
|
113
111
|
return new DockerGenerator();
|
|
114
112
|
},
|
|
115
113
|
|
|
@@ -161,12 +159,12 @@ export const api = new Service({
|
|
|
161
159
|
},
|
|
162
160
|
|
|
163
161
|
mcpTools() {
|
|
164
|
-
return [createDiffTool(dockerSerializer, "Compare current build output against previous output for Docker Compose")];
|
|
162
|
+
return [createDiffTool(dockerSerializer, "Compare current build output against previous output for Docker Compose", "docker")];
|
|
165
163
|
},
|
|
166
164
|
|
|
167
165
|
mcpResources() {
|
|
168
166
|
return [
|
|
169
|
-
createCatalogResource(import.meta.url, "Docker Entity Catalog", "JSON list of all supported Docker entity types", "lexicon-docker.json"),
|
|
167
|
+
createCatalogResource(import.meta.url, "Docker Entity Catalog", "JSON list of all supported Docker entity types", "lexicon-docker.json", "docker"),
|
|
170
168
|
{
|
|
171
169
|
uri: "examples/basic-app",
|
|
172
170
|
name: "Basic App Example",
|
|
@@ -247,4 +245,9 @@ export const api = new Service({
|
|
|
247
245
|
],
|
|
248
246
|
},
|
|
249
247
|
]),
|
|
248
|
+
|
|
249
|
+
async listArtifacts(options) {
|
|
250
|
+
const { listArtifacts } = await import("./list-artifacts");
|
|
251
|
+
return listArtifacts(options);
|
|
252
|
+
},
|
|
250
253
|
};
|
package/src/serializer.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { dockerSerializer } from "./serializer";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
4
4
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
package/src/validate-cli.ts
CHANGED
package/src/validate.test.ts
CHANGED
package/src/variables.test.ts
CHANGED