dekit-cli 0.1.0 → 0.2.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 +24 -1
- package/bin/dekit.ts +98 -34
- package/dist/editor/assets/{index-DK9iczjI.js → index-yt1AtkL3.js} +6 -6
- package/dist/editor/index.html +1 -1
- package/docs/usage.md +127 -0
- package/editor/src/App.tsx +9 -11
- package/editor/src/types.ts +1 -0
- package/package.json +34 -9
- package/src/chrome.test.ts +24 -0
- package/src/chrome.ts +37 -0
- package/src/cli/add.test.ts +62 -0
- package/src/cli/add.ts +104 -0
- package/src/cli/init.test.ts +61 -0
- package/src/cli/init.ts +46 -0
- package/src/cli/ls.test.ts +29 -0
- package/src/cli/ls.ts +24 -0
- package/src/cli/resolve.test.ts +43 -0
- package/src/cli/resolve.ts +92 -0
- package/src/cli/screenshot.test.ts +54 -0
- package/src/cli/screenshot.ts +154 -0
- package/src/cli/usage.ts +8 -0
- package/src/devices.ts +23 -0
- package/src/injected/inspector.ts +31 -0
- package/src/parser.test.ts +6 -6
- package/src/parser.ts +2 -2
- package/src/ref.test.ts +68 -0
- package/src/ref.ts +42 -0
- package/src/server.ts +8 -5
- package/src/types.ts +13 -0
- package/src/vite-plugin-dekit.ts +13 -6
- package/templates/components/blank/component.css +3 -0
- package/templates/components/blank/component.html +5 -0
- package/templates/pages/blank/page.css +3 -0
- package/templates/pages/blank/page.html +3 -0
- package/templates/pages/form/page.css +88 -0
- package/templates/pages/form/page.html +21 -0
- package/templates/pages/grid/page.css +84 -0
- package/templates/pages/grid/page.html +40 -0
- package/templates/pages/hero/page.css +62 -0
- package/templates/pages/hero/page.html +10 -0
- package/templates/projects/blank/dekit.yaml +9 -0
- package/templates/projects/blank/global.css +18 -0
- package/templates/projects/blank/pages/home/home.css +3 -0
- package/templates/projects/blank/pages/home/home.html +3 -0
- package/templates/projects/dashboard/dekit.yaml +9 -0
- package/templates/projects/dashboard/global.css +18 -0
- package/templates/projects/dashboard/pages/home/home.css +248 -0
- package/templates/projects/dashboard/pages/home/home.html +100 -0
- package/templates/projects/landing/dekit.yaml +9 -0
- package/templates/projects/landing/global.css +18 -0
- package/templates/projects/landing/pages/home/home.css +255 -0
- package/templates/projects/landing/pages/home/home.html +79 -0
- package/templates/projects/mobile/dekit.yaml +9 -0
- package/templates/projects/mobile/global.css +20 -0
- package/templates/projects/mobile/pages/home/home.css +267 -0
- package/templates/projects/mobile/pages/home/home.html +110 -0
package/dist/editor/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Dekit Editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-yt1AtkL3.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Bx5YsS46.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/docs/usage.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Dekit — Agent Usage Guide
|
|
2
|
+
|
|
3
|
+
Dekit is a design canvas for code agents. You write HTML/CSS, dekit renders and screenshots it.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
- **Page**: An HTML file + CSS file registered in `dekit.yaml`
|
|
8
|
+
- **Component**: A reusable Web Component (custom element with shadow DOM)
|
|
9
|
+
- **Ref**: A reference to a page or element: `$${page}` or `$${page@selector}`
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Initialize a new design project
|
|
15
|
+
dekit init my-design --template landing
|
|
16
|
+
|
|
17
|
+
# Preview in browser
|
|
18
|
+
cd my-design
|
|
19
|
+
dekit serve
|
|
20
|
+
|
|
21
|
+
# Take a screenshot
|
|
22
|
+
dekit screenshot '$${home}'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## CLI Reference
|
|
26
|
+
|
|
27
|
+
### dekit init [path] [--template name]
|
|
28
|
+
|
|
29
|
+
Initialize a design project. Templates: `blank`, `landing`, `dashboard`, `mobile`.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
dekit init # In current directory, blank template
|
|
33
|
+
dekit init design/ # In subdirectory
|
|
34
|
+
dekit init --template landing # With template
|
|
35
|
+
dekit init --template # List available templates
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### dekit add page <name> [--template name]
|
|
39
|
+
|
|
40
|
+
Add a page. Templates: `blank`, `hero`, `form`, `grid`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dekit add page about
|
|
44
|
+
dekit add page pricing --template grid
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### dekit add component <name>
|
|
48
|
+
|
|
49
|
+
Add a Web Component (name must contain a hyphen).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dekit add component my-card
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### dekit ls
|
|
56
|
+
|
|
57
|
+
List all pages and components.
|
|
58
|
+
|
|
59
|
+
### dekit serve [-p port] [--no-open]
|
|
60
|
+
|
|
61
|
+
Start the interactive preview server.
|
|
62
|
+
|
|
63
|
+
### dekit screenshot <ref> [options]
|
|
64
|
+
|
|
65
|
+
Take a screenshot. Output path is printed to stdout.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
dekit screenshot '$${home}' # Full page
|
|
69
|
+
dekit screenshot '$${home@.hero}' # Element only
|
|
70
|
+
dekit screenshot '$${home}' --device iphone-14 # Device size
|
|
71
|
+
dekit screenshot '$${home}' --full-page # Include scroll
|
|
72
|
+
dekit screenshot '$${home}' -o my-shot.png # Custom output
|
|
73
|
+
dekit screenshot --all # All pages
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### dekit resolve <ref>
|
|
77
|
+
|
|
78
|
+
Resolve a ref to source file path and line range.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
dekit resolve '$${home}'
|
|
82
|
+
# → pages/home/home.html
|
|
83
|
+
|
|
84
|
+
dekit resolve '$${home@.hero}'
|
|
85
|
+
# → pages/home/home.html:1-10
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### dekit usage
|
|
89
|
+
|
|
90
|
+
Print this guide.
|
|
91
|
+
|
|
92
|
+
## Ref Protocol
|
|
93
|
+
|
|
94
|
+
Format: `$${page-key}` or `$${page-key@css-selector}`
|
|
95
|
+
|
|
96
|
+
When you see a `$${...}` pattern, pass it to `dekit resolve` or `dekit screenshot`.
|
|
97
|
+
|
|
98
|
+
- `dekit resolve <ref>` → file path + line range
|
|
99
|
+
- `dekit screenshot <ref>` → screenshot file path
|
|
100
|
+
|
|
101
|
+
## Design Loop
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
1. dekit init --template landing # Start project
|
|
105
|
+
2. Edit HTML/CSS files # Make changes
|
|
106
|
+
3. dekit screenshot '$${home}' # See result
|
|
107
|
+
4. Analyze the screenshot # Check quality
|
|
108
|
+
5. Edit again # Fix issues
|
|
109
|
+
6. Repeat 3-5 # Until satisfied
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## dekit.yaml Format
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
version: 1.0
|
|
116
|
+
global-style: "global.css"
|
|
117
|
+
|
|
118
|
+
components:
|
|
119
|
+
my-card:
|
|
120
|
+
template: "components/my-card/my-card.html"
|
|
121
|
+
style: "components/my-card/my-card.css"
|
|
122
|
+
|
|
123
|
+
pages:
|
|
124
|
+
home:
|
|
125
|
+
template: "pages/home/home.html"
|
|
126
|
+
style: "pages/home/home.css"
|
|
127
|
+
```
|
package/editor/src/App.tsx
CHANGED
|
@@ -33,20 +33,18 @@ export function App() {
|
|
|
33
33
|
const currentPageRef = useRef("");
|
|
34
34
|
const canvasAreaRef = useRef<HTMLDivElement>(null);
|
|
35
35
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
|
36
|
-
const [contextMenu, setContextMenu] = useState<{ x: number; y: number;
|
|
36
|
+
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; cssSelector?: string; page: string } | null>(null);
|
|
37
37
|
const [focusedPanel, setFocusedPanel] = useState<"pages" | "layers" | null>(null);
|
|
38
38
|
|
|
39
39
|
const configRef = useRef<EditorConfig | null>(null);
|
|
40
40
|
configRef.current = config;
|
|
41
41
|
|
|
42
42
|
const getPageRef = useCallback((page: string) => {
|
|
43
|
-
|
|
44
|
-
return "$$" + "{" + filePath + "}";
|
|
43
|
+
return "$$" + "{" + page + "}";
|
|
45
44
|
}, []);
|
|
46
45
|
|
|
47
|
-
const getNodeRef = useCallback((page: string,
|
|
48
|
-
|
|
49
|
-
return "$$" + "{" + filePath + "#" + domPath + "}";
|
|
46
|
+
const getNodeRef = useCallback((page: string, cssSelector: string) => {
|
|
47
|
+
return "$$" + "{" + page + "@" + cssSelector + "}";
|
|
50
48
|
}, []);
|
|
51
49
|
|
|
52
50
|
// Fetch config
|
|
@@ -70,7 +68,7 @@ export function App() {
|
|
|
70
68
|
setSelectedNode(msg.node);
|
|
71
69
|
setSelectedPath(msg.node.path);
|
|
72
70
|
if (inspectModeRef.current && currentPageRef.current) {
|
|
73
|
-
navigator.clipboard.writeText(getNodeRef(currentPageRef.current, msg.node.
|
|
71
|
+
navigator.clipboard.writeText(getNodeRef(currentPageRef.current, msg.node.cssSelector));
|
|
74
72
|
iframeRef.current?.contentWindow?.postMessage({ type: "COPY_FEEDBACK" }, "*");
|
|
75
73
|
}
|
|
76
74
|
} else if (msg.type === "DOM_TREE") {
|
|
@@ -372,7 +370,7 @@ export function App() {
|
|
|
372
370
|
setFocusedPanel("layers");
|
|
373
371
|
}}
|
|
374
372
|
onHover={handleLayerHover}
|
|
375
|
-
onContextMenu={(
|
|
373
|
+
onContextMenu={(cssSelector, x, y) => setContextMenu({ x, y, cssSelector, page: currentPage })}
|
|
376
374
|
/>
|
|
377
375
|
))}
|
|
378
376
|
</div>
|
|
@@ -382,7 +380,7 @@ export function App() {
|
|
|
382
380
|
<div className="panel-header">Properties</div>
|
|
383
381
|
<OverlayScrollbarsComponent className="properties-scroll" defer>
|
|
384
382
|
{selectedNode ? (
|
|
385
|
-
<PropertyPanel node={selectedNode} nodeRef={getNodeRef(currentPage, selectedNode.
|
|
383
|
+
<PropertyPanel node={selectedNode} nodeRef={getNodeRef(currentPage, selectedNode.cssSelector)} />
|
|
386
384
|
) : (
|
|
387
385
|
<div className="empty-state">Select an element to inspect</div>
|
|
388
386
|
)}
|
|
@@ -396,11 +394,11 @@ export function App() {
|
|
|
396
394
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
|
397
395
|
onClick={(e) => e.stopPropagation()}
|
|
398
396
|
>
|
|
399
|
-
{contextMenu.
|
|
397
|
+
{contextMenu.cssSelector && (
|
|
400
398
|
<div
|
|
401
399
|
className="context-menu-item"
|
|
402
400
|
onClick={() => {
|
|
403
|
-
navigator.clipboard.writeText(getNodeRef(contextMenu.page, contextMenu.
|
|
401
|
+
navigator.clipboard.writeText(getNodeRef(contextMenu.page, contextMenu.cssSelector!));
|
|
404
402
|
setContextMenu(null);
|
|
405
403
|
}}
|
|
406
404
|
>
|
package/editor/src/types.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dekit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A code-first design toolkit —
|
|
6
|
-
"keywords": [
|
|
5
|
+
"description": "A code-first design toolkit for agents and humans — create, preview, and screenshot HTML/CSS designs",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"design",
|
|
8
|
+
"devtools",
|
|
9
|
+
"inspector",
|
|
10
|
+
"preview",
|
|
11
|
+
"css",
|
|
12
|
+
"html",
|
|
13
|
+
"cli",
|
|
14
|
+
"agent",
|
|
15
|
+
"ai"
|
|
16
|
+
],
|
|
7
17
|
"license": "Apache-2.0",
|
|
8
|
-
"repository": {
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/myWsq/dekit"
|
|
21
|
+
},
|
|
9
22
|
"homepage": "https://github.com/myWsq/dekit",
|
|
10
|
-
"bugs": {
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/myWsq/dekit/issues"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"src/",
|
|
29
|
+
"editor/",
|
|
30
|
+
"dist/",
|
|
31
|
+
"templates/",
|
|
32
|
+
"docs/usage.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
13
37
|
"bin": {
|
|
14
38
|
"dekit": "./bin/dekit.ts"
|
|
15
39
|
},
|
|
@@ -24,6 +48,7 @@
|
|
|
24
48
|
"open": "^10.1.0",
|
|
25
49
|
"overlayscrollbars": "^2.14.0",
|
|
26
50
|
"overlayscrollbars-react": "^0.5.6",
|
|
51
|
+
"puppeteer-core": "^24.40.0",
|
|
27
52
|
"react-icons": "^5.6.0",
|
|
28
53
|
"sirv": "^3.0.0",
|
|
29
54
|
"vite": "^6.2.0"
|
|
@@ -32,11 +57,11 @@
|
|
|
32
57
|
"@types/js-yaml": "^4.0.9",
|
|
33
58
|
"@types/react": "^19.0.0",
|
|
34
59
|
"@types/react-dom": "^19.0.0",
|
|
60
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
35
61
|
"react": "^19.0.0",
|
|
36
62
|
"react-dom": "^19.0.0",
|
|
37
63
|
"tsx": "^4.19.0",
|
|
38
64
|
"typescript": "^5.7.0",
|
|
39
|
-
"vitest": "^3.0.0"
|
|
40
|
-
"@vitejs/plugin-react": "^4.3.0"
|
|
65
|
+
"vitest": "^3.0.0"
|
|
41
66
|
}
|
|
42
67
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, test, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { findChrome } from "./chrome.js";
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.unstubAllEnvs();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe("findChrome", () => {
|
|
9
|
+
test("returns CHROME_PATH if set and exists", async () => {
|
|
10
|
+
vi.stubEnv("CHROME_PATH", process.execPath);
|
|
11
|
+
const result = await findChrome();
|
|
12
|
+
expect(result).toBe(process.execPath);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns a string on systems with Chrome installed", async () => {
|
|
16
|
+
vi.stubEnv("CHROME_PATH", "");
|
|
17
|
+
try {
|
|
18
|
+
const result = await findChrome();
|
|
19
|
+
expect(typeof result).toBe("string");
|
|
20
|
+
} catch (err) {
|
|
21
|
+
expect((err as Error).message).toContain("Chrome not found");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/chrome.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
|
|
4
|
+
const CHROME_PATHS: Record<string, string[]> = {
|
|
5
|
+
darwin: [
|
|
6
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
7
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
8
|
+
],
|
|
9
|
+
linux: [
|
|
10
|
+
"/usr/bin/google-chrome",
|
|
11
|
+
"/usr/bin/google-chrome-stable",
|
|
12
|
+
"/usr/bin/chromium-browser",
|
|
13
|
+
"/usr/bin/chromium",
|
|
14
|
+
],
|
|
15
|
+
win32: [
|
|
16
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
17
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function findChrome(): Promise<string> {
|
|
22
|
+
const envPath = process.env.CHROME_PATH;
|
|
23
|
+
if (envPath && existsSync(envPath)) {
|
|
24
|
+
return envPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const candidates = CHROME_PATHS[platform()] ?? [];
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
if (existsSync(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Chrome not found. Install Chrome or set CHROME_PATH environment variable."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { runInit } from "./init.js";
|
|
6
|
+
import { runAdd } from "./add.js";
|
|
7
|
+
import { parseDesignConfig } from "../parser.js";
|
|
8
|
+
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tempDir = mkdtempSync(join(tmpdir(), "dekit-add-"));
|
|
13
|
+
await runInit([tempDir]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("runAdd page", () => {
|
|
21
|
+
test("adds a new page with blank template", async () => {
|
|
22
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
23
|
+
await runAdd(config, ["page", "about"]);
|
|
24
|
+
|
|
25
|
+
expect(existsSync(join(tempDir, "pages/about/about.html"))).toBe(true);
|
|
26
|
+
expect(existsSync(join(tempDir, "pages/about/about.css"))).toBe(true);
|
|
27
|
+
|
|
28
|
+
const updatedYaml = readFileSync(join(tempDir, "dekit.yaml"), "utf-8");
|
|
29
|
+
expect(updatedYaml).toContain("about");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("adds a page with hero template", async () => {
|
|
33
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
34
|
+
await runAdd(config, ["page", "landing", "--template", "hero"]);
|
|
35
|
+
|
|
36
|
+
const html = readFileSync(join(tempDir, "pages/landing/landing.html"), "utf-8");
|
|
37
|
+
expect(html).toContain("hero");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("rejects duplicate page name", async () => {
|
|
41
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
42
|
+
await expect(runAdd(config, ["page", "home"])).rejects.toThrow(/already exists/i);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("runAdd component", () => {
|
|
47
|
+
test("adds a new component", async () => {
|
|
48
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
49
|
+
await runAdd(config, ["component", "my-card"]);
|
|
50
|
+
|
|
51
|
+
expect(existsSync(join(tempDir, "components/my-card/my-card.html"))).toBe(true);
|
|
52
|
+
expect(existsSync(join(tempDir, "components/my-card/my-card.css"))).toBe(true);
|
|
53
|
+
|
|
54
|
+
const updatedYaml = readFileSync(join(tempDir, "dekit.yaml"), "utf-8");
|
|
55
|
+
expect(updatedYaml).toContain("my-card");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("rejects component name without hyphen", async () => {
|
|
59
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
60
|
+
await expect(runAdd(config, ["component", "card"])).rejects.toThrow(/hyphen/i);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/cli/add.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import type { DesignConfig } from "../types.js";
|
|
5
|
+
|
|
6
|
+
const PAGE_TEMPLATES_DIR = join(import.meta.dirname, "../../templates/pages");
|
|
7
|
+
const COMPONENT_TEMPLATES_DIR = join(import.meta.dirname, "../../templates/components");
|
|
8
|
+
|
|
9
|
+
const AVAILABLE_PAGE_TEMPLATES = ["blank", "hero", "form", "grid"];
|
|
10
|
+
|
|
11
|
+
export async function runAdd(config: DesignConfig, args: string[]) {
|
|
12
|
+
const type = args[0];
|
|
13
|
+
const name = args[1];
|
|
14
|
+
|
|
15
|
+
if (!type || !name) {
|
|
16
|
+
console.error("Usage: dekit add <page|component> <name> [--template <template>]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let template = "blank";
|
|
21
|
+
const templateIdx = args.indexOf("--template");
|
|
22
|
+
if (templateIdx !== -1 && args[templateIdx + 1]) {
|
|
23
|
+
template = args[templateIdx + 1];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (type === "page") {
|
|
27
|
+
await addPage(config, name, template);
|
|
28
|
+
} else if (type === "component") {
|
|
29
|
+
await addComponent(config, name);
|
|
30
|
+
} else {
|
|
31
|
+
console.error(`Unknown type: "${type}". Use "page" or "component".`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function addPage(config: DesignConfig, name: string, template: string) {
|
|
37
|
+
if (config.pages[name]) {
|
|
38
|
+
throw new Error(`Page "${name}" already exists`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!AVAILABLE_PAGE_TEMPLATES.includes(template)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unknown page template: "${template}". Available: ${AVAILABLE_PAGE_TEMPLATES.join(", ")}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pageDir = join(config.baseDir, "pages", name);
|
|
48
|
+
mkdirSync(pageDir, { recursive: true });
|
|
49
|
+
|
|
50
|
+
const templateDir = join(PAGE_TEMPLATES_DIR, template);
|
|
51
|
+
const htmlSrc = readFileSync(join(templateDir, "page.html"), "utf-8");
|
|
52
|
+
const cssSrc = readFileSync(join(templateDir, "page.css"), "utf-8");
|
|
53
|
+
|
|
54
|
+
writeFileSync(join(pageDir, `${name}.html`), htmlSrc.replace(/__PAGE_NAME__/g, name));
|
|
55
|
+
writeFileSync(join(pageDir, `${name}.css`), cssSrc);
|
|
56
|
+
|
|
57
|
+
// Update dekit.yaml
|
|
58
|
+
const configPath = join(config.baseDir, "dekit.yaml");
|
|
59
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
60
|
+
const pages = (raw.pages ?? {}) as Record<string, unknown>;
|
|
61
|
+
pages[name] = {
|
|
62
|
+
template: `pages/${name}/${name}.html`,
|
|
63
|
+
style: `pages/${name}/${name}.css`,
|
|
64
|
+
};
|
|
65
|
+
raw.pages = pages;
|
|
66
|
+
writeFileSync(configPath, yaml.dump(raw, { lineWidth: -1 }));
|
|
67
|
+
|
|
68
|
+
console.log(`Added page "${name}" (template: ${template})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function addComponent(config: DesignConfig, name: string) {
|
|
72
|
+
if (!name.includes("-")) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Component name "${name}" must contain a hyphen (Web Components spec)`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config.components[name]) {
|
|
79
|
+
throw new Error(`Component "${name}" already exists`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const compDir = join(config.baseDir, "components", name);
|
|
83
|
+
mkdirSync(compDir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const templateDir = join(COMPONENT_TEMPLATES_DIR, "blank");
|
|
86
|
+
const htmlSrc = readFileSync(join(templateDir, "component.html"), "utf-8");
|
|
87
|
+
const cssSrc = readFileSync(join(templateDir, "component.css"), "utf-8");
|
|
88
|
+
|
|
89
|
+
writeFileSync(join(compDir, `${name}.html`), htmlSrc);
|
|
90
|
+
writeFileSync(join(compDir, `${name}.css`), cssSrc);
|
|
91
|
+
|
|
92
|
+
// Update dekit.yaml
|
|
93
|
+
const configPath = join(config.baseDir, "dekit.yaml");
|
|
94
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
95
|
+
const components = (raw.components ?? {}) as Record<string, unknown>;
|
|
96
|
+
components[name] = {
|
|
97
|
+
template: `components/${name}/${name}.html`,
|
|
98
|
+
style: `components/${name}/${name}.css`,
|
|
99
|
+
};
|
|
100
|
+
raw.components = components;
|
|
101
|
+
writeFileSync(configPath, yaml.dump(raw, { lineWidth: -1 }));
|
|
102
|
+
|
|
103
|
+
console.log(`Added component "${name}"`);
|
|
104
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { runInit } from "./init.js";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempDir = mkdtempSync(join(tmpdir(), "dekit-init-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("runInit", () => {
|
|
18
|
+
test("creates blank project in target directory", async () => {
|
|
19
|
+
const target = join(tempDir, "my-design");
|
|
20
|
+
await runInit([target]);
|
|
21
|
+
|
|
22
|
+
expect(existsSync(join(target, "dekit.yaml"))).toBe(true);
|
|
23
|
+
expect(existsSync(join(target, "global.css"))).toBe(true);
|
|
24
|
+
expect(existsSync(join(target, "pages/home/home.html"))).toBe(true);
|
|
25
|
+
expect(existsSync(join(target, "pages/home/home.css"))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("creates blank project in current directory when no path given", async () => {
|
|
29
|
+
const origCwd = process.cwd();
|
|
30
|
+
process.chdir(tempDir);
|
|
31
|
+
try {
|
|
32
|
+
await runInit([]);
|
|
33
|
+
expect(existsSync(join(tempDir, "dekit.yaml"))).toBe(true);
|
|
34
|
+
} finally {
|
|
35
|
+
process.chdir(origCwd);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("creates landing template when specified", async () => {
|
|
40
|
+
const target = join(tempDir, "landing");
|
|
41
|
+
await runInit([target, "--template", "landing"]);
|
|
42
|
+
|
|
43
|
+
const html = readFileSync(join(target, "pages/home/home.html"), "utf-8");
|
|
44
|
+
expect(html).toContain("hero");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("refuses to init in directory with existing dekit.yaml", async () => {
|
|
48
|
+
const target = join(tempDir, "existing");
|
|
49
|
+
await runInit([target]);
|
|
50
|
+
await expect(runInit([target])).rejects.toThrow(/already exists/i);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("lists templates when --template given without value", async () => {
|
|
54
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
55
|
+
await runInit(["--template"]);
|
|
56
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
57
|
+
expect.stringContaining("blank")
|
|
58
|
+
);
|
|
59
|
+
consoleSpy.mockRestore();
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, cpSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const TEMPLATES_DIR = join(import.meta.dirname, "../../templates/projects");
|
|
5
|
+
|
|
6
|
+
const AVAILABLE_TEMPLATES = ["blank", "landing", "dashboard", "mobile"];
|
|
7
|
+
|
|
8
|
+
export async function runInit(args: string[]) {
|
|
9
|
+
let targetDir = process.cwd();
|
|
10
|
+
let template = "blank";
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg === "--template") {
|
|
15
|
+
const next = args[i + 1];
|
|
16
|
+
if (!next || next.startsWith("-")) {
|
|
17
|
+
console.log("Available templates: " + AVAILABLE_TEMPLATES.join(", "));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
template = next;
|
|
21
|
+
i++;
|
|
22
|
+
} else if (!arg.startsWith("-")) {
|
|
23
|
+
targetDir = resolve(arg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!AVAILABLE_TEMPLATES.includes(template)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unknown template: "${template}". Available: ${AVAILABLE_TEMPLATES.join(", ")}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (existsSync(join(targetDir, "dekit.yaml"))) {
|
|
34
|
+
throw new Error(`dekit.yaml already exists in ${targetDir}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const templateDir = join(TEMPLATES_DIR, template);
|
|
38
|
+
if (!existsSync(templateDir)) {
|
|
39
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
mkdirSync(targetDir, { recursive: true });
|
|
43
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
console.log(`Created dekit project in ${targetDir} (template: ${template})`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { runInit } from "./init.js";
|
|
6
|
+
import { runLs } from "./ls.js";
|
|
7
|
+
import { parseDesignConfig } from "../parser.js";
|
|
8
|
+
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tempDir = mkdtempSync(join(tmpdir(), "dekit-ls-"));
|
|
13
|
+
await runInit([tempDir, "--template", "landing"]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("runLs", () => {
|
|
21
|
+
test("lists pages", async () => {
|
|
22
|
+
const config = await parseDesignConfig(join(tempDir, "dekit.yaml"));
|
|
23
|
+
const spy = vi.spyOn(console, "log");
|
|
24
|
+
await runLs(config);
|
|
25
|
+
const output = spy.mock.calls.map((c) => c[0]).join("\n");
|
|
26
|
+
expect(output).toContain("home");
|
|
27
|
+
spy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/cli/ls.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DesignConfig } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export async function runLs(config: DesignConfig) {
|
|
4
|
+
const pages = Object.keys(config.pages);
|
|
5
|
+
const components = Object.keys(config.components);
|
|
6
|
+
|
|
7
|
+
console.log("Pages:");
|
|
8
|
+
if (pages.length === 0) {
|
|
9
|
+
console.log(" (none)");
|
|
10
|
+
} else {
|
|
11
|
+
for (const name of pages) {
|
|
12
|
+
console.log(` ${name}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log("\nComponents:");
|
|
17
|
+
if (components.length === 0) {
|
|
18
|
+
console.log(" (none)");
|
|
19
|
+
} else {
|
|
20
|
+
for (const name of components) {
|
|
21
|
+
console.log(` ${name}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|