dekit-cli 0.1.1 → 0.2.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.
Files changed (57) hide show
  1. package/README.md +23 -0
  2. package/bin/dekit.js +16 -0
  3. package/bin/dekit.ts +98 -35
  4. package/dist/editor/assets/{index-DK9iczjI.js → index-yt1AtkL3.js} +6 -6
  5. package/dist/editor/index.html +1 -1
  6. package/docs/usage.md +127 -0
  7. package/editor/src/App.tsx +9 -11
  8. package/editor/src/types.ts +1 -0
  9. package/package.json +37 -12
  10. package/src/chrome.test.ts +24 -0
  11. package/src/chrome.ts +37 -0
  12. package/src/cli/add.test.ts +62 -0
  13. package/src/cli/add.ts +104 -0
  14. package/src/cli/init.test.ts +61 -0
  15. package/src/cli/init.ts +46 -0
  16. package/src/cli/ls.test.ts +29 -0
  17. package/src/cli/ls.ts +24 -0
  18. package/src/cli/resolve.test.ts +43 -0
  19. package/src/cli/resolve.ts +92 -0
  20. package/src/cli/screenshot.test.ts +54 -0
  21. package/src/cli/screenshot.ts +154 -0
  22. package/src/cli/usage.ts +8 -0
  23. package/src/devices.ts +23 -0
  24. package/src/injected/inspector.ts +31 -0
  25. package/src/parser.test.ts +6 -6
  26. package/src/parser.ts +2 -2
  27. package/src/ref.test.ts +68 -0
  28. package/src/ref.ts +42 -0
  29. package/src/server.ts +8 -5
  30. package/src/types.ts +13 -0
  31. package/src/vite-plugin-dekit.ts +13 -6
  32. package/templates/components/blank/component.css +3 -0
  33. package/templates/components/blank/component.html +5 -0
  34. package/templates/pages/blank/page.css +3 -0
  35. package/templates/pages/blank/page.html +3 -0
  36. package/templates/pages/form/page.css +88 -0
  37. package/templates/pages/form/page.html +21 -0
  38. package/templates/pages/grid/page.css +84 -0
  39. package/templates/pages/grid/page.html +40 -0
  40. package/templates/pages/hero/page.css +62 -0
  41. package/templates/pages/hero/page.html +10 -0
  42. package/templates/projects/blank/dekit.yaml +9 -0
  43. package/templates/projects/blank/global.css +18 -0
  44. package/templates/projects/blank/pages/home/home.css +3 -0
  45. package/templates/projects/blank/pages/home/home.html +3 -0
  46. package/templates/projects/dashboard/dekit.yaml +9 -0
  47. package/templates/projects/dashboard/global.css +18 -0
  48. package/templates/projects/dashboard/pages/home/home.css +248 -0
  49. package/templates/projects/dashboard/pages/home/home.html +100 -0
  50. package/templates/projects/landing/dekit.yaml +9 -0
  51. package/templates/projects/landing/global.css +18 -0
  52. package/templates/projects/landing/pages/home/home.css +255 -0
  53. package/templates/projects/landing/pages/home/home.html +79 -0
  54. package/templates/projects/mobile/dekit.yaml +9 -0
  55. package/templates/projects/mobile/global.css +20 -0
  56. package/templates/projects/mobile/pages/home/home.css +267 -0
  57. package/templates/projects/mobile/pages/home/home.html +110 -0
@@ -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-DK9iczjI.js"></script>
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
+ ```
@@ -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; domPath?: string; page: string } | null>(null);
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
- const filePath = configRef.current?.pagePaths?.[page] ?? page;
44
- return "$$" + "{" + filePath + "}";
43
+ return "$$" + "{" + page + "}";
45
44
  }, []);
46
45
 
47
- const getNodeRef = useCallback((page: string, domPath: string) => {
48
- const filePath = configRef.current?.pagePaths?.[page] ?? page;
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.path));
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={(domPath, x, y) => setContextMenu({ x, y, domPath, page: currentPage })}
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.path)} />
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.domPath && (
397
+ {contextMenu.cssSelector && (
400
398
  <div
401
399
  className="context-menu-item"
402
400
  onClick={() => {
403
- navigator.clipboard.writeText(getNodeRef(contextMenu.page, contextMenu.domPath!));
401
+ navigator.clipboard.writeText(getNodeRef(contextMenu.page, contextMenu.cssSelector!));
404
402
  setContextMenu(null);
405
403
  }}
406
404
  >
@@ -1,6 +1,7 @@
1
1
  export interface NodeInfo {
2
2
  tagName: string;
3
3
  path: string;
4
+ cssSelector: string;
4
5
  attributes: Record<string, string>;
5
6
  computedStyles: Record<string, string>;
6
7
  boundingRect: { x: number; y: number; width: number; height: number };
package/package.json CHANGED
@@ -1,20 +1,44 @@
1
1
  {
2
2
  "name": "dekit-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
- "description": "A code-first design toolkit — preview, inspect, and debug your HTML/CSS designs in the browser",
6
- "keywords": ["design", "devtools", "inspector", "preview", "css", "html", "cli"],
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": { "type": "git", "url": "https://github.com/myWsq/dekit" },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/myWsq/dekit"
21
+ },
9
22
  "homepage": "https://github.com/myWsq/dekit",
10
- "bugs": { "url": "https://github.com/myWsq/dekit/issues" },
11
- "files": ["bin/", "src/", "editor/", "dist/"],
12
- "engines": { "node": ">=18" },
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
- "dekit": "./bin/dekit.ts"
38
+ "dekit": "./bin/dekit.js"
15
39
  },
16
40
  "scripts": {
17
- "dev": "tsx bin/dekit.ts",
41
+ "dev": "node --import tsx bin/dekit.ts",
18
42
  "build:editor": "vite build --config editor/vite.config.ts",
19
43
  "test": "vitest run",
20
44
  "prepublishOnly": "npm run build:editor"
@@ -24,6 +48,8 @@
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",
52
+ "tsx": "^4.19.0",
27
53
  "react-icons": "^5.6.0",
28
54
  "sirv": "^3.0.0",
29
55
  "vite": "^6.2.0"
@@ -32,11 +58,10 @@
32
58
  "@types/js-yaml": "^4.0.9",
33
59
  "@types/react": "^19.0.0",
34
60
  "@types/react-dom": "^19.0.0",
61
+ "@vitejs/plugin-react": "^4.3.0",
35
62
  "react": "^19.0.0",
36
63
  "react-dom": "^19.0.0",
37
- "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
+ });
@@ -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
+ }