dekit-cli 0.3.0 → 0.4.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/README.md +16 -0
- package/bin/dekit.ts +7 -2
- package/package.json +2 -2
- package/src/cli/add.test.ts +14 -12
- package/src/cli/init.test.ts +15 -17
- package/src/cli/init.ts +7 -5
- package/src/cli/ls.test.ts +1 -1
- package/src/cli/resolve.test.ts +3 -3
- package/src/cli/screenshot.test.ts +4 -4
- package/src/parser.test.ts +8 -8
- package/src/types.ts +1 -1
- package/src/vite-plugin-dekit.test.ts +21 -14
package/README.md
CHANGED
|
@@ -54,6 +54,22 @@ When the agent starts the preview server, open the browser to review. Right-clic
|
|
|
54
54
|
> The hero section $${home@.hero} needs more padding, and make the CTA button blue
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
**Initialize in an existing project:**
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
> Initialize a dekit design project in the design/ directory using the dashboard template
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The agent creates a `design/` subdirectory with all the scaffolding, without touching your existing code.
|
|
64
|
+
|
|
65
|
+
**Build reusable components:**
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
> Create a ui-card component with an image slot and body slot, then build a showcase page that uses it
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The agent creates Web Components with `<template>` + scoped CSS, registers them in `dekit.yaml`, and uses them in pages like `<ui-card>`.
|
|
72
|
+
|
|
57
73
|
**Add a page to an existing design:**
|
|
58
74
|
|
|
59
75
|
```
|
package/bin/dekit.ts
CHANGED
|
@@ -26,7 +26,12 @@ Run 'dekit <command> --help' for command-specific help.
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function findDefaultConfig(): string | undefined {
|
|
29
|
-
for (const name of [
|
|
29
|
+
for (const name of [
|
|
30
|
+
".dekit/dekit.yaml",
|
|
31
|
+
".dekit/dekit.yml",
|
|
32
|
+
"dekit.yaml",
|
|
33
|
+
"dekit.yml",
|
|
34
|
+
]) {
|
|
30
35
|
const p = resolve(name);
|
|
31
36
|
if (existsSync(p)) return p;
|
|
32
37
|
}
|
|
@@ -57,7 +62,7 @@ async function loadConfig(configArg?: string) {
|
|
|
57
62
|
const configPath = configArg ? resolve(configArg) : findDefaultConfig();
|
|
58
63
|
if (!configPath) {
|
|
59
64
|
console.error(
|
|
60
|
-
"Error: no dekit.yaml found
|
|
65
|
+
"Error: no .dekit/dekit.yaml found. Run 'dekit init' or use -c to specify a config file.\n"
|
|
61
66
|
);
|
|
62
67
|
process.exit(1);
|
|
63
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dekit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A code-first design toolkit for agents and humans — create, preview, and screenshot HTML/CSS designs",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dekit": "./bin/dekit.js"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"dev": "node --import tsx bin/dekit.ts serve -c example/dekit.yaml",
|
|
41
|
+
"dev": "node --import tsx bin/dekit.ts serve -c example/.dekit/dekit.yaml",
|
|
42
42
|
"dev:editor": "vite dev --config editor/vite.config.ts",
|
|
43
43
|
"build:editor": "vite build --config editor/vite.config.ts",
|
|
44
44
|
"test": "vitest run",
|
package/src/cli/add.test.ts
CHANGED
|
@@ -7,10 +7,12 @@ import { runAdd } from "./add.js";
|
|
|
7
7
|
import { parseDesignConfig } from "../parser.js";
|
|
8
8
|
|
|
9
9
|
let tempDir: string;
|
|
10
|
+
let dekitDir: string;
|
|
10
11
|
|
|
11
12
|
beforeEach(async () => {
|
|
12
13
|
tempDir = mkdtempSync(join(tmpdir(), "dekit-add-"));
|
|
13
14
|
await runInit([tempDir]);
|
|
15
|
+
dekitDir = join(tempDir, ".dekit");
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
afterEach(() => {
|
|
@@ -19,44 +21,44 @@ afterEach(() => {
|
|
|
19
21
|
|
|
20
22
|
describe("runAdd page", () => {
|
|
21
23
|
test("adds a new page with blank template", async () => {
|
|
22
|
-
const config = await parseDesignConfig(join(
|
|
24
|
+
const config = await parseDesignConfig(join(dekitDir, "dekit.yaml"));
|
|
23
25
|
await runAdd(config, ["page", "about"]);
|
|
24
26
|
|
|
25
|
-
expect(existsSync(join(
|
|
26
|
-
expect(existsSync(join(
|
|
27
|
+
expect(existsSync(join(dekitDir, "pages/about/about.html"))).toBe(true);
|
|
28
|
+
expect(existsSync(join(dekitDir, "pages/about/about.css"))).toBe(true);
|
|
27
29
|
|
|
28
|
-
const updatedYaml = readFileSync(join(
|
|
30
|
+
const updatedYaml = readFileSync(join(dekitDir, "dekit.yaml"), "utf-8");
|
|
29
31
|
expect(updatedYaml).toContain("about");
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
test("adds a page with hero template", async () => {
|
|
33
|
-
const config = await parseDesignConfig(join(
|
|
35
|
+
const config = await parseDesignConfig(join(dekitDir, "dekit.yaml"));
|
|
34
36
|
await runAdd(config, ["page", "landing", "--template", "hero"]);
|
|
35
37
|
|
|
36
|
-
const html = readFileSync(join(
|
|
38
|
+
const html = readFileSync(join(dekitDir, "pages/landing/landing.html"), "utf-8");
|
|
37
39
|
expect(html).toContain("hero");
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
test("rejects duplicate page name", async () => {
|
|
41
|
-
const config = await parseDesignConfig(join(
|
|
43
|
+
const config = await parseDesignConfig(join(dekitDir, "dekit.yaml"));
|
|
42
44
|
await expect(runAdd(config, ["page", "home"])).rejects.toThrow(/already exists/i);
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
describe("runAdd component", () => {
|
|
47
49
|
test("adds a new component", async () => {
|
|
48
|
-
const config = await parseDesignConfig(join(
|
|
50
|
+
const config = await parseDesignConfig(join(dekitDir, "dekit.yaml"));
|
|
49
51
|
await runAdd(config, ["component", "my-card"]);
|
|
50
52
|
|
|
51
|
-
expect(existsSync(join(
|
|
52
|
-
expect(existsSync(join(
|
|
53
|
+
expect(existsSync(join(dekitDir, "components/my-card/my-card.html"))).toBe(true);
|
|
54
|
+
expect(existsSync(join(dekitDir, "components/my-card/my-card.css"))).toBe(true);
|
|
53
55
|
|
|
54
|
-
const updatedYaml = readFileSync(join(
|
|
56
|
+
const updatedYaml = readFileSync(join(dekitDir, "dekit.yaml"), "utf-8");
|
|
55
57
|
expect(updatedYaml).toContain("my-card");
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
test("rejects component name without hyphen", async () => {
|
|
59
|
-
const config = await parseDesignConfig(join(
|
|
61
|
+
const config = await parseDesignConfig(join(dekitDir, "dekit.yaml"));
|
|
60
62
|
await expect(runAdd(config, ["component", "card"])).rejects.toThrow(/hyphen/i);
|
|
61
63
|
});
|
|
62
64
|
});
|
package/src/cli/init.test.ts
CHANGED
|
@@ -15,39 +15,37 @@ afterEach(() => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
describe("runInit", () => {
|
|
18
|
-
test("creates blank project in target directory", async () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
expect(existsSync(join(
|
|
23
|
-
expect(existsSync(join(
|
|
24
|
-
expect(existsSync(join(
|
|
25
|
-
expect(existsSync(join(
|
|
18
|
+
test("creates blank project in .dekit/ under target directory", async () => {
|
|
19
|
+
await runInit([tempDir]);
|
|
20
|
+
|
|
21
|
+
const dekitDir = join(tempDir, ".dekit");
|
|
22
|
+
expect(existsSync(join(dekitDir, "dekit.yaml"))).toBe(true);
|
|
23
|
+
expect(existsSync(join(dekitDir, "global.css"))).toBe(true);
|
|
24
|
+
expect(existsSync(join(dekitDir, "pages/home/home.html"))).toBe(true);
|
|
25
|
+
expect(existsSync(join(dekitDir, "pages/home/home.css"))).toBe(true);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test("creates
|
|
28
|
+
test("creates project in .dekit/ under current directory when no path given", async () => {
|
|
29
29
|
const origCwd = process.cwd();
|
|
30
30
|
process.chdir(tempDir);
|
|
31
31
|
try {
|
|
32
32
|
await runInit([]);
|
|
33
|
-
expect(existsSync(join(tempDir, "dekit.yaml"))).toBe(true);
|
|
33
|
+
expect(existsSync(join(tempDir, ".dekit/dekit.yaml"))).toBe(true);
|
|
34
34
|
} finally {
|
|
35
35
|
process.chdir(origCwd);
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("creates landing template when specified", async () => {
|
|
40
|
-
|
|
41
|
-
await runInit([target, "--template", "landing"]);
|
|
40
|
+
await runInit([tempDir, "--template", "landing"]);
|
|
42
41
|
|
|
43
|
-
const html = readFileSync(join(
|
|
42
|
+
const html = readFileSync(join(tempDir, ".dekit/pages/home/home.html"), "utf-8");
|
|
44
43
|
expect(html).toContain("hero");
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
test("refuses to init
|
|
48
|
-
|
|
49
|
-
await runInit([
|
|
50
|
-
await expect(runInit([target])).rejects.toThrow(/already exists/i);
|
|
46
|
+
test("refuses to init when .dekit/dekit.yaml already exists", async () => {
|
|
47
|
+
await runInit([tempDir]);
|
|
48
|
+
await expect(runInit([tempDir])).rejects.toThrow(/already exists/i);
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
test("lists templates when --template given without value", async () => {
|
package/src/cli/init.ts
CHANGED
|
@@ -30,8 +30,10 @@ export async function runInit(args: string[]) {
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const dekitDir = join(targetDir, ".dekit");
|
|
34
|
+
|
|
35
|
+
if (existsSync(join(dekitDir, "dekit.yaml"))) {
|
|
36
|
+
throw new Error(`dekit.yaml already exists in ${dekitDir}`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const templateDir = join(TEMPLATES_DIR, template);
|
|
@@ -39,8 +41,8 @@ export async function runInit(args: string[]) {
|
|
|
39
41
|
throw new Error(`Template directory not found: ${templateDir}`);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
mkdirSync(
|
|
43
|
-
cpSync(templateDir,
|
|
44
|
+
mkdirSync(dekitDir, { recursive: true });
|
|
45
|
+
cpSync(templateDir, dekitDir, { recursive: true });
|
|
44
46
|
|
|
45
|
-
console.log(`Created dekit project in ${
|
|
47
|
+
console.log(`Created dekit project in ${dekitDir} (template: ${template})`);
|
|
46
48
|
}
|
package/src/cli/ls.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ afterEach(() => {
|
|
|
19
19
|
|
|
20
20
|
describe("runLs", () => {
|
|
21
21
|
test("lists pages", async () => {
|
|
22
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
22
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
23
23
|
const spy = vi.spyOn(console, "log");
|
|
24
24
|
await runLs(config);
|
|
25
25
|
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
package/src/cli/resolve.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ afterEach(() => {
|
|
|
19
19
|
|
|
20
20
|
describe("runResolve", () => {
|
|
21
21
|
test("resolves page ref to file path", async () => {
|
|
22
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
22
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
23
23
|
const spy = vi.spyOn(console, "log");
|
|
24
24
|
await runResolve(config, ["$${home}"]);
|
|
25
25
|
expect(spy.mock.calls[0][0]).toContain("pages/home/home.html");
|
|
@@ -27,7 +27,7 @@ describe("runResolve", () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test("resolves element ref to file path with line range", async () => {
|
|
30
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
30
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
31
31
|
const spy = vi.spyOn(console, "log");
|
|
32
32
|
await runResolve(config, ["$${home@.hero}"]);
|
|
33
33
|
const output = spy.mock.calls[0][0] as string;
|
|
@@ -37,7 +37,7 @@ describe("runResolve", () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("rejects unknown page", async () => {
|
|
40
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
40
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
41
41
|
await expect(runResolve(config, ["$${nonexistent}"])).rejects.toThrow(/not found/i);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -27,13 +27,13 @@ afterEach(() => {
|
|
|
27
27
|
|
|
28
28
|
describe("runScreenshot", () => {
|
|
29
29
|
test.skipIf(!hasChrome)("takes a page screenshot", async () => {
|
|
30
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
30
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
31
31
|
await runScreenshot(config, ["$${home}"]);
|
|
32
32
|
expect(existsSync(join(config.baseDir, "screenshots/home.png"))).toBe(true);
|
|
33
33
|
}, 30000);
|
|
34
34
|
|
|
35
35
|
test.skipIf(!hasChrome)("takes element screenshot", async () => {
|
|
36
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
36
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
37
37
|
await runScreenshot(config, ["$${home@.hero}"]);
|
|
38
38
|
// Output filename sanitizes the selector
|
|
39
39
|
const screenshots = join(config.baseDir, "screenshots");
|
|
@@ -41,14 +41,14 @@ describe("runScreenshot", () => {
|
|
|
41
41
|
}, 30000);
|
|
42
42
|
|
|
43
43
|
test.skipIf(!hasChrome)("respects -o flag", async () => {
|
|
44
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
44
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
45
45
|
const outPath = join(tempDir, "my-shot.png");
|
|
46
46
|
await runScreenshot(config, ["$${home}", "-o", outPath]);
|
|
47
47
|
expect(existsSync(outPath)).toBe(true);
|
|
48
48
|
}, 30000);
|
|
49
49
|
|
|
50
50
|
test("rejects invalid ref format", async () => {
|
|
51
|
-
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
51
|
+
const config = await parseDesignConfig(join(tempDir, ".dekit/dekit.yaml"));
|
|
52
52
|
await expect(runScreenshot(config, ["invalid"])).rejects.toThrow();
|
|
53
53
|
});
|
|
54
54
|
});
|
package/src/parser.test.ts
CHANGED
|
@@ -2,22 +2,22 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { parseDesignConfig } from "./parser.js";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
const DESIGN_DIR = join(import.meta.dirname, "../example");
|
|
5
|
+
const DESIGN_DIR = join(import.meta.dirname, "../example/.dekit");
|
|
6
6
|
|
|
7
7
|
describe("parseDesignConfig", () => {
|
|
8
8
|
test("parses valid dekit.yaml", async () => {
|
|
9
9
|
const config = await parseDesignConfig(join(DESIGN_DIR, "dekit.yaml"));
|
|
10
10
|
expect(config.version).toBe(1.0);
|
|
11
11
|
expect(config.globalStyle).toBe("global.css");
|
|
12
|
-
expect(config.components["
|
|
13
|
-
template: "components/
|
|
14
|
-
style: "components/
|
|
12
|
+
expect(config.components["ui-card"]).toEqual({
|
|
13
|
+
template: "components/ui-card/ui-card.html",
|
|
14
|
+
style: "components/ui-card/ui-card.css",
|
|
15
15
|
});
|
|
16
|
-
expect(config.pages["
|
|
17
|
-
template: "pages/
|
|
18
|
-
style: "pages/
|
|
16
|
+
expect(config.pages["home"]).toEqual({
|
|
17
|
+
template: "pages/home/home.html",
|
|
18
|
+
style: "pages/home/home.css",
|
|
19
19
|
});
|
|
20
|
-
expect(config.pages["
|
|
20
|
+
expect(config.pages["showcase"]).toBeDefined();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
test("stores resolved base directory", async () => {
|
package/src/types.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface DesignConfig {
|
|
|
13
13
|
globalStyle: string;
|
|
14
14
|
components: Record<string, ComponentDef>;
|
|
15
15
|
pages: Record<string, PageDef>;
|
|
16
|
-
baseDir: string; // absolute path to directory
|
|
16
|
+
baseDir: string; // absolute path to .dekit/ directory (where dekit.yaml lives)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface PageRef {
|
|
@@ -3,21 +3,29 @@ import { assemblePageHtml } from "./vite-plugin-dekit.js";
|
|
|
3
3
|
import type { DesignConfig } from "./types.js";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
const DESIGN_DIR = join(import.meta.dirname, "../example");
|
|
6
|
+
const DESIGN_DIR = join(import.meta.dirname, "../example/.dekit");
|
|
7
7
|
|
|
8
8
|
const mockConfig: DesignConfig = {
|
|
9
9
|
version: 1.0,
|
|
10
10
|
globalStyle: "global.css",
|
|
11
11
|
components: {
|
|
12
|
-
"
|
|
13
|
-
template: "components/
|
|
14
|
-
style: "components/
|
|
12
|
+
"ui-card": {
|
|
13
|
+
template: "components/ui-card/ui-card.html",
|
|
14
|
+
style: "components/ui-card/ui-card.css",
|
|
15
|
+
},
|
|
16
|
+
"ui-button": {
|
|
17
|
+
template: "components/ui-button/ui-button.html",
|
|
18
|
+
style: "components/ui-button/ui-button.css",
|
|
15
19
|
},
|
|
16
20
|
},
|
|
17
21
|
pages: {
|
|
18
|
-
|
|
19
|
-
template: "pages/
|
|
20
|
-
style: "pages/
|
|
22
|
+
home: {
|
|
23
|
+
template: "pages/home/home.html",
|
|
24
|
+
style: "pages/home/home.css",
|
|
25
|
+
},
|
|
26
|
+
showcase: {
|
|
27
|
+
template: "pages/showcase/showcase.html",
|
|
28
|
+
style: "pages/showcase/showcase.css",
|
|
21
29
|
},
|
|
22
30
|
},
|
|
23
31
|
baseDir: DESIGN_DIR,
|
|
@@ -25,13 +33,12 @@ const mockConfig: DesignConfig = {
|
|
|
25
33
|
|
|
26
34
|
describe("assemblePageHtml", () => {
|
|
27
35
|
test("generates valid HTML for a page", async () => {
|
|
28
|
-
const html = await assemblePageHtml(mockConfig, "
|
|
36
|
+
const html = await assemblePageHtml(mockConfig, "home");
|
|
29
37
|
expect(html).toContain("<!DOCTYPE html>");
|
|
30
38
|
expect(html).toContain("global.css");
|
|
31
|
-
expect(html).toContain("pages/
|
|
32
|
-
expect(html).toContain("Welcome to Dekit");
|
|
39
|
+
expect(html).toContain("pages/home/home.css");
|
|
33
40
|
expect(html).toContain("customElements.define");
|
|
34
|
-
expect(html).toContain("
|
|
41
|
+
expect(html).toContain("ui-card");
|
|
35
42
|
});
|
|
36
43
|
|
|
37
44
|
test("throws for unknown page", async () => {
|
|
@@ -41,14 +48,14 @@ describe("assemblePageHtml", () => {
|
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
test("includes inspector script", async () => {
|
|
44
|
-
const html = await assemblePageHtml(mockConfig, "
|
|
51
|
+
const html = await assemblePageHtml(mockConfig, "home");
|
|
45
52
|
expect(html).toContain("postMessage");
|
|
46
53
|
expect(html).toContain("NODE_SELECTED");
|
|
47
54
|
});
|
|
48
55
|
|
|
49
56
|
test("extracts template inner content for component registration", async () => {
|
|
50
|
-
const html = await assemblePageHtml(mockConfig, "
|
|
51
|
-
expect(html).toContain("class=\"
|
|
57
|
+
const html = await assemblePageHtml(mockConfig, "showcase");
|
|
58
|
+
expect(html).toContain("class=\"card\"");
|
|
52
59
|
expect(html).toContain("<slot></slot>");
|
|
53
60
|
expect(html).not.toMatch(/innerHTML\s*=\s*`\s*<template>/);
|
|
54
61
|
});
|