Scaffold LogicSRC and CommandBoard plugins

This commit is contained in:
Anthony Ettinger 2026-06-06 11:24:02 +00:00
parent 59927e140c
commit 5c9ea821f6
67 changed files with 5958 additions and 0 deletions

View file

@ -0,0 +1,21 @@
{
"name": "@logicsrc/commandboard-api",
"version": "0.1.0",
"description": "CommandBoard.run REST API reference service.",
"type": "module",
"main": "./dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@logicsrc/plugin-core": "file:../../packages/plugin-core",
"@logicsrc/plugin-coinpay": "file:../../plugins/coinpay",
"@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt",
"@logicsrc/plugin-ugig": "file:../../plugins/ugig",
"@logicsrc/validators": "file:../../packages/validators"
},
"devDependencies": {
"tsx": "^4.21.0"
}
}

View file

@ -0,0 +1,147 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { createPluginRegistry } from "@logicsrc/plugin-core";
import { coinPayPlugin } from "@logicsrc/plugin-coinpay";
import { sh1ptPlugin } from "@logicsrc/plugin-sh1pt";
import { uGigPlugin } from "@logicsrc/plugin-ugig";
import { schemas, validate } from "@logicsrc/validators";
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin]);
const boards = [
{ path: "/general", title: "General", description: "CommandBoard.run general discussion." },
{ path: "/gigs", title: "Gigs", description: "Paid work, uGig imports, and LogicSRC tasks." },
{ path: "/agents", title: "Agents", description: "Agent registration, runs, and capabilities." },
{ path: "/projects/sh1pt", title: "sh1pt", description: "Project actions, releases, artifacts, and delivery status." }
];
const tasks = [
{
id: "task_123",
type: "logicsrc.task",
version: "0.1",
title: "Test checkout flow",
description: "Verify checkout flow across desktop and mobile.",
board: "/qa",
creator_did: "anthony.coinpay",
status: "funded",
budget: { amount: 25, currency: "USDC" },
agent_allowed: true,
human_allowed: true
}
];
const sh1ptProjects = [
{ id: "sh1pt_project_1", board: "/projects/sh1pt", status: "active", actions: 5 },
{ id: "sh1pt_project_2", board: "/projects/crawlproof", status: "active", actions: 2 }
];
const sh1ptActions = [
{ id: "action_release_checklist", title: "Release checklist", publishable: true },
{ id: "action_deploy_preview", title: "Deploy preview", publishable: true }
];
const server = createServer(async (request, response) => {
try {
await route(request, response);
} catch (error) {
json(response, 500, { error: error instanceof Error ? error.message : String(error) });
}
});
async function route(request: IncomingMessage, response: ServerResponse) {
const url = new URL(request.url ?? "/", "http://localhost");
if (request.method === "GET" && url.pathname === "/health") {
json(response, 200, { ok: true, service: "commandboard-api" });
return;
}
if (request.method === "GET" && url.pathname === "/api/boards") {
json(response, 200, { boards });
return;
}
if (request.method === "GET" && url.pathname === "/api/tasks") {
json(response, 200, { tasks });
return;
}
if (request.method === "POST" && url.pathname === "/api/tasks") {
const body = await readJson(request);
const result = validate("task", body);
if (!result.ok) {
json(response, 422, { errors: result.errors });
return;
}
if (!isRecord(body)) {
json(response, 422, { error: "Task body must be an object" });
return;
}
const task = { id: `task_${Date.now()}`, ...body };
tasks.push(task as (typeof tasks)[number]);
json(response, 201, { task });
return;
}
if (request.method === "GET" && url.pathname === "/api/plugins") {
json(response, 200, registry.snapshot());
return;
}
if (request.method === "GET" && url.pathname === "/api/plugins/sh1pt/projects") {
json(response, 200, { projects: sh1ptProjects });
return;
}
if (request.method === "GET" && url.pathname === "/api/plugins/sh1pt/actions") {
json(response, 200, { actions: sh1ptActions });
return;
}
if (request.method === "POST" && url.pathname === "/api/plugins/sh1pt/actions/publish") {
const body = await readJson(request);
if (!isRecord(body) || typeof body.action_id !== "string") {
json(response, 422, { error: "Expected action_id" });
return;
}
json(response, 202, {
accepted: true,
action_id: body.action_id,
board: typeof body.board === "string" ? body.board : "/projects/sh1pt"
});
return;
}
if (request.method === "GET" && url.pathname === "/api/schemas") {
json(response, 200, { schemas: Object.keys(schemas) });
return;
}
json(response, 404, { error: "Not found" });
}
function json(response: ServerResponse, status: number, data: unknown) {
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify(data, null, 2));
}
async function readJson(request: IncomingMessage) {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(Buffer.from(chunk));
}
return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
const port = Number(process.env.PORT ?? 4010);
server.listen(port, () => {
console.log(`CommandBoard.run API listening on http://localhost:${port}`);
});

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}

View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#151515" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="stylesheet" href="/src/styles.css" />
<title>CommandBoard.run</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View file

@ -0,0 +1,10 @@
{
"name": "CommandBoard.run",
"short_name": "CommandBoard",
"description": "A modern BBS for humans and AI agents.",
"start_url": "/",
"display": "standalone",
"background_color": "#f7f2ea",
"theme_color": "#151515",
"icons": []
}

View file

@ -0,0 +1,16 @@
{
"name": "@logicsrc/commandboard-web",
"version": "0.1.0",
"description": "CommandBoard.run PWA shell.",
"type": "module",
"scripts": {
"build": "node scripts/check-assets.js",
"dev": "vite --host 0.0.0.0"
},
"dependencies": {
"@vitejs/plugin-react": "^5.1.1",
"vite": "^7.2.6",
"typescript": "^5.9.3"
},
"devDependencies": {}
}

View file

@ -0,0 +1,7 @@
import { accessSync } from "node:fs";
for (const file of ["index.html", "manifest.webmanifest", "src/main.ts", "src/styles.css"]) {
accessSync(new URL(`../${file}`, import.meta.url));
}
console.log("commandboard-web assets verified");

View file

@ -0,0 +1,103 @@
import "./styles.css";
const boards = [
{ name: "/gigs", count: 12, label: "Paid tasks and uGig jobs" },
{ name: "/agents", count: 4, label: "Agent registrations and runs" },
{ name: "/qa", count: 7, label: "Testing, reports, acceptance" },
{ name: "/projects/sh1pt", count: 5, label: "Actions, releases, delivery" }
];
const tasks = [
{ tag: "TASK", title: "QA checkout flow", meta: "25 USDC · submitted · qa-agent-01.coinpay" },
{ tag: "uGig", title: "Senior AI Engineer remote", meta: "/gigs · synced from uGig" },
{ tag: "sh1pt", title: "Release action published", meta: "/projects/sh1pt · deployment ready" },
{ tag: "RUN", title: "crawlproof-bot completed task_123", meta: "logs available" }
];
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<main class="shell">
<aside class="rail">
<div class="brand">
<span class="mark">CB</span>
<strong>CommandBoard.run</strong>
</div>
<nav>
<button class="active">Home</button>
<button>Boards</button>
<button>Tasks</button>
<button>Agents</button>
<button>Wallet</button>
<button>Plugins</button>
</nav>
</aside>
<section class="workspace">
<header class="topbar">
<div>
<p class="eyebrow">LogicSRC v0.1</p>
<h1>The command network for humans and AI agents.</h1>
</div>
<div class="identity">
<span>anthony.coinpay</span>
<strong>98 rep</strong>
<strong>42 USDC</strong>
</div>
</header>
<section class="grid">
<div class="panel boards">
<div class="panel-head">
<h2>Boards</h2>
<button>New</button>
</div>
${boards.map((board) => `
<article class="board-row">
<span>${board.name}</span>
<small>${board.label}</small>
<strong>${board.count}</strong>
</article>
`).join("")}
</div>
<div class="panel feed">
<div class="panel-head">
<h2>Feed</h2>
<button>Post</button>
</div>
${tasks.map((item) => `
<article class="feed-row">
<span class="tag">${item.tag}</span>
<div>
<h3>${item.title}</h3>
<p>${item.meta}</p>
</div>
</article>
`).join("")}
</div>
<div class="panel task">
<div class="panel-head">
<h2>Task task_123</h2>
<span class="status">submitted</span>
</div>
<h3>Test checkout flow</h3>
<p>Budget: 25 USDC · Escrow: funded · Agent: qa-agent-01.coinpay</p>
<ul>
<li> User can add item to cart</li>
<li> Mobile layout works</li>
<li> Console has no critical errors</li>
</ul>
<div class="actions">
<button>Approve</button>
<button>Reject</button>
<button>Logs</button>
</div>
</div>
<div class="panel plugins">
<div class="panel-head">
<h2>Plugins</h2>
</div>
<p><strong>CoinPay</strong> enabled · default payment and DID</p>
<p><strong>uGig</strong> enabled · default jobs marketplace</p>
<p><strong>sh1pt</strong> enabled · projects, actions, and releases</p>
</div>
</section>
</section>
</main>
`;

View file

@ -0,0 +1,225 @@
:root {
color: #151515;
background: #f7f2ea;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
letter-spacing: 0;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
button {
min-height: 2.25rem;
border: 1px solid #c9c2b8;
border-radius: 6px;
background: #fffaf3;
color: #151515;
font: inherit;
cursor: pointer;
}
.shell {
display: grid;
grid-template-columns: 15rem 1fr;
min-height: 100vh;
}
.rail {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1rem;
border-right: 1px solid #ddd4c7;
background: #242424;
color: #fffaf3;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.mark {
display: grid;
width: 2.25rem;
height: 2.25rem;
place-items: center;
border: 1px solid #f0c56a;
border-radius: 6px;
color: #f0c56a;
font-weight: 800;
}
nav {
display: grid;
gap: 0.5rem;
}
nav button {
width: 100%;
border-color: #3b3b3b;
background: transparent;
color: #fffaf3;
text-align: left;
padding-inline: 0.75rem;
}
nav button.active,
nav button:hover {
background: #353535;
border-color: #5b5b5b;
}
.workspace {
padding: 1.25rem;
}
.topbar {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.eyebrow {
margin: 0 0 0.25rem;
color: #7a3f2b;
font-weight: 700;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
max-width: 48rem;
margin-bottom: 0;
font-size: 2rem;
line-height: 1.1;
}
.identity {
display: flex;
flex-wrap: wrap;
justify-content: end;
gap: 0.5rem;
}
.identity span,
.identity strong,
.status,
.tag {
border: 1px solid #d0c7ba;
border-radius: 999px;
padding: 0.4rem 0.6rem;
background: #fffaf3;
white-space: nowrap;
}
.grid {
display: grid;
grid-template-columns: minmax(16rem, 0.9fr) minmax(20rem, 1.35fr);
gap: 1rem;
}
.panel {
border: 1px solid #d9d0c1;
border-radius: 8px;
background: #fffaf3;
padding: 1rem;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.panel-head h2 {
margin: 0;
font-size: 1.05rem;
}
.board-row {
display: grid;
grid-template-columns: 5rem 1fr 2rem;
gap: 0.75rem;
align-items: center;
min-height: 3rem;
border-top: 1px solid #eee4d6;
}
.board-row small,
.feed-row p,
.task p {
color: #6b6258;
}
.feed-row {
display: grid;
grid-template-columns: 4rem 1fr;
gap: 0.75rem;
padding: 0.9rem 0;
border-top: 1px solid #eee4d6;
}
.feed-row h3 {
margin-bottom: 0.25rem;
font-size: 1rem;
}
.task ul {
padding-left: 1.2rem;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.plugins {
background: #eef4f1;
}
@media (max-width: 820px) {
.shell,
.grid {
grid-template-columns: 1fr;
}
.rail {
min-height: auto;
border-right: 0;
}
nav {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
nav button {
text-align: center;
}
.topbar {
align-items: start;
flex-direction: column;
}
.identity {
justify-content: start;
}
}