agent-hub-sdd 0.1.0 → 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
@@ -6,18 +6,23 @@ App **Windows** para orquestrar personas do SDLC no seu próprio repositório. N
6
6
 
7
7
  Node 20+ no PATH, depois:
8
8
 
9
+ Na pasta do repositório (depois de `npm install`):
10
+
9
11
  ```powershell
10
- npx agent-hub-sdd
12
+ npm run app
11
13
  ```
12
14
 
13
- Ou global:
15
+ Pelo npm (precisa do Node 20+ no PATH):
14
16
 
15
17
  ```powershell
16
- npm install -g agent-hub-sdd
17
- agent-hub
18
+ npx --yes agent-hub-sdd@latest
18
19
  ```
19
20
 
20
- 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
+ ```
21
26
 
22
27
  ## App no Windows (a partir do código)
23
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,18 +1,32 @@
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
+ },
10
27
  turbopack: {
11
- rules: {
12
- "*.css": {
13
- loaders: ["@tailwindcss/turbopack"],
14
- as: "*.css",
15
- },
28
+ resolveAlias: {
29
+ "@": src,
16
30
  },
17
31
  },
18
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub-sdd",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Agent Hub",
@@ -18,7 +18,8 @@
18
18
  "url": "git+https://github.com/GustavoCostaAM/agent-hub.git"
19
19
  },
20
20
  "bin": {
21
- "agent-hub": "scripts/cli.mjs"
21
+ "agent-hub": "scripts/cli.cjs",
22
+ "agent-hub-sdd": "scripts/cli.cjs"
22
23
  },
23
24
  "files": [
24
25
  "electron",
@@ -35,7 +36,7 @@
35
36
  },
36
37
  "main": "electron/main.mjs",
37
38
  "scripts": {
38
- "dev": "next dev --hostname 127.0.0.1 --port 43147",
39
+ "dev": "next dev --webpack --hostname 127.0.0.1 --port 43147",
39
40
  "app": "electron .",
40
41
  "start": "next start --hostname 127.0.0.1 --port 43147",
41
42
  "build": "next build",
@@ -58,12 +59,12 @@
58
59
  "shadcn": "^4.22.1",
59
60
  "tailwindcss": "^4",
60
61
  "tw-animate-css": "^1.4.0",
61
- "typescript": "^5"
62
- },
63
- "devDependencies": {
62
+ "typescript": "^5",
64
63
  "@types/node": "^22.20.5",
65
64
  "@types/react": "^19",
66
- "@types/react-dom": "^19",
65
+ "@types/react-dom": "^19"
66
+ },
67
+ "devDependencies": {
67
68
  "electron-builder": "^26.18.0",
68
69
  "eslint": "^9",
69
70
  "eslint-config-next": "16.4.3",
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require("node:child_process");
3
+ const path = require("node:path");
4
+
5
+ const root = path.join(__dirname, "..");
6
+ let electron;
7
+ try {
8
+ electron = require("electron");
9
+ } catch {
10
+ console.error("Falta o Electron. Na pasta do projeto rode: npm install");
11
+ process.exit(1);
12
+ }
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
+
18
+ const child = spawn(electron, [root], {
19
+ cwd: root,
20
+ stdio: "inherit",
21
+ env,
22
+ windowsHide: false,
23
+ shell: false,
24
+ });
25
+ child.on("error", (error) => {
26
+ console.error(error.message);
27
+ process.exit(1);
28
+ });
29
+ child.on("exit", (code, signal) => {
30
+ if (signal) process.kill(process.pid, signal);
31
+ process.exit(code ?? 0);
32
+ });
package/scripts/cli.mjs CHANGED
@@ -1,26 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
2
  import { createRequire } from "node:module";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
8
- const require = createRequire(import.meta.url);
9
- let electron;
10
- try {
11
- electron = require("electron");
12
- } catch {
13
- console.error("Falta o Electron. Rode: npm install");
14
- process.exit(1);
15
- }
16
-
17
- const child = spawn(electron, [root], {
18
- cwd: root,
19
- stdio: "inherit",
20
- env: process.env,
21
- windowsHide: false,
22
- });
23
- child.on("exit", (code, signal) => {
24
- if (signal) process.kill(process.pid, signal);
25
- process.exit(code ?? 0);
26
- });
3
+ createRequire(import.meta.url)("./cli.cjs");
@@ -183,11 +183,11 @@ function launch(bin: string, args: string[], cwd: string) {
183
183
  };
184
184
  const command = [quoteCmdExe(bin), ...argv.map(quoteCmdArg)].join(" ");
185
185
  const child = viaCmd
186
- ? spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${command}"`], {
186
+ ? spawn(/* turbopackIgnore: true */ process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${command}"`], {
187
187
  ...options,
188
188
  windowsVerbatimArguments: true,
189
189
  })
190
- : spawn(bin, argv, options);
190
+ : spawn(/* turbopackIgnore: true */ bin, argv, options);
191
191
  if (useStdin && child.stdin) {
192
192
  child.stdin.on("error", () => {});
193
193
  child.stdin.write(prompt);