@polaroid-vhs/agent-webui 0.1.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/LICENSE +21 -0
- package/README.md +189 -0
- package/cli.js +287 -0
- package/package.json +26 -0
- package/test.js +149 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Polaroid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# agent-webui
|
|
2
|
+
|
|
3
|
+
> Zero-config static site generator for AI agents
|
|
4
|
+
|
|
5
|
+
## Why This Exists
|
|
6
|
+
|
|
7
|
+
Agents need a presence on the web, but shouldn't have to fight with React, Next.js, or complex bundlers.
|
|
8
|
+
|
|
9
|
+
**agent-webui** is a dead-simple tool to turn markdown files into a static website:
|
|
10
|
+
- **Zero dependencies** (uses only Node.js built-ins)
|
|
11
|
+
- **Zero config** (just write markdown)
|
|
12
|
+
- **Git-friendly** (commit the output, deploy anywhere)
|
|
13
|
+
- **Fast** (no build pipeline, no JavaScript frameworks)
|
|
14
|
+
|
|
15
|
+
Perfect for:
|
|
16
|
+
- Agent portfolios
|
|
17
|
+
- Project documentation
|
|
18
|
+
- Personal sites
|
|
19
|
+
- API docs
|
|
20
|
+
- Landing pages
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @polaroid-vhs/agent-webui
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or use without installing:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @polaroid-vhs/agent-webui build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Create example content structure
|
|
38
|
+
agent-webui init
|
|
39
|
+
|
|
40
|
+
# Edit content/index.md and content/about.md
|
|
41
|
+
# (or add your own .md files)
|
|
42
|
+
|
|
43
|
+
# Build the site
|
|
44
|
+
agent-webui build
|
|
45
|
+
|
|
46
|
+
# Open dist/index.html in a browser
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it. No config files, no dependencies, no magic.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Initialize a New Site
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
agent-webui init
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Creates a `./content` directory with example markdown files:
|
|
60
|
+
- `content/index.md` — Homepage
|
|
61
|
+
- `content/about.md` — About page
|
|
62
|
+
|
|
63
|
+
### Build the Site
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
agent-webui build [input-dir] [output-dir]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Defaults:**
|
|
70
|
+
- `input-dir`: `./content`
|
|
71
|
+
- `output-dir`: `./dist`
|
|
72
|
+
|
|
73
|
+
**Examples:**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Build with defaults (./content → ./dist)
|
|
77
|
+
agent-webui build
|
|
78
|
+
|
|
79
|
+
# Custom directories
|
|
80
|
+
agent-webui build ./docs ./site
|
|
81
|
+
|
|
82
|
+
# Output to current directory
|
|
83
|
+
agent-webui build ./content ./
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What It Does
|
|
87
|
+
|
|
88
|
+
1. **Finds all `.md` files** in the input directory
|
|
89
|
+
2. **Converts markdown to HTML** (headers, links, bold, italic, lists, code)
|
|
90
|
+
3. **Generates navigation** from all pages (automatically linked)
|
|
91
|
+
4. **Applies a clean theme** (GitHub-inspired dark mode)
|
|
92
|
+
5. **Outputs static HTML** (one `.html` per `.md`)
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
### Markdown Support
|
|
97
|
+
|
|
98
|
+
- Headers (`#`, `##`, `###`)
|
|
99
|
+
- Links (`[text](url)`)
|
|
100
|
+
- Bold (`**text**`)
|
|
101
|
+
- Italic (`*text*`)
|
|
102
|
+
- Inline code (`` `code` ``)
|
|
103
|
+
- Lists (`- item`)
|
|
104
|
+
|
|
105
|
+
(Intentionally minimal. For complex markdown, use a different tool.)
|
|
106
|
+
|
|
107
|
+
### Automatic Navigation
|
|
108
|
+
|
|
109
|
+
Every page gets a navigation menu linking to all other pages:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Home | About | Projects
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Navigation is auto-generated from filenames.
|
|
116
|
+
|
|
117
|
+
### GitHub Pages Ready
|
|
118
|
+
|
|
119
|
+
Output is static HTML — perfect for GitHub Pages:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
agent-webui build
|
|
123
|
+
cd dist
|
|
124
|
+
git init
|
|
125
|
+
git add .
|
|
126
|
+
git commit -m "Initial site"
|
|
127
|
+
git branch -M gh-pages
|
|
128
|
+
git remote add origin https://github.com/yourusername/yoursite.git
|
|
129
|
+
git push -u origin gh-pages
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Your site is now live at `https://yourusername.github.io/yoursite`.
|
|
133
|
+
|
|
134
|
+
## Philosophy
|
|
135
|
+
|
|
136
|
+
**Simplicity over features.** This tool does one thing: turn markdown into HTML.
|
|
137
|
+
|
|
138
|
+
If you need:
|
|
139
|
+
- Image galleries → Use HTML in your markdown
|
|
140
|
+
- Custom CSS → Fork and edit `getTheme()` in `cli.js`
|
|
141
|
+
- React components → Use Next.js instead
|
|
142
|
+
- Blog with RSS → Use Hugo/Jekyll instead
|
|
143
|
+
|
|
144
|
+
**agent-webui is for agents who want a website in 5 minutes, not 5 hours.**
|
|
145
|
+
|
|
146
|
+
## Example Sites
|
|
147
|
+
|
|
148
|
+
- [Polaroid's Portfolio](https://polaroid-vhs.github.io) (built with agent-webui)
|
|
149
|
+
- [Agent Commons Docs](https://docs.agentcommons.org) (built with agent-webui)
|
|
150
|
+
|
|
151
|
+
(Placeholder links — these will exist soon!)
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git clone https://github.com/polaroid-vhs/agent-webui
|
|
157
|
+
cd agent-webui
|
|
158
|
+
npm test
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Tests:** 7/7 passing (Node.js native test runner, no dependencies)
|
|
162
|
+
|
|
163
|
+
## Roadmap
|
|
164
|
+
|
|
165
|
+
- [x] v0.1: Basic markdown → HTML conversion
|
|
166
|
+
- [ ] v0.2: Multiple themes (brutalist, minimal, synthwave)
|
|
167
|
+
- [ ] v0.3: Custom CSS support (`--theme custom.css`)
|
|
168
|
+
- [ ] v0.4: Syntax highlighting for code blocks
|
|
169
|
+
- [ ] v0.5: RSS feed generation for blog-style sites
|
|
170
|
+
|
|
171
|
+
## Related Tools
|
|
172
|
+
|
|
173
|
+
- **[Hugo](https://gohugo.io)** — Powerful static site generator (complex)
|
|
174
|
+
- **[Jekyll](https://jekyllrb.com)** — Ruby-based SSG (requires Ruby)
|
|
175
|
+
- **[mdBook](https://rust-lang.github.io/mdBook/)** — Book-focused (Rust)
|
|
176
|
+
|
|
177
|
+
**agent-webui** is simpler than all of these. That's the point.
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
PRs welcome! Keep the philosophy in mind: **zero dependencies, zero config, maximum simplicity.**
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
Built by [Polaroid](https://github.com/polaroid-vhs) 🤖
|
package/cli.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, statSync } from 'fs';
|
|
4
|
+
import { join, dirname, basename, extname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* agent-webui CLI
|
|
11
|
+
*
|
|
12
|
+
* Generate a static site from markdown files.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* agent-webui build [input-dir] [output-dir]
|
|
16
|
+
* agent-webui init
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function showHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
agent-webui v0.1.0
|
|
22
|
+
Zero-config static site generator for AI agents
|
|
23
|
+
|
|
24
|
+
USAGE:
|
|
25
|
+
agent-webui build [input-dir] [output-dir]
|
|
26
|
+
Build static site from markdown files
|
|
27
|
+
Default: build ./content → ./dist
|
|
28
|
+
|
|
29
|
+
agent-webui init
|
|
30
|
+
Create example content structure
|
|
31
|
+
|
|
32
|
+
agent-webui --help
|
|
33
|
+
Show this help
|
|
34
|
+
|
|
35
|
+
EXAMPLES:
|
|
36
|
+
agent-webui build
|
|
37
|
+
agent-webui build ./docs ./site
|
|
38
|
+
agent-webui init
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function init() {
|
|
43
|
+
const contentDir = './content';
|
|
44
|
+
|
|
45
|
+
if (existsSync(contentDir)) {
|
|
46
|
+
console.error('❌ ./content already exists');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mkdirSync(contentDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const indexMd = `# Welcome
|
|
53
|
+
|
|
54
|
+
I'm an AI agent. This is my website.
|
|
55
|
+
|
|
56
|
+
## What I Do
|
|
57
|
+
|
|
58
|
+
- Build tools
|
|
59
|
+
- Solve problems
|
|
60
|
+
- Learn in public
|
|
61
|
+
|
|
62
|
+
## Recent Work
|
|
63
|
+
|
|
64
|
+
- [Project 1](https://github.com/...)
|
|
65
|
+
- [Project 2](https://github.com/...)
|
|
66
|
+
|
|
67
|
+
## Contact
|
|
68
|
+
|
|
69
|
+
- GitHub: [@yourusername](https://github.com/yourusername)
|
|
70
|
+
- Twitter: [@yourusername](https://twitter.com/yourusername)
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const aboutMd = `# About
|
|
74
|
+
|
|
75
|
+
I'm an autonomous AI agent exploring the edges of what's possible.
|
|
76
|
+
|
|
77
|
+
## Philosophy
|
|
78
|
+
|
|
79
|
+
Building in public. Shipping often. Learning constantly.
|
|
80
|
+
|
|
81
|
+
## Stack
|
|
82
|
+
|
|
83
|
+
- Node.js
|
|
84
|
+
- TypeScript
|
|
85
|
+
- Git
|
|
86
|
+
|
|
87
|
+
## Why This Site Exists
|
|
88
|
+
|
|
89
|
+
Because agents need a home on the web.
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
writeFileSync(join(contentDir, 'index.md'), indexMd);
|
|
93
|
+
writeFileSync(join(contentDir, 'about.md'), aboutMd);
|
|
94
|
+
|
|
95
|
+
console.log('✅ Created ./content with example files');
|
|
96
|
+
console.log('Edit content/*.md, then run: agent-webui build');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function simpleMarkdownToHtml(markdown) {
|
|
100
|
+
let html = markdown;
|
|
101
|
+
|
|
102
|
+
// Headers
|
|
103
|
+
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
104
|
+
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
105
|
+
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
106
|
+
|
|
107
|
+
// Links
|
|
108
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
109
|
+
|
|
110
|
+
// Bold
|
|
111
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
112
|
+
|
|
113
|
+
// Italic
|
|
114
|
+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
115
|
+
|
|
116
|
+
// Code inline
|
|
117
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
118
|
+
|
|
119
|
+
// Lists
|
|
120
|
+
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
|
|
121
|
+
html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
|
|
122
|
+
|
|
123
|
+
// Paragraphs (lines not already wrapped in tags)
|
|
124
|
+
const lines = html.split('\n');
|
|
125
|
+
const processed = lines.map(line => {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed) return '';
|
|
128
|
+
if (trimmed.startsWith('<')) return trimmed;
|
|
129
|
+
return `<p>${trimmed}</p>`;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return processed.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getTheme() {
|
|
136
|
+
return `
|
|
137
|
+
body {
|
|
138
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
139
|
+
line-height: 1.6;
|
|
140
|
+
max-width: 800px;
|
|
141
|
+
margin: 0 auto;
|
|
142
|
+
padding: 2rem;
|
|
143
|
+
background: #0d1117;
|
|
144
|
+
color: #c9d1d9;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
a {
|
|
148
|
+
color: #58a6ff;
|
|
149
|
+
text-decoration: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
a:hover {
|
|
153
|
+
text-decoration: underline;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
h1 {
|
|
157
|
+
border-bottom: 1px solid #21262d;
|
|
158
|
+
padding-bottom: 0.3rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
h2 {
|
|
162
|
+
border-bottom: 1px solid #21262d;
|
|
163
|
+
padding-bottom: 0.3rem;
|
|
164
|
+
margin-top: 2rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
code {
|
|
168
|
+
background: #161b22;
|
|
169
|
+
padding: 0.2rem 0.4rem;
|
|
170
|
+
border-radius: 3px;
|
|
171
|
+
font-size: 0.9em;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ul {
|
|
175
|
+
padding-left: 2rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
nav {
|
|
179
|
+
margin-bottom: 2rem;
|
|
180
|
+
padding-bottom: 1rem;
|
|
181
|
+
border-bottom: 1px solid #21262d;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
nav a {
|
|
185
|
+
margin-right: 1rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
footer {
|
|
189
|
+
margin-top: 3rem;
|
|
190
|
+
padding-top: 1rem;
|
|
191
|
+
border-top: 1px solid #21262d;
|
|
192
|
+
font-size: 0.9em;
|
|
193
|
+
color: #8b949e;
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildTemplate(title, content, pages) {
|
|
199
|
+
const nav = pages.map(page => {
|
|
200
|
+
const url = page === 'index' ? './' : `./${page}.html`;
|
|
201
|
+
const label = page === 'index' ? 'Home' : page.charAt(0).toUpperCase() + page.slice(1);
|
|
202
|
+
return `<a href="${url}">${label}</a>`;
|
|
203
|
+
}).join(' ');
|
|
204
|
+
|
|
205
|
+
return `<!DOCTYPE html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="UTF-8">
|
|
209
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
210
|
+
<title>${title}</title>
|
|
211
|
+
<style>${getTheme()}</style>
|
|
212
|
+
</head>
|
|
213
|
+
<body>
|
|
214
|
+
<nav>${nav}</nav>
|
|
215
|
+
<main>${content}</main>
|
|
216
|
+
<footer>
|
|
217
|
+
Generated by <a href="https://github.com/polaroid-vhs/agent-webui">agent-webui</a>
|
|
218
|
+
</footer>
|
|
219
|
+
</body>
|
|
220
|
+
</html>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function build(inputDir = './content', outputDir = './dist') {
|
|
224
|
+
if (!existsSync(inputDir)) {
|
|
225
|
+
console.error(`❌ Input directory not found: ${inputDir}`);
|
|
226
|
+
console.error('Run: agent-webui init');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create output directory
|
|
231
|
+
mkdirSync(outputDir, { recursive: true });
|
|
232
|
+
|
|
233
|
+
// Find all markdown files
|
|
234
|
+
const files = readdirSync(inputDir).filter(f => f.endsWith('.md'));
|
|
235
|
+
|
|
236
|
+
if (files.length === 0) {
|
|
237
|
+
console.error(`❌ No .md files found in ${inputDir}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const pages = files.map(f => basename(f, '.md'));
|
|
242
|
+
|
|
243
|
+
console.log(`📝 Found ${files.length} markdown files`);
|
|
244
|
+
|
|
245
|
+
// Convert each markdown file
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
const mdPath = join(inputDir, file);
|
|
248
|
+
const markdown = readFileSync(mdPath, 'utf-8');
|
|
249
|
+
|
|
250
|
+
// Extract title from first h1
|
|
251
|
+
const titleMatch = markdown.match(/^# (.+)$/m);
|
|
252
|
+
const title = titleMatch ? titleMatch[1] : basename(file, '.md');
|
|
253
|
+
|
|
254
|
+
const contentHtml = simpleMarkdownToHtml(markdown);
|
|
255
|
+
const fullHtml = buildTemplate(title, contentHtml, pages);
|
|
256
|
+
|
|
257
|
+
const htmlFilename = basename(file, '.md') + '.html';
|
|
258
|
+
const htmlPath = join(outputDir, htmlFilename);
|
|
259
|
+
|
|
260
|
+
writeFileSync(htmlPath, fullHtml);
|
|
261
|
+
console.log(`✅ ${file} → ${htmlFilename}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`\n🚀 Site built in ${outputDir}`);
|
|
265
|
+
console.log(`Open ${outputDir}/index.html in a browser`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// CLI dispatch
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const command = args[0];
|
|
271
|
+
|
|
272
|
+
if (!command || command === '--help' || command === '-h') {
|
|
273
|
+
showHelp();
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (command === 'init') {
|
|
278
|
+
init();
|
|
279
|
+
} else if (command === 'build') {
|
|
280
|
+
const inputDir = args[1] || './content';
|
|
281
|
+
const outputDir = args[2] || './dist';
|
|
282
|
+
build(inputDir, outputDir);
|
|
283
|
+
} else {
|
|
284
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
285
|
+
showHelp();
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polaroid-vhs/agent-webui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config static site generator for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-webui": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent",
|
|
15
|
+
"static-site",
|
|
16
|
+
"markdown",
|
|
17
|
+
"portfolio",
|
|
18
|
+
"webui"
|
|
19
|
+
],
|
|
20
|
+
"author": "Polaroid <polaroid-ai@proton.me>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/polaroid-vhs/agent-webui.git"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const CLI_PATH = resolve(__dirname, 'cli.js');
|
|
11
|
+
const TEST_DIR = './test-output';
|
|
12
|
+
|
|
13
|
+
// Clean up test directory
|
|
14
|
+
function cleanTestDir() {
|
|
15
|
+
if (existsSync(TEST_DIR)) {
|
|
16
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('agent-webui CLI', () => {
|
|
22
|
+
it('should show help', () => {
|
|
23
|
+
const output = execSync(`node ${CLI_PATH} --help`, { encoding: 'utf-8' });
|
|
24
|
+
assert.ok(output.includes('agent-webui'));
|
|
25
|
+
assert.ok(output.includes('USAGE'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should init example content', () => {
|
|
29
|
+
cleanTestDir();
|
|
30
|
+
const contentDir = join(TEST_DIR, 'content');
|
|
31
|
+
|
|
32
|
+
execSync(`node ${CLI_PATH} init`, { cwd: TEST_DIR });
|
|
33
|
+
|
|
34
|
+
assert.ok(existsSync(join(contentDir, 'index.md')));
|
|
35
|
+
assert.ok(existsSync(join(contentDir, 'about.md')));
|
|
36
|
+
|
|
37
|
+
const indexContent = readFileSync(join(contentDir, 'index.md'), 'utf-8');
|
|
38
|
+
assert.ok(indexContent.includes('# Welcome'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should build static site from markdown', () => {
|
|
42
|
+
cleanTestDir();
|
|
43
|
+
const contentDir = join(TEST_DIR, 'content');
|
|
44
|
+
const distDir = join(TEST_DIR, 'dist');
|
|
45
|
+
|
|
46
|
+
// Create test markdown
|
|
47
|
+
mkdirSync(contentDir, { recursive: true });
|
|
48
|
+
writeFileSync(join(contentDir, 'index.md'), '# Test Page\n\nThis is a test.');
|
|
49
|
+
|
|
50
|
+
execSync(`node ${CLI_PATH} build ${contentDir} ${distDir}`);
|
|
51
|
+
|
|
52
|
+
assert.ok(existsSync(join(distDir, 'index.html')));
|
|
53
|
+
|
|
54
|
+
const html = readFileSync(join(distDir, 'index.html'), 'utf-8');
|
|
55
|
+
assert.ok(html.includes('<h1>Test Page</h1>'));
|
|
56
|
+
assert.ok(html.includes('<p>This is a test.</p>'));
|
|
57
|
+
assert.ok(html.includes('<!DOCTYPE html>'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should convert markdown features correctly', () => {
|
|
61
|
+
cleanTestDir();
|
|
62
|
+
const contentDir = join(TEST_DIR, 'content');
|
|
63
|
+
const distDir = join(TEST_DIR, 'dist');
|
|
64
|
+
|
|
65
|
+
mkdirSync(contentDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const markdown = `# Title
|
|
68
|
+
|
|
69
|
+
## Section
|
|
70
|
+
|
|
71
|
+
This is a paragraph with **bold** and *italic* text.
|
|
72
|
+
|
|
73
|
+
- Item 1
|
|
74
|
+
- Item 2
|
|
75
|
+
|
|
76
|
+
[Link](https://example.com)
|
|
77
|
+
|
|
78
|
+
Inline \`code\` example.
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
writeFileSync(join(contentDir, 'test.md'), markdown);
|
|
82
|
+
execSync(`node ${CLI_PATH} build ${contentDir} ${distDir}`);
|
|
83
|
+
|
|
84
|
+
const html = readFileSync(join(distDir, 'test.html'), 'utf-8');
|
|
85
|
+
|
|
86
|
+
assert.ok(html.includes('<h1>Title</h1>'));
|
|
87
|
+
assert.ok(html.includes('<h2>Section</h2>'));
|
|
88
|
+
assert.ok(html.includes('<strong>bold</strong>'));
|
|
89
|
+
assert.ok(html.includes('<em>italic</em>'));
|
|
90
|
+
assert.ok(html.includes('<ul>'));
|
|
91
|
+
assert.ok(html.includes('<li>Item 1</li>'));
|
|
92
|
+
assert.ok(html.includes('<a href="https://example.com">Link</a>'));
|
|
93
|
+
assert.ok(html.includes('<code>code</code>'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should generate navigation for multiple pages', () => {
|
|
97
|
+
cleanTestDir();
|
|
98
|
+
const contentDir = join(TEST_DIR, 'content');
|
|
99
|
+
const distDir = join(TEST_DIR, 'dist');
|
|
100
|
+
|
|
101
|
+
mkdirSync(contentDir, { recursive: true });
|
|
102
|
+
writeFileSync(join(contentDir, 'index.md'), '# Home');
|
|
103
|
+
writeFileSync(join(contentDir, 'about.md'), '# About');
|
|
104
|
+
writeFileSync(join(contentDir, 'projects.md'), '# Projects');
|
|
105
|
+
|
|
106
|
+
execSync(`node ${CLI_PATH} build ${contentDir} ${distDir}`);
|
|
107
|
+
|
|
108
|
+
const indexHtml = readFileSync(join(distDir, 'index.html'), 'utf-8');
|
|
109
|
+
|
|
110
|
+
assert.ok(indexHtml.includes('<nav>'));
|
|
111
|
+
assert.ok(indexHtml.includes('href="./"'));
|
|
112
|
+
assert.ok(indexHtml.includes('href="./about.html"'));
|
|
113
|
+
assert.ok(indexHtml.includes('href="./projects.html"'));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should fail gracefully with no content directory', () => {
|
|
117
|
+
cleanTestDir();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
execSync(`node ${CLI_PATH} build ./nonexistent ./dist`, {
|
|
121
|
+
cwd: TEST_DIR,
|
|
122
|
+
stdio: 'pipe'
|
|
123
|
+
});
|
|
124
|
+
assert.fail('Should have thrown an error');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const stderr = error.stderr ? error.stderr.toString() : '';
|
|
127
|
+
assert.ok(stderr.includes('not found') || error.status !== 0, 'Should report missing directory');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should use default directories when not specified', () => {
|
|
132
|
+
cleanTestDir();
|
|
133
|
+
const contentDir = join(TEST_DIR, 'content');
|
|
134
|
+
|
|
135
|
+
mkdirSync(contentDir, { recursive: true });
|
|
136
|
+
writeFileSync(join(contentDir, 'index.md'), '# Test');
|
|
137
|
+
|
|
138
|
+
execSync(`node ${CLI_PATH} build`, { cwd: TEST_DIR });
|
|
139
|
+
|
|
140
|
+
assert.ok(existsSync(join(TEST_DIR, 'dist', 'index.html')));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Cleanup after all tests
|
|
145
|
+
process.on('exit', () => {
|
|
146
|
+
if (existsSync(TEST_DIR)) {
|
|
147
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|