agent-hub-sdd 0.1.2 → 0.1.6

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/README.md CHANGED
@@ -12,13 +12,30 @@ Na pasta do repositório (depois de `npm install`):
12
12
  npm run app
13
13
  ```
14
14
 
15
- Pelo npm, quando a versão 0.1.1 estiver publicada:
15
+ Pelo npm (precisa do Node 20+ no PATH):
16
16
 
17
17
  ```powershell
18
- npx agent-hub-sdd
18
+ npx --yes agent-hub-sdd@0.1.6
19
19
  ```
20
20
 
21
- Isso abre a janela do app. Na primeira vez o npm baixa o Electron e o Next.
21
+ Na primeira abertura a janela diz que está compilando. O **N** no canto é o Next.js em modo dev, não o app. Sem `@0.1.6`, o Windows pode reusar cache antigo.
22
+
23
+ Se ainda abrir a 0.1.2, ou se o npm disser `EBUSY` / `resource busy or locked`, o Next da tentativa antiga ainda está com o cache aberto. O `Stop-Process electron` não mata esse `node`:
24
+
25
+ ```powershell
26
+ Get-CimInstance Win32_Process | Where-Object {
27
+ $_.CommandLine -and ($_.CommandLine -match 'agent-hub|_npx|next dev|43147')
28
+ } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
29
+ Remove-Item -Recurse -Force "$env:LOCALAPPDATA\npm-cache\_npx" -ErrorAction SilentlyContinue
30
+ npx --yes agent-hub-sdd@0.1.6
31
+ ```
32
+
33
+ Instalar global evita o rename do `npx` nessa pasta:
34
+
35
+ ```powershell
36
+ npm install -g agent-hub-sdd@0.1.6
37
+ agent-hub-sdd
38
+ ```
22
39
 
23
40
  ## App no Windows (a partir do código)
24
41
 
package/electron/main.mjs CHANGED
@@ -1,66 +1,170 @@
1
1
  import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
2
3
  import http from "node:http";
4
+ import { createRequire } from "node:module";
3
5
  import path from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
8
+ import { stageHub } from "./stage-hub.mjs";
6
9
 
7
10
  const PORT = Number(process.env.AGENT_HUB_PORT || 43147);
8
11
  const HUB = `http://127.0.0.1:${PORT}`;
9
12
  const here = path.dirname(fileURLToPath(import.meta.url));
10
13
  const repoRoot = path.join(here, "..");
14
+ const VERSION = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
11
15
 
12
16
  let win;
13
17
  let child;
14
18
  let startedServer = false;
19
+ let logs = "";
15
20
 
16
21
  function hubDir() {
17
- return app.isPackaged ? path.join(process.resourcesPath, "hub") : repoRoot;
22
+ if (app.isPackaged) return path.join(process.resourcesPath, "hub");
23
+ const dist = path.join(repoRoot, "dist-hub");
24
+ if (fs.existsSync(path.join(dist, "server.js"))) return dist;
25
+ return stageHub(repoRoot);
18
26
  }
19
27
 
20
- function isUp() {
28
+ function nodeName() {
29
+ return process.platform === "win32" ? "node.exe" : "node";
30
+ }
31
+
32
+ function nodeBin() {
33
+ const guessed = [
34
+ process.env.AGENT_HUB_NODE,
35
+ process.env.npm_node_execpath,
36
+ ];
37
+ const npmCli = process.env.npm_execpath;
38
+ if (npmCli) {
39
+ guessed.push(path.join(path.dirname(npmCli), nodeName()));
40
+ guessed.push(path.resolve(path.dirname(npmCli), "..", "..", "..", nodeName()));
41
+ }
42
+ if (process.platform === "win32") {
43
+ if (process.env.ProgramFiles) {
44
+ guessed.push(path.join(process.env.ProgramFiles, "nodejs", "node.exe"));
45
+ }
46
+ guessed.push("C:\\Program Files\\nodejs\\node.exe");
47
+ if (process.env.LOCALAPPDATA) {
48
+ guessed.push(path.join(process.env.LOCALAPPDATA, "Programs", "nodejs", "node.exe"));
49
+ }
50
+ }
51
+ for (const candidate of guessed) {
52
+ if (candidate && fs.existsSync(candidate)) return candidate;
53
+ }
54
+ return nodeName();
55
+ }
56
+
57
+ function nextBin(cwd) {
58
+ const starts = [path.join(cwd, "package.json"), fileURLToPath(import.meta.url)];
59
+ const errors = [];
60
+ for (const start of starts) {
61
+ try {
62
+ const req = createRequire(start);
63
+ const root = path.dirname(req.resolve("next/package.json"));
64
+ const bin = path.join(root, "dist", "bin", "next");
65
+ if (fs.existsSync(bin)) return bin;
66
+ errors.push(`Next em ${root}, mas falta dist/bin/next`);
67
+ } catch (error) {
68
+ errors.push(error instanceof Error ? error.message : String(error));
69
+ }
70
+ }
71
+ throw new Error(`Não achei o Next.js no pacote.\n${errors.join("\n")}`);
72
+ }
73
+
74
+ function withNodeOnPath(env, node) {
75
+ const next = { ...env };
76
+ if (!node || node === "node" || node === "node.exe") return next;
77
+ const dir = path.dirname(node);
78
+ const key = Object.keys(next).find((k) => k.toLowerCase() === "path") ?? "PATH";
79
+ next[key] = `${dir}${path.delimiter}${next[key] ?? ""}`;
80
+ return next;
81
+ }
82
+
83
+ function hubHtml() {
21
84
  return new Promise((resolve) => {
22
85
  const req = http.get(HUB, (res) => {
23
- res.resume();
24
- resolve(true);
86
+ const chunks = [];
87
+ res.on("data", (chunk) => {
88
+ chunks.push(chunk);
89
+ if (Buffer.concat(chunks).length > 40000) res.destroy();
90
+ });
91
+ res.on("end", () => {
92
+ resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") });
93
+ });
25
94
  });
26
- req.on("error", () => resolve(false));
27
- req.setTimeout(800, () => {
95
+ req.on("error", () => resolve(null));
96
+ req.setTimeout(20000, () => {
28
97
  req.destroy();
29
- resolve(false);
98
+ resolve(null);
30
99
  });
31
100
  });
32
101
  }
33
102
 
34
- async function waitForHub() {
35
- for (let i = 0; i < 120; i++) {
36
- if (await isUp()) return;
37
- await new Promise((r) => setTimeout(r, 250));
38
- }
39
- throw new Error(`O servidor do Agent Hub não subiu em ${HUB}`);
103
+ function pageReady(html) {
104
+ if (!html) return false;
105
+ if (/Can't resolve|Module not found|Module parse failed/.test(html.body)) return false;
106
+ return html.status === 200 && /Agent Hub|Carregando o hub/.test(html.body);
107
+ }
108
+
109
+ function note(chunk) {
110
+ logs = (logs + chunk.toString()).slice(-8000);
40
111
  }
41
112
 
42
113
  function startServer() {
43
114
  const cwd = hubDir();
44
- const env = { ...process.env, HOSTNAME: "127.0.0.1", PORT: String(PORT) };
45
- if (app.isPackaged) {
46
- const serverJs = path.join(cwd, "server.js");
47
- const node = process.platform === "win32" ? "node.exe" : "node";
48
- return spawn(node, [serverJs], {
49
- cwd,
50
- env,
51
- stdio: "ignore",
115
+ const node = nodeBin();
116
+ const serverJs = path.join(cwd, "server.js");
117
+ const args = fs.existsSync(serverJs)
118
+ ? [serverJs]
119
+ : [nextBin(cwd), "dev", "--webpack", "-H", "127.0.0.1", "-p", String(PORT)];
120
+ const env = withNodeOnPath(
121
+ { ...process.env, HOSTNAME: "127.0.0.1", PORT: String(PORT) },
122
+ node,
123
+ );
124
+ note(`\n${node} ${args.join(" ")}\n`);
125
+ const proc = spawn(node, args, {
126
+ cwd,
127
+ env,
128
+ stdio: ["ignore", "pipe", "pipe"],
129
+ windowsHide: true,
130
+ shell: false,
131
+ });
132
+ proc.stdout?.on("data", note);
133
+ proc.stderr?.on("data", note);
134
+ proc.on("error", (error) => note(`\n${error.message}\n`));
135
+ return proc;
136
+ }
137
+
138
+ function stopServer() {
139
+ if (!child || child.killed) return;
140
+ if (process.platform === "win32" && child.pid) {
141
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
52
142
  windowsHide: true,
53
143
  shell: false,
144
+ stdio: "ignore",
54
145
  });
146
+ return;
55
147
  }
56
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
57
- return spawn(npm, ["run", "dev"], {
58
- cwd,
59
- env,
60
- stdio: "inherit",
61
- shell: process.platform === "win32",
62
- windowsHide: false,
148
+ child.kill("SIGTERM");
149
+ }
150
+
151
+ async function waitForHub(proc) {
152
+ let exited;
153
+ proc?.once("exit", (code) => {
154
+ exited = code ?? 1;
63
155
  });
156
+ for (let i = 0; i < 24; i++) {
157
+ const html = await hubHtml();
158
+ if (pageReady(html)) return;
159
+ if (html?.body && /Can't resolve|Module not found|Module parse failed/.test(html.body)) {
160
+ throw new Error(`Falha ao compilar o hub.\n${html.body.slice(0, 1500)}`);
161
+ }
162
+ if (exited !== undefined) {
163
+ throw new Error(`O servidor saiu com código ${exited}.\n${logs.trim() || "Sem log."}`);
164
+ }
165
+ await new Promise((r) => setTimeout(r, 500));
166
+ }
167
+ throw new Error(`O servidor não subiu em ${HUB}.\n${logs.trim() || "Sem log."}`);
64
168
  }
65
169
 
66
170
  function createWindow() {
@@ -88,7 +192,17 @@ function createWindow() {
88
192
  },
89
193
  });
90
194
  win.once("ready-to-show", () => win.show());
91
- win.loadURL(HUB);
195
+ void win.loadURL(
196
+ `data:text/html;charset=utf-8,${encodeURIComponent(`<!doctype html>
197
+ <html lang="pt-BR">
198
+ <body style="margin:0;background:#000;color:#e4e4e7;font-family:'Segoe UI',sans-serif;display:flex;min-height:100vh;align-items:center;justify-content:center">
199
+ <div style="text-align:center;max-width:28rem;padding:1.5rem">
200
+ <p style="font-size:1.15rem;margin:0 0 .75rem">Agent Hub ${VERSION}</p>
201
+ <p style="color:#a1a1aa;line-height:1.6;margin:0">Primeira abertura compila o hub. Pode levar alguns minutos. Não feche a janela.</p>
202
+ </div>
203
+ </body>
204
+ </html>`)}`,
205
+ );
92
206
  win.webContents.setWindowOpenHandler(({ url }) => {
93
207
  void shell.openExternal(url);
94
208
  return { action: "deny" };
@@ -107,26 +221,30 @@ ipcMain.handle("pick-folder", async () => {
107
221
 
108
222
  app.whenReady().then(async () => {
109
223
  try {
110
- if (!(await isUp())) {
224
+ createWindow();
225
+ const html = await hubHtml();
226
+ if (!pageReady(html)) {
111
227
  child = startServer();
112
228
  startedServer = true;
113
- child.on("error", (error) => {
114
- void dialog.showErrorBox("Agent Hub", error.message);
115
- });
229
+ await waitForHub(child);
116
230
  }
117
- await waitForHub();
118
- createWindow();
231
+ await win.loadURL(HUB);
119
232
  } catch (error) {
233
+ stopServer();
120
234
  const message = error instanceof Error ? error.message : String(error);
121
235
  await dialog.showErrorBox(
122
236
  "Agent Hub",
123
- `${message}\n\nNo Windows empacotado, o Node.js 20+ precisa estar no PATH para subir o hub.`,
237
+ `${message}\n\nAgent Hub ${VERSION}. O app precisa do Node.js 20+ (o mesmo node do npx).\nRode: npx --yes agent-hub-sdd@${VERSION}`,
124
238
  );
125
239
  app.quit();
126
240
  }
127
241
  });
128
242
 
243
+ app.on("before-quit", () => {
244
+ if (startedServer) stopServer();
245
+ });
246
+
129
247
  app.on("window-all-closed", () => {
130
- if (startedServer && child && !child.killed) child.kill();
248
+ if (startedServer) stopServer();
131
249
  app.quit();
132
250
  });
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const COPY = ["src", "public", "next.config.ts", "tsconfig.json", "components.json", "package.json"];
6
+
7
+ export function isNestedPackage(dir) {
8
+ return path.basename(path.dirname(path.resolve(dir))) === "node_modules";
9
+ }
10
+
11
+ export function hubHome() {
12
+ return process.env.AGENT_HUB_HOME || path.join(os.homedir(), ".agent-hub");
13
+ }
14
+
15
+ function unlinkQuiet(target) {
16
+ try {
17
+ const st = fs.lstatSync(target);
18
+ if (st.isSymbolicLink()) {
19
+ fs.unlinkSync(target);
20
+ return;
21
+ }
22
+ } catch {
23
+ return;
24
+ }
25
+ fs.rmSync(target, { recursive: true, force: true });
26
+ }
27
+
28
+ function linkModules(from, to) {
29
+ try {
30
+ if (fs.existsSync(to) && fs.realpathSync(to) === fs.realpathSync(from)) return;
31
+ } catch {
32
+ // broken link
33
+ }
34
+ unlinkQuiet(to);
35
+ fs.symlinkSync(from, to, process.platform === "win32" ? "junction" : "dir");
36
+ }
37
+
38
+ export function stageHub(srcDir, destDir) {
39
+ const src = path.resolve(srcDir);
40
+ if (!isNestedPackage(src)) return src;
41
+ const dest = path.resolve(destDir ?? path.join(hubHome(), "runtime"));
42
+ fs.mkdirSync(dest, { recursive: true });
43
+ const version = JSON.parse(fs.readFileSync(path.join(src, "package.json"), "utf8")).version;
44
+ const stamp = path.join(dest, ".hub-version");
45
+ const same = fs.existsSync(stamp) && fs.readFileSync(stamp, "utf8").trim() === version;
46
+ if (!same) {
47
+ for (const name of COPY) {
48
+ const from = path.join(src, name);
49
+ if (!fs.existsSync(from)) continue;
50
+ fs.cpSync(from, path.join(dest, name), { recursive: true, force: true });
51
+ }
52
+ fs.writeFileSync(stamp, `${version}\n`);
53
+ }
54
+ linkModules(path.join(src, ".."), path.join(dest, "node_modules"));
55
+ return dest;
56
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { isNestedPackage, stageHub } from "./stage-hub.mjs";
6
+
7
+ const temps: string[] = [];
8
+
9
+ afterEach(() => {
10
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
11
+ });
12
+
13
+ describe("stageHub", () => {
14
+ it("leaves a git clone in place", () => {
15
+ expect(isNestedPackage("/workspace")).toBe(false);
16
+ expect(stageHub("/workspace")).toBe(path.resolve("/workspace"));
17
+ });
18
+
19
+ it("copies an npx install out of node_modules", () => {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "hub-stage-"));
21
+ temps.push(root);
22
+ const pkg = path.join(root, "node_modules", "agent-hub-sdd");
23
+ fs.mkdirSync(path.join(pkg, "src", "components"), { recursive: true });
24
+ fs.writeFileSync(path.join(pkg, "package.json"), JSON.stringify({ name: "agent-hub-sdd", version: "0.1.6" }));
25
+ fs.writeFileSync(path.join(pkg, "src", "components", "desktop-chrome.tsx"), "export const DesktopChrome = () => null;\n");
26
+ fs.writeFileSync(path.join(root, "node_modules", "sentinel"), "ok");
27
+ const dest = path.join(root, "runtime");
28
+ expect(stageHub(pkg, dest)).toBe(dest);
29
+ expect(fs.readFileSync(path.join(dest, "src", "components", "desktop-chrome.tsx"), "utf8")).toContain("DesktopChrome");
30
+ expect(fs.readFileSync(path.join(dest, "node_modules", "sentinel"), "utf8")).toBe("ok");
31
+ expect(fs.realpathSync(path.join(dest, "src")).includes(`${path.sep}node_modules${path.sep}`)).toBe(false);
32
+ });
33
+ });
package/next.config.ts CHANGED
@@ -1,12 +1,35 @@
1
1
  import type { NextConfig } from "next";
2
+ import path from "node:path";
3
+
4
+ const projectRoot = process.cwd();
5
+ const src = path.join(projectRoot, "src");
2
6
 
3
7
  const nextConfig: NextConfig = {
4
- /* config options here */
5
8
  output: "standalone",
9
+ outputFileTracingRoot: projectRoot,
6
10
  // Cache Components rejects `runtime` / `dynamic` on route handlers. The hub
7
11
  // APIs stay Node and force-dynamic so the JSON store and SSE turns are not prerendered.
8
12
  cacheComponents: false,
9
13
  partialPrefetching: false,
14
+ agentRules: false,
15
+ devIndicators: false,
16
+ // npx installs this app under node_modules. Next's tsconfig-paths plugin
17
+ // skips any importer in node_modules, so @/ would 404 without a webpack alias.
18
+ webpack: (config) => {
19
+ config.resolve ??= {};
20
+ const alias = config.resolve.alias;
21
+ if (Array.isArray(alias)) {
22
+ alias.push({ name: "@", alias: src });
23
+ } else {
24
+ config.resolve.alias = { ...alias, "@": src };
25
+ }
26
+ return config;
27
+ },
28
+ turbopack: {
29
+ resolveAlias: {
30
+ "@": src,
31
+ },
32
+ },
10
33
  };
11
34
 
12
35
  export default nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub-sdd",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Agent Hub",
@@ -59,12 +59,12 @@
59
59
  "shadcn": "^4.22.1",
60
60
  "tailwindcss": "^4",
61
61
  "tw-animate-css": "^1.4.0",
62
- "typescript": "^5"
63
- },
64
- "devDependencies": {
62
+ "typescript": "^5",
65
63
  "@types/node": "^22.20.5",
66
64
  "@types/react": "^19",
67
- "@types/react-dom": "^19",
65
+ "@types/react-dom": "^19"
66
+ },
67
+ "devDependencies": {
68
68
  "electron-builder": "^26.18.0",
69
69
  "eslint": "^9",
70
70
  "eslint-config-next": "16.4.3",
package/scripts/cli.cjs CHANGED
@@ -1,8 +1,10 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const { spawn } = require("node:child_process");
3
3
  const path = require("node:path");
4
4
 
5
5
  const root = path.join(__dirname, "..");
6
+ const pkg = require(path.join(root, "package.json"));
7
+ console.log(`Agent Hub ${pkg.version}`);
6
8
  let electron;
7
9
  try {
8
10
  electron = require("electron");
@@ -11,12 +13,16 @@ try {
11
13
  process.exit(1);
12
14
  }
13
15
 
16
+ const env = { ...process.env, AGENT_HUB_NODE: process.execPath };
17
+ delete env.ELECTRON_RUN_AS_NODE;
18
+ if (!env.npm_node_execpath) env.npm_node_execpath = process.execPath;
19
+
14
20
  const child = spawn(electron, [root], {
15
21
  cwd: root,
16
22
  stdio: "inherit",
17
- env: process.env,
23
+ env,
18
24
  windowsHide: false,
19
- shell: process.platform === "win32",
25
+ shell: false,
20
26
  });
21
27
  child.on("error", (error) => {
22
28
  console.error(error.message);
@@ -0,0 +1,11 @@
1
+ $ErrorActionPreference = "SilentlyContinue"
2
+ Get-CimInstance Win32_Process |
3
+ Where-Object {
4
+ $_.CommandLine -and
5
+ ($_.CommandLine -match "agent-hub|_npx|next dev|43147")
6
+ } |
7
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
8
+ Stop-Process -Name electron -Force
9
+ $npx = Join-Path $env:LOCALAPPDATA "npm-cache\_npx"
10
+ if (Test-Path $npx) { Remove-Item -Recurse -Force $npx }
11
+ Write-Host "Cache do npx limpo. Rode: npx --yes agent-hub-sdd@0.1.6"
@@ -7,8 +7,8 @@
7
7
  @theme inline {
8
8
  --color-background: var(--background);
9
9
  --color-foreground: var(--foreground);
10
- --font-sans: var(--font-geist-sans);
11
- --font-mono: var(--font-geist-mono);
10
+ --font-sans: "Segoe UI", ui-sans-serif, system-ui, sans-serif;
11
+ --font-mono: "Cascadia Mono", Consolas, ui-monospace, monospace;
12
12
  --font-heading: var(--font-sans);
13
13
  --color-sidebar-ring: var(--sidebar-ring);
14
14
  --color-sidebar-border: var(--sidebar-border);
@@ -1,19 +1,8 @@
1
1
  import type { Metadata } from "next";
2
- import { Geist, Geist_Mono } from "next/font/google";
3
2
  import { DesktopChrome } from "@/components/desktop-chrome";
4
3
  import { TooltipProvider } from "@/components/ui/tooltip";
5
4
  import "./globals.css";
6
5
 
7
- const geistSans = Geist({
8
- variable: "--font-geist-sans",
9
- subsets: ["latin"],
10
- });
11
-
12
- const geistMono = Geist_Mono({
13
- variable: "--font-geist-mono",
14
- subsets: ["latin"],
15
- });
16
-
17
6
  export const metadata: Metadata = {
18
7
  title: "Agent Hub",
19
8
  description: "Harness visual local para agentes do SDLC",
@@ -21,10 +10,7 @@ export const metadata: Metadata = {
21
10
 
22
11
  export default function RootLayout({ children }: LayoutProps<"/">) {
23
12
  return (
24
- <html
25
- lang="pt-BR"
26
- className={`${geistSans.variable} ${geistMono.variable} dark h-full antialiased`}
27
- >
13
+ <html lang="pt-BR" className="dark h-full antialiased">
28
14
  <body className="flex min-h-dvh flex-col bg-black text-zinc-100">
29
15
  <TooltipProvider>
30
16
  <DesktopChrome />