feat: Rebrand from UWF Dashboard to Uncaged Dashboard #6

Merged
xiaonuo merged 1 commits from fix/5-rebrand-uncaged-dashboard into main 2026-05-28 16:13:12 +00:00
13 changed files with 268 additions and 11 deletions
+1
View File
@@ -1,6 +1,7 @@
{
"name": "worker-dashboard",
"private": true,
"description": "Uncaged Dashboard - a real-time distributed command execution monitoring system",
"workspaces": ["packages/*"],
"scripts": {
"dev:server": "node packages/server/src/index.mjs",
+6 -2
View File
@@ -1,11 +1,15 @@
# @uncaged/cli-dashboard
CLI tools for UWF Worker Dashboard.
CLI tools for Uncaged Dashboard.
## Commands
### `urec <command> [args...]`
Run a command and record its output to `~/.uwf-dashboard/records/`.
Run a command and record its output to `~/.uncaged/dashboard/records/`.
### `uconn [--url <ws-url>]`
Connect to the dashboard server and sync records. Defaults to `wss://dashboard.shazhou.work/ws/worker`.
## Migration
If you have existing data in `~/.uwf-dashboard`, it will be automatically migrated to `~/.uncaged/dashboard` on first run.
+138
View File
@@ -0,0 +1,138 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
describe("CLI Package Metadata", () => {
it("should have 'Uncaged Dashboard' in package description", () => {
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
if (pkg.description) {
expect(pkg.description.toLowerCase()).toContain("uncaged");
expect(pkg.description.toLowerCase()).not.toContain("uwf");
}
});
});
describe("CLI Help Text", () => {
it("urec.ts should not contain 'UWF Dashboard' references", () => {
const content = readFileSync(join(__dirname, "..", "src", "urec.ts"), "utf-8");
expect(content.toLowerCase()).not.toContain("uwf dashboard");
});
it("uconn.ts should not contain 'UWF Dashboard' references", () => {
const content = readFileSync(join(__dirname, "..", "src", "uconn.ts"), "utf-8");
expect(content.toLowerCase()).not.toContain("uwf dashboard");
});
it("CLI README should reference 'Uncaged' not 'UWF' in user-facing text", () => {
const readmePath = join(__dirname, "..", "README.md");
if (existsSync(readmePath)) {
const content = readFileSync(readmePath, "utf-8");
expect(content).toContain("Uncaged");
expect(content).toContain(".uncaged/dashboard");
expect(content).not.toContain("UWF Worker Dashboard");
expect(content).not.toContain("UWF Dashboard");
}
});
});
describe("Directory Migration", () => {
it("urec.ts should use new directory path as primary", () => {
const content = readFileSync(join(__dirname, "..", "src", "urec.ts"), "utf-8");
expect(content).toContain('".uncaged/dashboard"');
expect(content).toContain(', "records")');
// Should define NEW_BASE_DIR before RECORDS_DIR
const newBaseIndex = content.indexOf("NEW_BASE_DIR");
const recordsDirIndex = content.indexOf("RECORDS_DIR: string = join(NEW_BASE_DIR");
expect(newBaseIndex).toBeGreaterThan(0);
expect(recordsDirIndex).toBeGreaterThan(newBaseIndex);
});
it("uconn.ts should use new directory paths as primary", () => {
const content = readFileSync(join(__dirname, "..", "src", "uconn.ts"), "utf-8");
expect(content).toContain('".uncaged/dashboard"');
expect(content).toContain(', "records")');
expect(content).toContain(', ".synced")');
// Should define NEW_BASE_DIR and use it for RECORDS_DIR and SYNCED_FILE
const newBaseIndex = content.indexOf("NEW_BASE_DIR");
const recordsDirIndex = content.indexOf("RECORDS_DIR: string = join(NEW_BASE_DIR");
const syncedFileIndex = content.indexOf("SYNCED_FILE: string = join(NEW_BASE_DIR");
expect(newBaseIndex).toBeGreaterThan(0);
expect(recordsDirIndex).toBeGreaterThan(newBaseIndex);
expect(syncedFileIndex).toBeGreaterThan(newBaseIndex);
});
});
describe("Legacy Directory Auto-Migration", () => {
let testHome: string;
beforeEach(() => {
testHome = join(tmpdir(), `test-migration-${Date.now()}`);
mkdirSync(testHome, { recursive: true });
});
afterEach(() => {
if (existsSync(testHome)) {
rmSync(testHome, { recursive: true, force: true });
}
});
it("should migrate from legacy .uwf-dashboard to .uncaged/dashboard", async () => {
// Setup: Create legacy directory with test data
const legacyDir = join(testHome, ".uwf-dashboard", "records");
const newDir = join(testHome, ".uncaged", "dashboard", "records");
mkdirSync(legacyDir, { recursive: true });
const testRecord = { id: "test-123", device: "test-device", command: "echo test" };
writeFileSync(join(legacyDir, "test-123.json"), JSON.stringify(testRecord));
// Test migration logic
expect(existsSync(legacyDir)).toBe(true);
expect(existsSync(newDir)).toBe(false);
// Migration should happen when new directory doesn't exist
// This test verifies the paths are correct
});
it("should handle empty legacy directory", () => {
const legacyDir = join(testHome, ".uwf-dashboard");
mkdirSync(legacyDir, { recursive: true });
expect(existsSync(legacyDir)).toBe(true);
// Migration should create new directory even if old is empty
});
it("should not migrate when new directory already exists", () => {
const newDir = join(testHome, ".uncaged", "dashboard", "records");
mkdirSync(newDir, { recursive: true });
const existingRecord = { id: "existing", device: "test" };
writeFileSync(join(newDir, "existing.json"), JSON.stringify(existingRecord));
expect(existsSync(newDir)).toBe(true);
const content = readFileSync(join(newDir, "existing.json"), "utf-8");
expect(JSON.parse(content).id).toBe("existing");
});
});
describe("Package Metadata", () => {
it("frontend package.json should reference Uncaged", () => {
const pkg = JSON.parse(
readFileSync(join(__dirname, "..", "..", "frontend", "package.json"), "utf-8"),
);
if (pkg.description) {
expect(pkg.description.toLowerCase()).toContain("uncaged");
expect(pkg.description.toLowerCase()).not.toContain("uwf");
}
});
it("server package.json should reference Uncaged", () => {
const pkg = JSON.parse(
readFileSync(join(__dirname, "..", "..", "server", "package.json"), "utf-8"),
);
if (pkg.description) {
expect(pkg.description.toLowerCase()).toContain("uncaged");
expect(pkg.description.toLowerCase()).not.toContain("uwf");
}
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
const RECORDS_DIR = join(homedir(), ".uwf-dashboard/records");
const RECORDS_DIR = join(homedir(), ".uncaged/dashboard/records");
const UREC_PATH = join(import.meta.dirname, "../dist/urec.js");
describe("urec Type Safety Tests", () => {
+1
View File
@@ -2,6 +2,7 @@
"name": "@uncaged/cli-dashboard",
"version": "1.0.0",
"type": "module",
"description": "Uncaged Dashboard CLI - command recording and sync tools (urec and uconn)",
"bin": {
"urec": "./dist/urec.js",
"uconn": "./dist/uconn.js"
+47 -3
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { mkdir, readFile, readdir, rename, rmdir, stat, unlink, writeFile } from "node:fs/promises";
import { homedir, hostname } from "node:os";
import { join } from "node:path";
import { MSG } from "@uncaged/dashboard-server/protocol";
@@ -25,10 +26,53 @@ program.option("--url <url>", "WebSocket URL", "wss://dashboard.shazhou.work/ws/
const opts = program.opts<{ url: string }>();
const WS_URL: string = opts.url;
const DEVICE: string = hostname();
const RECORDS_DIR: string = join(homedir(), ".uwf-dashboard/records");
const SYNCED_FILE: string = join(homedir(), ".uwf-dashboard/.synced");
// Migration: Move from legacy .uwf-dashboard to .uncaged/dashboard
const LEGACY_DIR: string = join(homedir(), ".uwf-dashboard");
const NEW_BASE_DIR: string = join(homedir(), ".uncaged/dashboard");
const RECORDS_DIR: string = join(NEW_BASE_DIR, "records");
const SYNCED_FILE: string = join(NEW_BASE_DIR, ".synced");
const THREE_DAYS: number = 3 * 24 * 60 * 60 * 1000;
async function migrateFromLegacy(): Promise<void> {
if (existsSync(LEGACY_DIR) && !existsSync(NEW_BASE_DIR)) {
console.log("Migrating from legacy .uwf-dashboard to .uncaged/dashboard...");
await mkdir(NEW_BASE_DIR, { recursive: true });
// Migrate records directory if it exists
const legacyRecordsDir = join(LEGACY_DIR, "records");
if (existsSync(legacyRecordsDir)) {
const files = await readdir(legacyRecordsDir);
await mkdir(RECORDS_DIR, { recursive: true });
for (const file of files) {
const oldPath = join(legacyRecordsDir, file);
const newPath = join(RECORDS_DIR, file);
await rename(oldPath, newPath);
}
await rmdir(legacyRecordsDir);
}
// Migrate .synced file if it exists
const legacySyncedFile = join(LEGACY_DIR, ".synced");
if (existsSync(legacySyncedFile)) {
await rename(legacySyncedFile, SYNCED_FILE);
}
// Remove legacy directory if empty
try {
const remaining = await readdir(LEGACY_DIR);
if (remaining.length === 0) {
await rmdir(LEGACY_DIR);
}
} catch {}
console.log("Migration complete.");
}
}
await migrateFromLegacy();
await mkdir(RECORDS_DIR, { recursive: true });
let synced: Set<string> = new Set();
+40 -2
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env node
import { type ChildProcess, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { mkdir, readdir, rename, rmdir, writeFile } from "node:fs/promises";
import { homedir, hostname } from "node:os";
import { join } from "node:path";
@@ -18,7 +19,44 @@ interface Record {
durationMs: number;
}
const RECORDS_DIR: string = join(homedir(), ".uwf-dashboard/records");
// Migration: Move from legacy .uwf-dashboard to .uncaged/dashboard
const LEGACY_DIR: string = join(homedir(), ".uwf-dashboard");
const NEW_BASE_DIR: string = join(homedir(), ".uncaged/dashboard");
const RECORDS_DIR: string = join(NEW_BASE_DIR, "records");
async function migrateFromLegacy(): Promise<void> {
if (existsSync(LEGACY_DIR) && !existsSync(NEW_BASE_DIR)) {
console.log("Migrating from legacy .uwf-dashboard to .uncaged/dashboard...");
await mkdir(NEW_BASE_DIR, { recursive: true });
// Migrate records directory if it exists
const legacyRecordsDir = join(LEGACY_DIR, "records");
if (existsSync(legacyRecordsDir)) {
const files = await readdir(legacyRecordsDir);
await mkdir(RECORDS_DIR, { recursive: true });
for (const file of files) {
const oldPath = join(legacyRecordsDir, file);
const newPath = join(RECORDS_DIR, file);
await rename(oldPath, newPath);
}
await rmdir(legacyRecordsDir);
}
// Remove legacy directory if empty
try {
const remaining = await readdir(LEGACY_DIR);
if (remaining.length === 0) {
await rmdir(LEGACY_DIR);
}
} catch {}
console.log("Migration complete.");
}
}
await migrateFromLegacy();
await mkdir(RECORDS_DIR, { recursive: true });
const args: string[] = process.argv.slice(2);
+2 -1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
include: ["__tests__/**/*.test.ts"],
exclude: ["**/node_modules/**", "**/dist/**"],
},
});
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>UWF Dashboard</title></head>
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Uncaged Dashboard</title></head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>
+1
View File
@@ -3,6 +3,7 @@
"private": true,
"version": "1.0.0",
"type": "module",
"description": "Uncaged Dashboard frontend - a real-time web interface for monitoring command execution",
"repository": {
"type": "git",
"url": "https://git.shazhou.work/uncaged/worker-dashboard.git",
+28
View File
@@ -0,0 +1,28 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
describe("Frontend Branding", () => {
it("should display 'Uncaged Dashboard' in header", () => {
const content = readFileSync(join(__dirname, "App.tsx"), "utf-8");
expect(content).toContain("<h1>⚡ Uncaged Dashboard</h1>");
expect(content).not.toContain("<h1>⚡ UWF Dashboard</h1>");
});
it("should not have residual UWF references in UI strings", () => {
const content = readFileSync(join(__dirname, "App.tsx"), "utf-8");
// Remove comments and check for UWF in UI strings
const withoutComments = content.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
// Allow variable names but not in JSX strings
const jsxMatches = withoutComments.match(/<[^>]*>.*?UWF.*?<\/[^>]*>/gi);
expect(jsxMatches).toBeNull();
});
});
describe("HTML Page Title", () => {
it("should have 'Uncaged Dashboard' as page title", () => {
const content = readFileSync(join(__dirname, "..", "index.html"), "utf-8");
expect(content).toContain("<title>Uncaged Dashboard</title>");
expect(content).not.toContain("<title>UWF Dashboard</title>");
});
});
+1 -1
View File
@@ -80,7 +80,7 @@ export default function App() {
return (
<div className="container">
<header>
<h1> UWF Dashboard</h1>
<h1> Uncaged Dashboard</h1>
<div className="workers">
{workers.length > 0 ? (
workers.map((w) => (
+1
View File
@@ -3,6 +3,7 @@
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Uncaged Dashboard server - WebSocket and REST API for aggregating command records",
"exports": {
".": {
"bun": "./src/index.ts",