@bamptee/aia-code 0.10.0 → 1.0.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.
@@ -0,0 +1,119 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Dashboard } from '/components/dashboard.js';
4
+ import { FeatureDetail } from '/components/feature-detail.js';
5
+ import { ConfigView } from '/components/config-view.js';
6
+
7
+ // --- API client ---
8
+ export const api = {
9
+ async get(path) {
10
+ const res = await fetch(`/api${path}`);
11
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
12
+ return res.json();
13
+ },
14
+ async post(path, body = {}) {
15
+ const res = await fetch(`/api${path}`, {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify(body),
19
+ });
20
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
21
+ return res.json();
22
+ },
23
+ async put(path, body = {}) {
24
+ const res = await fetch(`/api${path}`, {
25
+ method: 'PUT',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(body),
28
+ });
29
+ if (!res.ok) throw new Error((await res.json()).error || res.statusText);
30
+ return res.json();
31
+ },
32
+ };
33
+
34
+ // --- SSE stream helper ---
35
+ // POST with SSE response. Calls onLog(text) for each log chunk, returns { ok, error }.
36
+ export async function streamPost(path, body, { onLog, onStatus }) {
37
+ const res = await fetch(`/api${path}`, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(body),
41
+ });
42
+
43
+ const reader = res.body.getReader();
44
+ const decoder = new TextDecoder();
45
+ let buffer = '';
46
+ let result = { ok: true };
47
+
48
+ while (true) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+ buffer += decoder.decode(value, { stream: true });
52
+
53
+ const lines = buffer.split('\n');
54
+ buffer = lines.pop(); // keep incomplete line
55
+
56
+ let eventType = null;
57
+ for (const line of lines) {
58
+ if (line.startsWith('event: ')) {
59
+ eventType = line.slice(7);
60
+ } else if (line.startsWith('data: ') && eventType) {
61
+ try {
62
+ const data = JSON.parse(line.slice(6));
63
+ if (eventType === 'log' && onLog) {
64
+ onLog(data.text, data.type);
65
+ } else if (eventType === 'status' && onStatus) {
66
+ onStatus(data);
67
+ } else if (eventType === 'error') {
68
+ result = { ok: false, error: data.message };
69
+ } else if (eventType === 'done') {
70
+ result = { ok: true, ...data };
71
+ }
72
+ } catch {}
73
+ eventType = null;
74
+ }
75
+ }
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ // --- Simple hash router ---
82
+ function useHashRoute() {
83
+ const [route, setRoute] = React.useState(window.location.hash || '#/');
84
+ React.useEffect(() => {
85
+ const handler = () => setRoute(window.location.hash || '#/');
86
+ window.addEventListener('hashchange', handler);
87
+ return () => window.removeEventListener('hashchange', handler);
88
+ }, []);
89
+ return route;
90
+ }
91
+
92
+ function parseRoute(hash) {
93
+ if (hash.startsWith('#/features/')) {
94
+ return { page: 'feature', name: decodeURIComponent(hash.slice('#/features/'.length)) };
95
+ }
96
+ if (hash === '#/config') return { page: 'config' };
97
+ return { page: 'dashboard' };
98
+ }
99
+
100
+ // --- App ---
101
+ function App() {
102
+ const hash = useHashRoute();
103
+ const { page, name } = parseRoute(hash);
104
+
105
+ return React.createElement('div', { className: 'min-h-screen' },
106
+ React.createElement('nav', { className: 'border-b border-aia-border px-6 py-3 flex items-center gap-6' },
107
+ React.createElement('a', { href: '#/', className: 'text-aia-accent font-bold text-lg hover:text-sky-300' }, 'AIA'),
108
+ React.createElement('a', { href: '#/', className: 'text-slate-400 hover:text-slate-200 text-sm' }, 'Features'),
109
+ React.createElement('a', { href: '#/config', className: 'text-slate-400 hover:text-slate-200 text-sm' }, 'Config'),
110
+ ),
111
+ React.createElement('main', { className: 'max-w-6xl mx-auto p-6' },
112
+ page === 'dashboard' ? React.createElement(Dashboard) :
113
+ page === 'feature' ? React.createElement(FeatureDetail, { name }) :
114
+ page === 'config' ? React.createElement(ConfigView) : null
115
+ )
116
+ );
117
+ }
118
+
119
+ createRoot(document.getElementById('root')).render(React.createElement(App));
@@ -0,0 +1,47 @@
1
+ export function createRouter() {
2
+ const routes = [];
3
+
4
+ function add(method, pattern, handler) {
5
+ const keys = [];
6
+ const regex = new RegExp(
7
+ '^' + pattern.replace(/:([^/]+)/g, (_, key) => { keys.push(key); return '([^/]+)'; }) + '$'
8
+ );
9
+ routes.push({ method, regex, keys, handler });
10
+ }
11
+
12
+ function match(method, pathname) {
13
+ for (const route of routes) {
14
+ if (route.method !== method) continue;
15
+ const m = pathname.match(route.regex);
16
+ if (m) {
17
+ const params = {};
18
+ route.keys.forEach((key, i) => { params[key] = decodeURIComponent(m[i + 1]); });
19
+ return { handler: route.handler, params };
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ return { add, match, get: (p, h) => add('GET', p, h), post: (p, h) => add('POST', p, h), put: (p, h) => add('PUT', p, h), delete: (p, h) => add('DELETE', p, h) };
26
+ }
27
+
28
+ export async function parseBody(req) {
29
+ return new Promise((resolve, reject) => {
30
+ let body = '';
31
+ req.on('data', chunk => { body += chunk; });
32
+ req.on('end', () => {
33
+ try { resolve(body ? JSON.parse(body) : {}); }
34
+ catch (e) { reject(new Error('Invalid JSON body')); }
35
+ });
36
+ req.on('error', reject);
37
+ });
38
+ }
39
+
40
+ export function json(res, data, status = 200) {
41
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
42
+ res.end(JSON.stringify(data));
43
+ }
44
+
45
+ export function error(res, message, status = 500) {
46
+ json(res, { error: message }, status);
47
+ }
@@ -0,0 +1,105 @@
1
+ import { createServer } from 'node:http';
2
+ import { createConnection } from 'node:net';
3
+ import { createReadStream, existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createRouter, parseBody, json, error } from './router.js';
7
+ import { registerApiRoutes } from './api/index.js';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const MIME_TYPES = {
12
+ '.html': 'text/html',
13
+ '.js': 'application/javascript',
14
+ '.css': 'text/css',
15
+ '.json': 'application/json',
16
+ '.svg': 'image/svg+xml',
17
+ '.png': 'image/png',
18
+ '.ico': 'image/x-icon',
19
+ };
20
+
21
+ function serveStatic(res, filePath) {
22
+ const ext = path.extname(filePath);
23
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
24
+
25
+ if (!existsSync(filePath)) {
26
+ // SPA fallback: serve index.html for non-API, non-file routes
27
+ const indexPath = path.join(__dirname, 'public', 'index.html');
28
+ if (existsSync(indexPath)) {
29
+ res.writeHead(200, { 'Content-Type': 'text/html' });
30
+ createReadStream(indexPath).pipe(res);
31
+ return;
32
+ }
33
+ res.writeHead(404);
34
+ res.end('Not found');
35
+ return;
36
+ }
37
+
38
+ res.writeHead(200, { 'Content-Type': mime });
39
+ createReadStream(filePath).pipe(res);
40
+ }
41
+
42
+ function isPortFree(port) {
43
+ return new Promise((resolve) => {
44
+ const socket = createConnection({ port, host: '127.0.0.1' });
45
+ socket.once('connect', () => { socket.destroy(); resolve(false); });
46
+ socket.once('error', () => { resolve(true); });
47
+ });
48
+ }
49
+
50
+ async function findFreePort(start = 3100, maxAttempts = 50) {
51
+ for (let port = start; port < start + maxAttempts; port++) {
52
+ if (await isPortFree(port)) return port;
53
+ }
54
+ throw new Error(`No free port found between ${start} and ${start + maxAttempts - 1}`);
55
+ }
56
+
57
+ export async function startServer(preferredPort, root = process.cwd()) {
58
+ const port = preferredPort ?? await findFreePort();
59
+
60
+ const router = createRouter();
61
+ registerApiRoutes(router, root);
62
+
63
+ const server = createServer(async (req, res) => {
64
+ // CORS preflight
65
+ if (req.method === 'OPTIONS') {
66
+ res.writeHead(204, {
67
+ 'Access-Control-Allow-Origin': '*',
68
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
69
+ 'Access-Control-Allow-Headers': 'Content-Type',
70
+ });
71
+ res.end();
72
+ return;
73
+ }
74
+
75
+ const url = new URL(req.url, `http://${req.headers.host}`);
76
+ const pathname = url.pathname;
77
+
78
+ // API routes
79
+ if (pathname.startsWith('/api/')) {
80
+ const matched = router.match(req.method, pathname);
81
+ if (matched) {
82
+ try {
83
+ await matched.handler(req, res, { params: matched.params, query: Object.fromEntries(url.searchParams), root, parseBody: () => parseBody(req) });
84
+ } catch (err) {
85
+ error(res, err.message, 500);
86
+ }
87
+ } else {
88
+ error(res, 'Not found', 404);
89
+ }
90
+ return;
91
+ }
92
+
93
+ // Static files
94
+ let filePath = path.join(__dirname, 'public', pathname === '/' ? 'index.html' : pathname);
95
+ serveStatic(res, filePath);
96
+ });
97
+
98
+ return new Promise((resolve, reject) => {
99
+ server.on('error', reject);
100
+ server.listen(port, '127.0.0.1', () => {
101
+ console.log(`AIA UI running at http://127.0.0.1:${port}`);
102
+ resolve({ server, port });
103
+ });
104
+ });
105
+ }