Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

The world's first open-source AI development platform covering the **complete software lifecycle** — from coding to deployment to monitoring. **7 packages**, **56 source files**, **~10,000 LoC** — all free, private, and multi-model.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify file counts in each package phase

echo "=== File counts by phase ==="
for phase in cli scanner healer sdk testgen deployer guardian; do
  file_count=$(find packages/$phase -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" 2>/dev/null | wc -l)
  echo "packages/$phase: $file_count files"
done

echo ""
echo "=== Total files ==="
total=$(find packages -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" 2>/dev/null | wc -l)
echo "Total: $total files"

Repository: 0xgetz/nexusforge

Length of output: 306


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== All-file counts by phase (exclude node_modules/.next/dist) ==="
for phase in cli scanner healer sdk testgen deployer guardian; do
  file_count=$(find packages/$phase -type f \
    ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" 2>/dev/null | wc -l)
  echo "packages/$phase: $file_count files (all types)"
done

echo ""
echo "=== Total all-file count (exclude node_modules/.next/dist) ==="
total=$(find packages -type f \
  ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" 2>/dev/null | wc -l)
echo "Total: $total files (all types)"

Repository: 0xgetz/nexusforge

Length of output: 486


Fix README “source file” counts to match package structure (guardian + total are currently off)

  • README.md (line 7) claims guardian = 10 files and total source files = 56.
  • Counting packages/* while excluding node_modules, .next, and dist:
    • Code files (*.ts/*.tsx/*.js/*.json): total 77, guardian 13
    • All files (any type): total 98, guardian 16
  • Update the README figures (and the underlying counting definition/script) so “source files”/LoC are computed consistently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 7, The README's source-file and guardian counts are
incorrect; update README.md (the banner line) to reflect the corrected counts
and change the counting definition/script so both "source files" and LoC are
computed consistently by counting code files (*.ts, *.tsx, *.js, *.json) while
excluding node_modules, .next, and dist. Specifically: replace the old figures
(guardian = 10, total source files = 56) with the new validated numbers
(guardian = 13, total source files = 77 for code files; optionally list
all-files totals if desired), and modify the repository counting script (the
script that generates these stats) to use the precise include/exclude globs and
to output guardian vs total consistently so README.md can be regenerated from
it. Ensure the README text describing what "source files" and LoC include
matches the script's rules.


[![CI/CD Pipeline](https://github.com/0xgetz/nexusforge/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/0xgetz/nexusforge/actions/workflows/ci-cd.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-emerald.svg)](https://opensource.org/licenses/MIT)
[![Phase](https://img.shields.io/badge/7_Phases-Complete-brightgreen.svg)](#roadmap)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/0xgetz/nexusforge/pulls)
Expand Down Expand Up @@ -398,8 +399,8 @@ node packages/guardian/dist/index.js changelog --output CHANGELOG.md
| **Phase 4** | `@nexusforge/sdk` | Plugin SDK, events, hooks, registry | 7 | 740 | ✅ Complete |
| **Phase 5** | `@nexusforge/testgen` | AI test generation, coverage, mutation | 8 | 2,187 | ✅ Complete |
| **Phase 6** | `@nexusforge/deployer` | Multi-cloud deploy, IaC, CI/CD pipelines | 14 | 1,730 | ✅ Complete |
| **Phase 7** | `@nexusforge/guardian` | Code review, metrics, architecture, docs | 12 | 2,363 | ✅ Complete |
| | | **Total** | **58** | **~9,875** | **🎉 All Complete** |
| **Phase 7** | `@nexusforge/guardian` | Code review, metrics, architecture, docs | 10 | 2,363 | ✅ Complete |
| | | **Total** | **56** | **~10,000** | **🎉 All Complete** |

## 📊 Comparison

Expand Down
920 changes: 454 additions & 466 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ const nextConfig = {
typescript: {
ignoreBuildErrors: false,
},
// Linting is handled by Biome (see biome.json) and runs in CI via `bun run lint`.
// Next's built-in ESLint integration stays disabled to avoid running two linters.
eslint: {
ignoreDuringBuilds: true,
},
};

module.exports = nextConfig;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^22.0.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
2 changes: 1 addition & 1 deletion postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
"@tailwindcss/postcss": {},
},
};

Expand Down
5 changes: 2 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Manrope:wght@300;400;500;600;700;800&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@config "../../tailwind.config.ts";

@layer base {
:root {
Expand Down
2 changes: 1 addition & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Config } from "tailwindcss";

export default {
darkMode: ["class"],
darkMode: "class",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
Expand Down
37 changes: 37 additions & 0 deletions tests/deployer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, test, expect } from "bun:test";
import { getRecommendedProvider } from "../packages/deployer/src/detector.js";
import type { ProjectConfig } from "../packages/deployer/src/types.js";

function makeConfig(type: ProjectConfig["type"]): ProjectConfig {
return {
type,
runtime: "node",
runtimeVersion: "22",
framework: type,
buildCommand: "build",
startCommand: "start",
outputDir: "dist",
port: 3000,
envVars: [],
hasDatabase: false,
hasDocker: false,
hasCICD: false,
packageManager: "npm",
};
}

describe("getRecommendedProvider", () => {
test("recommends vercel for Next.js", () => {
expect(getRecommendedProvider(makeConfig("nextjs"))).toBe("vercel");
});

test("recommends netlify for static frontends", () => {
expect(getRecommendedProvider(makeConfig("react"))).toBe("netlify");
expect(getRecommendedProvider(makeConfig("vue"))).toBe("netlify");
});

test("recommends docker for compiled backends", () => {
expect(getRecommendedProvider(makeConfig("go"))).toBe("docker");
expect(getRecommendedProvider(makeConfig("rust"))).toBe("docker");
});
});
44 changes: 44 additions & 0 deletions tests/guardian.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, test, expect } from "bun:test";
import { getBuiltinRules, filterRules, createRule } from "../packages/guardian/src/rules.js";

describe("guardian rules", () => {
test("ships a non-empty set of built-in rules with unique ids", () => {
const rules = getBuiltinRules();
expect(rules.length).toBeGreaterThanOrEqual(14);
const ids = rules.map((r) => r.id);
expect(new Set(ids).size).toBe(ids.length);
});

test("getBuiltinRules returns a fresh copy each call", () => {
const a = getBuiltinRules();
a.pop();
expect(getBuiltinRules().length).toBeGreaterThan(a.length);
});

test("filterRules narrows by severity", () => {
const critical = filterRules(getBuiltinRules(), { severity: "critical" });
expect(critical.length).toBeGreaterThan(0);
expect(critical.every((r) => r.severity === "critical")).toBe(true);
});

test("filterRules narrows by category", () => {
const security = filterRules(getBuiltinRules(), { category: "security" });
expect(security.every((r) => r.category === "security")).toBe(true);
});
Comment on lines +24 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid a vacuous pass in the category filter test.

At Line 26, every(...) is true on an empty array, so this can pass even if no "security" rules are returned. Add a non-empty assertion first.

Suggested patch
   test("filterRules narrows by category", () => {
     const security = filterRules(getBuiltinRules(), { category: "security" });
+    expect(security.length).toBeGreaterThan(0);
     expect(security.every((r) => r.category === "security")).toBe(true);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/guardian.test.ts` around lines 24 - 27, The test "filterRules narrows
by category" currently can vacuously pass because security.every(...) is true
for an empty array; update the test for filterRules/getBuiltinRules to first
assert the result is non-empty (e.g., expect(security.length).toBeGreaterThan(0)
or expect(security).not.toHaveLength(0)) and then keep the existing
expect(security.every((r) => r.category === "security")).toBe(true") to ensure
the category filter actually returned rules.


test("filterRules excludes disabled rules unless asked", () => {
const rules = [
createRule({ id: "T1", name: "on", pattern: "a", message: "m", enabled: true }),
createRule({ id: "T2", name: "off", pattern: "b", message: "m", enabled: false }),
];
expect(filterRules(rules)).toHaveLength(1);
expect(filterRules(rules, { enabledOnly: false })).toHaveLength(2);
});

test("createRule applies sensible defaults", () => {
const rule = createRule({ id: "X1", name: "x", pattern: "p", message: "m" });
expect(rule.severity).toBe("minor");
expect(rule.category).toBe("best-practice");
expect(rule.enabled).toBe(true);
});
});
76 changes: 76 additions & 0 deletions tests/scanner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, test, expect } from "bun:test";
import { mkdtempSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { toJSON, toMarkdown, toSARIF } from "../packages/scanner/src/reporter.js";
import { detectEcosystem } from "../packages/scanner/src/scanner.js";
import type { ScanResult } from "../packages/scanner/src/types.js";

const sampleResult: ScanResult = {
projectName: "demo",
projectPath: "/tmp/demo",
timestamp: new Date("2026-01-01T00:00:00Z").toISOString(),
duration: 42,
ecosystem: "npm",
totalDependencies: 3,
vulnerablePackages: 1,
vulnerabilities: [
{
id: "GHSA-xxxx-yyyy-zzzz",
package: "leftpad",
version: "1.0.0",
severity: "high",
title: "Prototype pollution",
description: "A prototype pollution vulnerability.",
fixedIn: "1.0.1",
url: "https://example.com/advisory",
},
],
dependencies: [],
summary: { critical: 0, high: 1, medium: 0, low: 0, info: 0, total: 1, score: 85 },
};

describe("scanner reporters", () => {
test("toJSON round-trips the result", () => {
const parsed = JSON.parse(toJSON(sampleResult));
expect(parsed.projectName).toBe("demo");
expect(parsed.summary.score).toBe(85);
expect(parsed.vulnerabilities).toHaveLength(1);
});

test("toMarkdown includes the score and vulnerability id", () => {
const md = toMarkdown(sampleResult);
expect(md).toContain("# NexusForge Security Report");
expect(md).toContain("85/100");
expect(md).toContain("GHSA-xxxx-yyyy-zzzz");
});

test("toSARIF emits valid SARIF 2.1.0 with one result", () => {
const sarif = JSON.parse(toSARIF(sampleResult));
expect(sarif.version).toBe("2.1.0");
expect(sarif.runs[0].tool.driver.name).toBe("NexusForge Scanner");
expect(sarif.runs[0].results).toHaveLength(1);
// high severity maps to an "error" level
expect(sarif.runs[0].results[0].level).toBe("error");
expect(sarif.runs[0].results[0].ruleId).toBe("GHSA-xxxx-yyyy-zzzz");
});
});

describe("detectEcosystem", () => {
test("detects npm from package.json", () => {
const dir = mkdtempSync(join(tmpdir(), "nxf-"));
writeFileSync(join(dir, "package.json"), "{}");
expect(detectEcosystem(dir)).toBe("npm");
});

test("detects cargo from Cargo.toml", () => {
const dir = mkdtempSync(join(tmpdir(), "nxf-"));
writeFileSync(join(dir, "Cargo.toml"), "");
expect(detectEcosystem(dir)).toBe("cargo");
});

test("returns unknown for an empty directory", () => {
const dir = mkdtempSync(join(tmpdir(), "nxf-"));
expect(detectEcosystem(dir)).toBe("unknown");
});
});
108 changes: 108 additions & 0 deletions tests/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, test, expect } from "bun:test";
import {
EventBus,
HookRegistry,
definePlugin,
createPluginContext,
} from "../packages/sdk/src/index.js";

describe("EventBus", () => {
test("delivers payloads to subscribers", async () => {
const bus = new EventBus();
const received: number[] = [];
bus.on<number>("ping", (n) => received.push(n));

await bus.emit("ping", 1);
await bus.emit("ping", 2);

expect(received).toEqual([1, 2]);
});

test("once() only fires a single time", async () => {
const bus = new EventBus();
let calls = 0;
bus.once("boot", () => calls++);

await bus.emit("boot");
await bus.emit("boot");

expect(calls).toBe(1);
expect(bus.listenerCount("boot")).toBe(0);
});

test("the unsubscribe handle removes the listener", async () => {
const bus = new EventBus();
let calls = 0;
const off = bus.on("tick", () => calls++);

await bus.emit("tick");
off();
await bus.emit("tick");

expect(calls).toBe(1);
});

test("a throwing handler does not block other handlers", async () => {
const bus = new EventBus();
let reached = false;
bus.on("x", () => {
throw new Error("boom");
});
bus.on("x", () => {
reached = true;
});

await bus.emit("x");

expect(reached).toBe(true);
});
});

describe("HookRegistry", () => {
test("executes handlers ordered by ascending priority", async () => {
const registry = new HookRegistry();
const order: string[] = [];
const ctx = createPluginContext("/tmp", "test");

registry.register("onInit", "late", () => void order.push("late"), 100);
registry.register("onInit", "early", () => void order.push("early"), 1);

await registry.execute("onInit", {}, ctx);

expect(order).toEqual(["early", "late"]);
});

test("unregister removes a plugin's handlers", async () => {
const registry = new HookRegistry();
registry.register("onInit", "p1", () => {});

expect(registry.hasListeners("onInit")).toBe(true);
registry.unregister("onInit", "p1");
expect(registry.hasListeners("onInit")).toBe(false);
});
});

describe("definePlugin", () => {
test("builds a manifest and exposes command handlers", () => {
const plugin = definePlugin({
name: "greeter",
version: "1.0.0",
description: "says hi",
permissions: ["fs:read"],
activate() {},
commands: {
hello: {
description: "Say hello",
handler: (args) => `Hello, ${args.name ?? "World"}!`,
},
},
});

expect(plugin.manifest.name).toBe("greeter");
expect(plugin.manifest.permissions).toEqual(["fs:read"]);
expect(plugin.manifest.commands?.[0]?.name).toBe("hello");

const ctx = createPluginContext("/tmp", "greeter");
expect(plugin.commands?.hello({ name: "Forge" }, ctx)).toBe("Hello, Forge!");
});
});
29 changes: 29 additions & 0 deletions tests/testgen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, test, expect } from "bun:test";
import { getMutatorDescriptions } from "../packages/testgen/src/mutator.js";
import { buildTestFileName } from "../packages/testgen/src/generator.js";
import { getFrameworkConfig } from "../packages/testgen/src/frameworks.js";

describe("testgen mutators", () => {
test("exposes a description for every mutator type", () => {
const descriptions = getMutatorDescriptions();
expect(Object.keys(descriptions)).toContain("arithmetic");
expect(Object.keys(descriptions)).toContain("conditional");
for (const value of Object.values(descriptions)) {
expect(typeof value).toBe("string");
expect(value.length).toBeGreaterThan(0);
}
});
});

describe("testgen helpers", () => {
test("buildTestFileName swaps the extension for the test extension", () => {
expect(buildTestFileName("src/utils.ts", ".test.ts")).toBe("utils.test.ts");
expect(buildTestFileName("/abs/path/auth.py", "_test.py")).toBe("auth_test.py");
});

test("getFrameworkConfig returns the matching framework metadata", () => {
const vitest = getFrameworkConfig("vitest");
expect(vitest.name).toBe("vitest");
expect(vitest.testExtension).toBe(".test.ts");
});
});
8 changes: 5 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -34,9 +34,11 @@
".next/types/**/*.ts",
"build/types/**/*.ts",
"next-env.d.ts",
".next-build/types/**/*.ts"
".next-build/types/**/*.ts",
".next-build/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"tests"
]
}
Loading