agent-hub-sdd 0.1.2 → 0.1.4

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,17 @@ 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@latest
19
19
  ```
20
20
 
21
- Isso abre a janela do app. Na primeira vez o npm baixa o Electron e o Next.
21
+ Isso abre a **janela** do app. Na primeira vez o npm baixa o Electron e o Next. Se uma versão antiga ficou em cache e a janela não sobe, use a versão explícita:
22
+
23
+ ```powershell
24
+ npx --yes agent-hub-sdd@0.1.4
25
+ ```
22
26
 
23
27
  ## App no Windows (a partir do código)
24
28
 
package/electron/main.mjs CHANGED
@@ -1,8 +1,11 @@
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}`;
@@ -12,9 +15,66 @@ const repoRoot = path.join(here, "..");
12
15
  let win;
13
16
  let child;
14
17
  let startedServer = false;
18
+ let logs = "";
15
19
 
16
20
  function hubDir() {
17
- return app.isPackaged ? path.join(process.resourcesPath, "hub") : repoRoot;
21
+ const raw = app.isPackaged ? path.join(process.resourcesPath, "hub") : repoRoot;
22
+ return stageHub(raw);
23
+ }
24
+
25
+ function nodeName() {
26
+ return process.platform === "win32" ? "node.exe" : "node";
27
+ }
28
+
29
+ function nodeBin() {
30
+ const guessed = [
31
+ process.env.AGENT_HUB_NODE,
32
+ process.env.npm_node_execpath,
33
+ ];
34
+ const npmCli = process.env.npm_execpath;
35
+ if (npmCli) {
36
+ guessed.push(path.join(path.dirname(npmCli), nodeName()));
37
+ guessed.push(path.resolve(path.dirname(npmCli), "..", "..", "..", nodeName()));
38
+ }
39
+ if (process.platform === "win32") {
40
+ if (process.env.ProgramFiles) {
41
+ guessed.push(path.join(process.env.ProgramFiles, "nodejs", "node.exe"));
42
+ }
43
+ guessed.push("C:\\Program Files\\nodejs\\node.exe");
44
+ if (process.env.LOCALAPPDATA) {
45
+ guessed.push(path.join(process.env.LOCALAPPDATA, "Programs", "nodejs", "node.exe"));
46
+ }
47
+ }
48
+ for (const candidate of guessed) {
49
+ if (candidate && fs.existsSync(candidate)) return candidate;
50
+ }
51
+ return nodeName();
52
+ }
53
+
54
+ function nextBin(cwd) {
55
+ const starts = [path.join(cwd, "package.json"), fileURLToPath(import.meta.url)];
56
+ const errors = [];
57
+ for (const start of starts) {
58
+ try {
59
+ const req = createRequire(start);
60
+ const root = path.dirname(req.resolve("next/package.json"));
61
+ const bin = path.join(root, "dist", "bin", "next");
62
+ if (fs.existsSync(bin)) return bin;
63
+ errors.push(`Next em ${root}, mas falta dist/bin/next`);
64
+ } catch (error) {
65
+ errors.push(error instanceof Error ? error.message : String(error));
66
+ }
67
+ }
68
+ throw new Error(`Não achei o Next.js no pacote.\n${errors.join("\n")}`);
69
+ }
70
+
71
+ function withNodeOnPath(env, node) {
72
+ const next = { ...env };
73
+ if (!node || node === "node" || node === "node.exe") return next;
74
+ const dir = path.dirname(node);
75
+ const key = Object.keys(next).find((k) => k.toLowerCase() === "path") ?? "PATH";
76
+ next[key] = `${dir}${path.delimiter}${next[key] ?? ""}`;
77
+ return next;
18
78
  }
19
79
 
20
80
  function isUp() {
@@ -31,36 +91,60 @@ function isUp() {
31
91
  });
32
92
  }
33
93
 
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}`);
94
+ function note(chunk) {
95
+ logs = (logs + chunk.toString()).slice(-8000);
40
96
  }
41
97
 
42
98
  function startServer() {
43
99
  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",
100
+ const node = nodeBin();
101
+ const args = app.isPackaged
102
+ ? [path.join(cwd, "server.js")]
103
+ : [nextBin(cwd), "dev", "--webpack", "-H", "127.0.0.1", "-p", String(PORT)];
104
+ const env = withNodeOnPath(
105
+ { ...process.env, HOSTNAME: "127.0.0.1", PORT: String(PORT) },
106
+ node,
107
+ );
108
+ note(`\n${node} ${args.join(" ")}\n`);
109
+ const proc = spawn(node, args, {
110
+ cwd,
111
+ env,
112
+ stdio: ["ignore", "pipe", "pipe"],
113
+ windowsHide: true,
114
+ shell: false,
115
+ });
116
+ proc.stdout?.on("data", note);
117
+ proc.stderr?.on("data", note);
118
+ proc.on("error", (error) => note(`\n${error.message}\n`));
119
+ return proc;
120
+ }
121
+
122
+ function stopServer() {
123
+ if (!child || child.killed) return;
124
+ if (process.platform === "win32" && child.pid) {
125
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
52
126
  windowsHide: true,
53
127
  shell: false,
128
+ stdio: "ignore",
54
129
  });
130
+ return;
55
131
  }
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,
132
+ child.kill("SIGTERM");
133
+ }
134
+
135
+ async function waitForHub(proc) {
136
+ let exited;
137
+ proc?.once("exit", (code) => {
138
+ exited = code ?? 1;
63
139
  });
140
+ for (let i = 0; i < 180; i++) {
141
+ if (await isUp()) return;
142
+ if (exited !== undefined) {
143
+ throw new Error(`O servidor saiu com código ${exited}.\n${logs.trim() || "Sem log."}`);
144
+ }
145
+ await new Promise((r) => setTimeout(r, 500));
146
+ }
147
+ throw new Error(`O servidor não subiu em ${HUB}.\n${logs.trim() || "Sem log."}`);
64
148
  }
65
149
 
66
150
  function createWindow() {
@@ -110,23 +194,25 @@ app.whenReady().then(async () => {
110
194
  if (!(await isUp())) {
111
195
  child = startServer();
112
196
  startedServer = true;
113
- child.on("error", (error) => {
114
- void dialog.showErrorBox("Agent Hub", error.message);
115
- });
116
197
  }
117
- await waitForHub();
198
+ await waitForHub(startedServer ? child : null);
118
199
  createWindow();
119
200
  } catch (error) {
201
+ stopServer();
120
202
  const message = error instanceof Error ? error.message : String(error);
121
203
  await dialog.showErrorBox(
122
204
  "Agent Hub",
123
- `${message}\n\nNo Windows empacotado, o Node.js 20+ precisa estar no PATH para subir o hub.`,
205
+ `${message}\n\nO app precisa do Node.js 20+ (o mesmo node do npx). Instale em https://nodejs.org/ e rode: npx --yes agent-hub-sdd@0.1.4`,
124
206
  );
125
207
  app.quit();
126
208
  }
127
209
  });
128
210
 
211
+ app.on("before-quit", () => {
212
+ if (startedServer) stopServer();
213
+ });
214
+
129
215
  app.on("window-all-closed", () => {
130
- if (startedServer && child && !child.killed) child.kill();
216
+ if (startedServer) stopServer();
131
217
  app.quit();
132
218
  });
@@ -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.4" }));
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,34 @@
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
+ // npx installs this app under node_modules. Next's tsconfig-paths plugin
16
+ // skips any importer in node_modules, so @/ would 404 without a webpack alias.
17
+ webpack: (config) => {
18
+ config.resolve ??= {};
19
+ const alias = config.resolve.alias;
20
+ if (Array.isArray(alias)) {
21
+ alias.push({ name: "@", alias: src });
22
+ } else {
23
+ config.resolve.alias = { ...alias, "@": src };
24
+ }
25
+ return config;
26
+ },
27
+ turbopack: {
28
+ resolveAlias: {
29
+ "@": src,
30
+ },
31
+ },
10
32
  };
11
33
 
12
34
  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.4",
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,4 +1,4 @@
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
 
@@ -11,12 +11,16 @@ try {
11
11
  process.exit(1);
12
12
  }
13
13
 
14
+ const env = { ...process.env, AGENT_HUB_NODE: process.execPath };
15
+ delete env.ELECTRON_RUN_AS_NODE;
16
+ if (!env.npm_node_execpath) env.npm_node_execpath = process.execPath;
17
+
14
18
  const child = spawn(electron, [root], {
15
19
  cwd: root,
16
20
  stdio: "inherit",
17
- env: process.env,
21
+ env,
18
22
  windowsHide: false,
19
- shell: process.platform === "win32",
23
+ shell: false,
20
24
  });
21
25
  child.on("error", (error) => {
22
26
  console.error(error.message);