Skip to content

Commit aa1bce4

Browse files
authored
Merge pull request #3 from jd316/fix/ui-tab-title-favicon
feat(ui): add GitHub release packaging and CLI launcher
2 parents 09ddf48 + a1cfd64 commit aa1bce4

11 files changed

Lines changed: 378 additions & 18 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
verify:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Node.js 20
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: npm
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Run tests
25+
run: npm run test
26+
27+
- name: Run type and Svelte checks
28+
run: npm run check
29+
30+
- name: Build
31+
run: npm run build

‎.github/workflows/release.yml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Node.js 20
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: npm
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Validate
26+
run: |
27+
npm run test
28+
npm run check
29+
30+
- name: Build production bundle
31+
run: npm run build
32+
33+
- name: Package release archives
34+
run: bash ./scripts/package-release.sh "${GITHUB_REF_NAME}" release
35+
36+
- name: Create GitHub Release
37+
uses: softprops/action-gh-release@v2
38+
with:
39+
files: |
40+
release/nullclaw-chat-ui-*.tar.gz
41+
release/nullclaw-chat-ui-*.zip
42+
generate_release_notes: true

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ Thumbs.db
2222
# Vite
2323
vite.config.js.timestamp-*
2424
vite.config.ts.timestamp-*
25+
26+
release/

‎README.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# nullclaw-ui
1+
# nullclaw-chat-ui
22

33
A terminal-style web interface for `nullclaw`.
44
The app uses WebSocket + `WebChannel v1` and supports PIN pairing, streaming assistant responses, tool-call rendering, approval prompts, and end-to-end encryption.
@@ -57,11 +57,29 @@ Open `http://localhost:5173`.
5757
- `npm run dev` - local development server.
5858
- `npm run build` - production build.
5959
- `npm run preview` - preview built app.
60+
- `npm run cli -- run` - serve built UI via local CLI entrypoint.
6061
- `npm run test` - run Vitest suite.
6162
- `npm run test:watch` - run Vitest in watch mode.
6263
- `npm run check` - `svelte-kit sync` + `svelte-check`.
6364
- `npm run check:watch` - `svelte-check` in watch mode.
6465

66+
## Global CLI Usage
67+
68+
GitHub release artifacts include a runnable CLI bundle with `build/` assets and launchers.
69+
After extracting a release archive, put the launcher on your `$PATH`:
70+
71+
```bash
72+
chmod +x nullclaw-chat-ui/nullclaw-chat-ui
73+
sudo ln -sf "$(pwd)/nullclaw-chat-ui/nullclaw-chat-ui" /usr/local/bin/nullclaw-chat-ui
74+
nullclaw-chat-ui run --host 127.0.0.1 --port 4173
75+
```
76+
77+
Notes:
78+
79+
- Requires `Node.js 20+` available as `node`.
80+
- Windows users can use `nullclaw-chat-ui.cmd` from the extracted folder.
81+
- `nullclaw-chat-ui run` serves the built static UI bundle with SPA fallback routing and still requires a reachable `nullclaw` WebSocket backend.
82+
6583
## Architecture (Short)
6684

6785
- [`src/routes/+page.svelte`](src/routes/+page.svelte): page composition root.

‎bin/nullclaw-chat-ui.js‎

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env node
2+
3+
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
4+
import { createServer } from "node:http";
5+
import { dirname, extname, join, normalize, resolve } from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const rootDir = resolve(__dirname, "..");
10+
const buildDir = resolve(rootDir, "build");
11+
const packageJsonPath = resolve(rootDir, "package.json");
12+
const packageVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
13+
14+
const mimeTypes = {
15+
".css": "text/css; charset=utf-8",
16+
".gif": "image/gif",
17+
".html": "text/html; charset=utf-8",
18+
".ico": "image/x-icon",
19+
".jpeg": "image/jpeg",
20+
".jpg": "image/jpeg",
21+
".js": "text/javascript; charset=utf-8",
22+
".json": "application/json; charset=utf-8",
23+
".map": "application/json; charset=utf-8",
24+
".png": "image/png",
25+
".svg": "image/svg+xml",
26+
".txt": "text/plain; charset=utf-8",
27+
".webmanifest": "application/manifest+json; charset=utf-8",
28+
".woff": "font/woff",
29+
".woff2": "font/woff2"
30+
};
31+
32+
function printHelp() {
33+
console.log(`nullclaw-chat-ui ${packageVersion}
34+
35+
Usage:
36+
nullclaw-chat-ui run [--host <host>] [--port <port>]
37+
nullclaw-chat-ui version
38+
nullclaw-chat-ui help
39+
40+
Commands:
41+
run Serve the built UI from the local package.
42+
version Print the installed package version.
43+
help Show this help text.
44+
45+
Options for "run":
46+
--host Bind address (default: 127.0.0.1)
47+
--port Port number (default: 4173)
48+
`);
49+
}
50+
51+
function parseRunOptions(args) {
52+
const options = { host: "127.0.0.1", port: 4173 };
53+
54+
for (let i = 0; i < args.length; i += 1) {
55+
const arg = args[i];
56+
if (arg === "--host") {
57+
options.host = args[i + 1];
58+
i += 1;
59+
} else if (arg === "--port") {
60+
const parsed = Number.parseInt(args[i + 1], 10);
61+
options.port = parsed;
62+
i += 1;
63+
} else if (arg === "-h" || arg === "--help") {
64+
options.help = true;
65+
} else {
66+
throw new Error(`Unknown option: ${arg}`);
67+
}
68+
}
69+
70+
if (!options.help) {
71+
if (!options.host) {
72+
throw new Error("Missing value for --host");
73+
}
74+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
75+
throw new Error("Port must be an integer between 1 and 65535");
76+
}
77+
}
78+
79+
return options;
80+
}
81+
82+
function resolveStaticFile(requestPath) {
83+
let decodedPath = "/";
84+
try {
85+
decodedPath = decodeURIComponent(requestPath || "/");
86+
} catch {
87+
return null;
88+
}
89+
90+
const cleanPath = decodedPath.split("?")[0].split("#")[0].replace(/^\/+/, "");
91+
const normalizedPath = normalize(cleanPath);
92+
if (normalizedPath.includes("\0") || normalizedPath.startsWith("..")) {
93+
return null;
94+
}
95+
96+
let candidate = join(buildDir, normalizedPath);
97+
try {
98+
if (statSync(candidate).isDirectory()) {
99+
candidate = join(candidate, "index.html");
100+
}
101+
} catch {
102+
// Keep candidate as-is; we'll fall through to SPA fallback below.
103+
}
104+
105+
if (existsSync(candidate)) {
106+
return candidate;
107+
}
108+
109+
return join(buildDir, "index.html");
110+
}
111+
112+
function serveBuiltUi({ host, port }) {
113+
const entryHtml = join(buildDir, "index.html");
114+
if (!existsSync(entryHtml)) {
115+
console.error("Build output was not found.");
116+
console.error("Run `npm run build` in this package, then retry `nullclaw-chat-ui run`.");
117+
process.exit(1);
118+
}
119+
120+
const server = createServer((req, res) => {
121+
if (req.method !== "GET" && req.method !== "HEAD") {
122+
res.statusCode = 405;
123+
res.setHeader("Allow", "GET, HEAD");
124+
res.end("Method Not Allowed");
125+
return;
126+
}
127+
128+
const filePath = resolveStaticFile(req.url || "/");
129+
if (!filePath) {
130+
res.statusCode = 400;
131+
res.end("Bad Request");
132+
return;
133+
}
134+
135+
const ext = extname(filePath).toLowerCase();
136+
res.statusCode = 200;
137+
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
138+
139+
if (req.method === "HEAD") {
140+
res.end();
141+
return;
142+
}
143+
144+
const stream = createReadStream(filePath);
145+
stream.on("error", () => {
146+
res.statusCode = 500;
147+
res.end("Internal Server Error");
148+
});
149+
stream.pipe(res);
150+
});
151+
152+
server.listen(port, host, () => {
153+
console.log(`nullclaw-chat-ui running at http://${host}:${port}`);
154+
console.log("Press Ctrl+C to stop.");
155+
});
156+
157+
const shutdown = () => {
158+
server.close(() => process.exit(0));
159+
};
160+
process.on("SIGINT", shutdown);
161+
process.on("SIGTERM", shutdown);
162+
}
163+
164+
function main() {
165+
const [command, ...rest] = process.argv.slice(2);
166+
167+
if (!command || command === "help" || command === "-h" || command === "--help") {
168+
printHelp();
169+
return;
170+
}
171+
172+
if (command === "version" || command === "-v" || command === "--version") {
173+
console.log(packageVersion);
174+
return;
175+
}
176+
177+
if (command === "run") {
178+
const options = parseRunOptions(rest);
179+
if (options.help) {
180+
printHelp();
181+
return;
182+
}
183+
serveBuiltUi(options);
184+
return;
185+
}
186+
187+
console.error(`Unknown command: ${command}`);
188+
console.error("Use `nullclaw-chat-ui help` for usage.");
189+
process.exit(1);
190+
}
191+
192+
try {
193+
main();
194+
} catch (error) {
195+
console.error(error instanceof Error ? error.message : String(error));
196+
process.exit(1);
197+
}

‎docs/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# nullclaw-ui Documentation
1+
# nullclaw-chat-ui Documentation
22

33
- [Architecture](./architecture.md)
44
- [Protocol and E2E](./protocol.md)

‎docs/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# nullclaw-ui Architecture
1+
# nullclaw-chat-ui Architecture
22

33
## Goals
44

‎docs/operations.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,24 @@ Security expectations:
4848
2. Run `npm run test` and `npm run check`.
4949
3. Manual verification of pairing, chat, approvals, logout.
5050
4. Build production bundle.
51-
5. Confirm diagnostics panel reflects actual E2E/runtime details.
51+
5. Verify CLI startup: `npm run cli -- run --host 127.0.0.1 --port 4173`.
52+
6. Tag a release: `git tag vYYYY.M.D && git push origin vYYYY.M.D`.
53+
7. Confirm GitHub Actions `Release` workflow attached `.tar.gz` and `.zip` assets.
54+
8. Confirm diagnostics panel reflects actual E2E/runtime details.
55+
56+
## GitHub Release Artifacts
57+
58+
When a tag matching `v*` is pushed, `.github/workflows/release.yml` runs and publishes:
59+
60+
- `nullclaw-chat-ui-<tag>.tar.gz`
61+
- `nullclaw-chat-ui-<tag>.zip`
62+
63+
Each archive contains:
64+
65+
- `nullclaw-chat-ui` (Unix launcher)
66+
- `nullclaw-chat-ui.cmd` (Windows launcher)
67+
- `bin/nullclaw-chat-ui.js`
68+
- `build/` (static app bundle)
5269

5370
## Troubleshooting
5471

0 commit comments

Comments
 (0)