diff --git a/org-profile-README.md b/org-profile-README.md
new file mode 100644
index 0000000..5f019ed
--- /dev/null
+++ b/org-profile-README.md
@@ -0,0 +1,80 @@
+
+
+# Arhitekton
+
+**Building open-source tools for developers**
+
+[](https://github.com/Arhitekton)
+
+
+
+---
+
+## About
+
+Arhitekton is an open-source organization focused on building tools that make developers' lives easier. We work across three areas:
+
+- **Open-source tooling** — developer utilities, CLI tools, and libraries you can use and contribute to
+- **Web applications** — full-stack web projects built with modern technologies
+- **Mobile applications** — cross-platform mobile experiences
+
+We believe good tooling should be free, accessible, and community-driven.
+
+---
+
+## Projects
+
+| Project | Description | Status |
+|---|---|---|
+| [Code Visualizer](https://github.com/Arhitekton/GitCodeVisualizer) | Analyze source code and generate interactive dependency graphs | 🚧 Active |
+
+> More projects coming soon.
+
+---
+
+## Contributors
+
+
+
+---
+
+## Contributing
+
+We welcome contributions of all kinds — bug fixes, features, documentation, or ideas.
+
+1. Browse open issues across our repositories
+2. Fork the repo and create a branch (`feat/your-feature`)
+3. Open a pull request with a clear description
+
+Please follow the contribution guidelines in each project's `CONTRIBUTING.md`.
+
+---
+
+## License
+
+All projects under Arhitekton are open-source and released under the [MIT License](https://opensource.org/licenses/MIT) unless stated otherwise.
+
+---
+
+
+
+*Built with care by the Arhitekton team*
+
+
diff --git a/package-lock.json b/package-lock.json
index 3c6a340..b3766d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"license": "MIT",
"dependencies": {
"commander": "13.1.0",
+ "open": "^10.1.0",
"typescript": "5.8.3"
},
"bin": {
@@ -2084,6 +2085,21 @@
"node": ">=8"
}
},
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/bundle-require": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz",
@@ -2263,6 +2279,46 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/default-browser": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+ "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2809,6 +2865,21 @@
"node": ">=0.8.19"
}
},
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2832,6 +2903,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -2842,6 +2931,21 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3464,6 +3568,24 @@
"node": ">=12.20.0"
}
},
+ "node_modules/open": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
+ "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3823,6 +3945,18 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
diff --git a/package.json b/package.json
index ccdcf1a..33df1fe 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"packageManager": "npm@10.9.2",
"dependencies": {
"commander": "13.1.0",
+ "open": "^10.1.0",
"typescript": "5.8.3"
},
"devDependencies": {
diff --git a/src/cli.ts b/src/cli.ts
index 7f3c506..eec96f5 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -3,6 +3,7 @@ import { Command } from 'commander'
import { VERSION } from './index.js'
import { analyze } from './analyze.js'
import { renderToTerminal } from './renderer.js'
+import { serveGraph } from './serve.js'
import { generateHtmlReport } from './html-report.js'
const program = new Command()
@@ -12,25 +13,32 @@ program
.description('Analyze source code and generate interactive dependency graphs')
.version(VERSION)
.argument('[directory]', 'source directory to analyze', '.')
- .option('--html', 'generate an interactive HTML report in the report/ directory')
- .option('--out-dir ', 'output directory for the HTML report', 'report')
+ .option('--html', 'open an interactive graph in the browser (live server, no files written)')
+ .option('--save', 'write the HTML report to disk instead of serving it')
+ .option('--out-dir ', 'output directory when using --save', 'report')
+ .option('--port ', 'port for the live server (default: 4242)', '4242')
.option('--show-external', 'include external npm packages in the output')
.option('--ignore ', 'comma-separated list of directory names to ignore', '')
.action(async (
directory: string,
- options: { html?: boolean; outDir?: string; showExternal?: boolean; ignore?: string },
+ options: { html?: boolean; save?: boolean; outDir?: string; port?: string; showExternal?: boolean; ignore?: string },
) => {
const ignore = options.ignore ? options.ignore.split(',').map((d) => d.trim()).filter(Boolean) : []
try {
const graph = await analyze(directory, { ignore })
- if (options.html) {
+ if (options.save) {
const { htmlFile, jsonFile } = await generateHtmlReport(graph, { outDir: options.outDir })
- console.log(`\n HTML report generated:`)
+ console.log(`\n HTML report saved:`)
console.log(` ${htmlFile}`)
console.log(` ${jsonFile}`)
console.log(`\n Open in browser: file://${htmlFile}\n`)
+ } else if (options.html) {
+ const port = parseInt(options.port ?? '4242', 10)
+ const url = await serveGraph(graph, { port })
+ console.log(`\n Serving graph at ${url}`)
+ console.log(` Press Ctrl+C to stop.\n`)
} else {
renderToTerminal(graph, { showExternal: options.showExternal })
}
diff --git a/src/html-report.ts b/src/html-report.ts
index 94cf7c3..39382ef 100644
--- a/src/html-report.ts
+++ b/src/html-report.ts
@@ -37,7 +37,7 @@ interface SerializedGraph {
cycles: string[][]
}
-function serialize(graph: DependencyGraph): SerializedGraph {
+export function serializeGraph(graph: DependencyGraph): SerializedGraph {
return {
nodes: [...graph.nodes.values()].map((n) => ({
id: n.id,
@@ -58,7 +58,7 @@ function serialize(graph: DependencyGraph): SerializedGraph {
// HTML template
// ---------------------------------------------------------------------------
-function buildHtml(title: string, graphJson: string): string {
+export function buildHtml(title: string, graphJson: string): string {
return /* html */ `
@@ -603,7 +603,7 @@ export async function generateHtmlReport(
await mkdir(outDir, { recursive: true })
- const serialized = serialize(graph)
+ const serialized = serializeGraph(graph)
const graphJson = JSON.stringify(serialized, null, 2)
const jsonFile = join(outDir, 'graph.json')
diff --git a/src/index.ts b/src/index.ts
index e10bfed..6ae5137 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,6 +7,7 @@ export { extractDependencies } from './extractor.js'
export { buildGraph } from './graph.js'
export { renderToTerminal } from './renderer.js'
export { generateHtmlReport } from './html-report.js'
+export { serveGraph } from './serve.js'
export type { AnalyzeOptions } from './analyze.js'
export type { ScanOptions } from './scanner.js'
@@ -15,3 +16,4 @@ export type { Dependency } from './extractor.js'
export type { DependencyGraph, GraphNode, GraphEdge } from './graph.js'
export type { RenderOptions } from './renderer.js'
export type { HtmlReportOptions, HtmlReportResult } from './html-report.js'
+export type { ServeOptions } from './serve.js'
diff --git a/src/serve.ts b/src/serve.ts
new file mode 100644
index 0000000..358ba86
--- /dev/null
+++ b/src/serve.ts
@@ -0,0 +1,60 @@
+import { createServer } from 'node:http'
+import type { AddressInfo } from 'node:net'
+import type { DependencyGraph } from './graph.js'
+import { serializeGraph, buildHtml } from './html-report.js'
+
+export interface ServeOptions {
+ /** Preferred port — falls back to a random free port if taken. Defaults to 4242. */
+ port?: number
+ /** Page title */
+ title?: string
+}
+
+/**
+ * Starts an in-process HTTP server that serves the interactive graph report,
+ * opens the browser automatically, and keeps running until the user hits Ctrl+C.
+ *
+ * Returns the bound URL.
+ */
+export async function serveGraph(graph: DependencyGraph, options: ServeOptions = {}): Promise {
+ const title = options.title ?? 'codeviz — Dependency Graph'
+ const graphJson = JSON.stringify(serializeGraph(graph))
+ const html = buildHtml(title, graphJson)
+
+ const server = createServer((_req, res) => {
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
+ res.end(html)
+ })
+
+ const port = await listen(server, options.port ?? 4242)
+ const url = `http://localhost:${port}`
+
+ // Dynamic import so the ESM-only `open` package works inside a CJS bundle
+ const { default: open } = await import('open')
+ await open(url)
+
+ process.on('SIGINT', () => {
+ server.close()
+ process.exit(0)
+ })
+
+ return url
+}
+
+/** Binds the server to `preferredPort`, retrying on a random port if EADDRINUSE. */
+function listen(server: ReturnType, preferredPort: number): Promise {
+ return new Promise((resolve, reject) => {
+ server.once('error', (err: NodeJS.ErrnoException) => {
+ if (err.code === 'EADDRINUSE') {
+ server.listen(0, '127.0.0.1', () => {
+ resolve((server.address() as AddressInfo).port)
+ })
+ } else {
+ reject(err)
+ }
+ })
+ server.listen(preferredPort, '127.0.0.1', () => {
+ resolve((server.address() as AddressInfo).port)
+ })
+ })
+}