stealthos-cli 0.1.0-alpha.3 → 0.1.0-alpha.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/ai/CONTRACT.md +110 -0
- package/ai/INDEX.md +203 -0
- package/ai/README.md +434 -0
- package/ai/ROUTER.md +288 -0
- package/ai/agents/README.md +103 -0
- package/ai/agents/architect.md +59 -0
- package/ai/agents/backend-engineer.md +62 -0
- package/ai/agents/founder.md +45 -0
- package/ai/agents/frontend-engineer.md +61 -0
- package/ai/agents/product-manager.md +56 -0
- package/ai/agents/qa-engineer.md +53 -0
- package/ai/agents/researcher.md +74 -0
- package/ai/agents/reviewer.md +73 -0
- package/ai/agents/security-engineer.md +59 -0
- package/ai/agents/sre-engineer.md +70 -0
- package/ai/agents/tech-lead.md +70 -0
- package/ai/architecture/README.md +35 -0
- package/ai/architecture/components.md +24 -0
- package/ai/architecture/containers.md +30 -0
- package/ai/architecture/event-flows.md +36 -0
- package/ai/architecture/sequence-diagrams.md +38 -0
- package/ai/architecture/system-context.md +46 -0
- package/ai/architecture/threat-modeling.md +40 -0
- package/ai/blueprints/README.md +67 -0
- package/ai/blueprints/_schema.json +40 -0
- package/ai/blueprints/ai-platform.json +28 -0
- package/ai/blueprints/crm.json +22 -0
- package/ai/blueprints/game.json +25 -0
- package/ai/blueprints/mobile.json +24 -0
- package/ai/blueprints/realtime.json +22 -0
- package/ai/blueprints/saas.json +25 -0
- package/ai/blueprints/telemetry.json +30 -0
- package/ai/blueprints/web.json +23 -0
- package/ai/bootstrap/discovery-questions.md +117 -0
- package/ai/bootstrap/dispatcher.md +85 -0
- package/ai/bootstrap/existing-project.md +191 -0
- package/ai/bootstrap/new-project.md +127 -0
- package/ai/bootstrap/tech-mapping.md +164 -0
- package/ai/clients/README.md +114 -0
- package/ai/clients/antigravity.md +125 -0
- package/ai/clients/claude-code.md +65 -0
- package/ai/clients/cline.md +69 -0
- package/ai/clients/codex-aider-cli.md +82 -0
- package/ai/clients/continue.md +67 -0
- package/ai/clients/copilot.md +49 -0
- package/ai/clients/cursor.md +81 -0
- package/ai/clients/snippets/mcp-absolute-paths.json +9 -0
- package/ai/clients/snippets/mcp-http.json +7 -0
- package/ai/clients/snippets/mcp-stdio.json +9 -0
- package/ai/clients/trae.md +69 -0
- package/ai/clients/windsurf.md +71 -0
- package/ai/core/pipeline/execution-engine.md +157 -0
- package/ai/engineering/README.md +32 -0
- package/ai/engineering/observability/incident-response.md +82 -0
- package/ai/evals/protocol-tests.md +150 -0
- package/ai/evolution/agent-evolution.md +161 -0
- package/ai/evolution/improvements.md +91 -0
- package/ai/evolution/learnings.md +49 -0
- package/ai/evolution/patterns-discovered.md +48 -0
- package/ai/execution/README.md +33 -0
- package/ai/execution/backlog.md +27 -0
- package/ai/execution/milestones.md +26 -0
- package/ai/execution/roadmap.md +30 -0
- package/ai/execution/sprint.md +42 -0
- package/ai/governance/README.md +34 -0
- package/ai/governance/architecture-principles.md +99 -0
- package/ai/governance/definition-of-done.md +88 -0
- package/ai/governance/definition-of-ready.md +69 -0
- package/ai/governance/engineering-principles.md +70 -0
- package/ai/governance/quality-gates.md +85 -0
- package/ai/governance/security-policies.md +84 -0
- package/ai/hooks/enforce-audit.ps1 +41 -0
- package/ai/hooks/enforce-audit.sh +39 -0
- package/ai/hooks/guard-edit.ps1 +182 -0
- package/ai/hooks/guard-edit.sh +161 -0
- package/ai/hooks/inject-os-reminder.ps1 +40 -0
- package/ai/hooks/inject-os-reminder.sh +16 -0
- package/ai/manifest.json +238 -0
- package/ai/memory/_detected-stack.json +33 -0
- package/ai/memory/_summary.md +49 -0
- package/ai/memory/archive/.gitkeep +3 -0
- package/ai/memory/completed-tasks.md +156 -0
- package/ai/memory/decisions.md +257 -0
- package/ai/memory/errors-and-solutions.md +41 -0
- package/ai/memory/known-issues.md +40 -0
- package/ai/memory/pending-tasks.md +37 -0
- package/ai/memory/project-context.md +67 -0
- package/ai/operating-system/architecture.md +54 -0
- package/ai/operating-system/coding-standards.md +84 -0
- package/ai/operating-system/folder-structure.md +126 -0
- package/ai/operating-system/performance-rules.md +86 -0
- package/ai/operating-system/quality-control.md +81 -0
- package/ai/operating-system/security-rules.md +91 -0
- package/ai/operating-system/workflow.md +86 -0
- package/ai/product/README.md +24 -0
- package/ai/product/business-rules.md +26 -0
- package/ai/product/personas.md +29 -0
- package/ai/product/user-journeys.md +30 -0
- package/ai/product/vision.md +35 -0
- package/ai/rules/behavior.md +45 -0
- package/ai/rules/do.md +47 -0
- package/ai/rules/dont.md +46 -0
- package/ai/rules/execution-flow.md +125 -0
- package/ai/rules/structural-constraints.md +59 -0
- package/ai/rules/structure-canon.md +116 -0
- package/ai/runtime.md +179 -0
- package/ai/scripts/detect-stack.ps1 +166 -0
- package/ai/scripts/detect-stack.sh +172 -0
- package/ai/scripts/init-ai-os.ps1 +170 -0
- package/ai/scripts/init-ai-os.sh +99 -0
- package/ai/scripts/lint-os.ps1 +99 -0
- package/ai/scripts/lint-os.sh +85 -0
- package/ai/scripts/start-os.ps1 +151 -0
- package/ai/scripts/start-os.sh +141 -0
- package/ai/server/README.md +105 -0
- package/ai/server/aios-server.mjs +2134 -0
- package/ai/server/package-lock.json +802 -0
- package/ai/server/package.json +31 -0
- package/ai/server/src/analyzer/graph-builder.ts +92 -0
- package/ai/server/src/analyzer/index.ts +191 -0
- package/ai/server/src/analyzer/module-mapper.ts +171 -0
- package/ai/server/src/analyzer/smell-detector.ts +54 -0
- package/ai/server/src/analyzer/stack-detector.ts +70 -0
- package/ai/server/src/index.ts +16 -0
- package/ai/server/src/packager/context-builder.ts +217 -0
- package/ai/server/src/packager/index.ts +3 -0
- package/ai/server/src/packager/memory-injector.ts +128 -0
- package/ai/server/src/packager/module-summarizer.ts +60 -0
- package/ai/server/src/packager/token-estimator.ts +26 -0
- package/ai/server/src/snapshot/index.ts +3 -0
- package/ai/server/src/snapshot/snapshot-creator.ts +206 -0
- package/ai/server/src/snapshot/snapshot-diff.ts +86 -0
- package/ai/server/src/snapshot/snapshot-restore.ts +14 -0
- package/ai/server/src/types.ts +94 -0
- package/ai/server/tsconfig.json +26 -0
- package/ai/skills/architecture-design.md +82 -0
- package/ai/skills/backend-engineering.md +57 -0
- package/ai/skills/database-design.md +76 -0
- package/ai/skills/frontend-engineering.md +63 -0
- package/ai/skills/performance.md +73 -0
- package/ai/skills/scalability.md +84 -0
- package/ai/skills/security.md +71 -0
- package/ai/skills/testing.md +77 -0
- package/ai/specs/ADR/ADR-0002-typescript-runtime.md +103 -0
- package/ai/specs/ADR/ADR-0004-runtime-orchestrator.md +94 -0
- package/ai/specs/ADR/ADR-0005-workflow-engine.md +105 -0
- package/ai/specs/ADR/ADR-0006-runtime-state.md +104 -0
- package/ai/specs/ADR/ADR-0007-state-compiler-drift-context-layers-artifact-index.md +82 -0
- package/ai/specs/ADR/ADR-0008-intent-runtime-discovery-branching.md +93 -0
- package/ai/specs/ADR/ADR-0009-confidence-system-maturity-tracking.md +113 -0
- package/ai/specs/ADR/ADR-0010-structural-architecture-standards.md +121 -0
- package/ai/specs/ADR/ADR-0011-mcp-prompts.md +86 -0
- package/ai/specs/ADR/ADR-0012-stealthos-hybrid-architecture.md +174 -0
- package/ai/specs/ADR/_TEMPLATE.md +60 -0
- package/ai/specs/BRD/_TEMPLATE.md +50 -0
- package/ai/specs/PRD/_TEMPLATE.md +72 -0
- package/ai/specs/README.md +43 -0
- package/ai/specs/RFC/RFC-0001-runtime-orchestrator.md +149 -0
- package/ai/specs/RFC/RFC-0002-runtime-orchestrator-extended.md +134 -0
- package/ai/specs/RFC/_TEMPLATE.md +61 -0
- package/ai/specs/RUNBOOKS/_TEMPLATE.md +68 -0
- package/ai/specs/SDD/_TEMPLATE.md +104 -0
- package/ai/specs/TASKS/_TEMPLATE.md +52 -0
- package/ai/tools/debugging.md +64 -0
- package/ai/tools/dependency-analysis.md +46 -0
- package/ai/tools/internet-research.md +42 -0
- package/ai/tools/mcp-discovery.md +44 -0
- package/ai/workflows/_schema.json +81 -0
- package/ai/workflows/init.json +148 -0
- package/ai/workflows/sync.json +71 -0
- package/ai/workflows/work.json +91 -0
- package/package.json +7 -1
- package/scripts/bundle-ai.mjs +58 -0
- package/src/cli.mjs +1 -1
- package/src/commands/install.mjs +35 -11
- package/src/lib/resolve-source.mjs +27 -10
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-os-server",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI Operating System — MCP daemon + Runtime v0.1 (Project State / Context Packager / Snapshot)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "aios-server.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aios-server": "./aios-server.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node aios-server.mjs",
|
|
12
|
+
"http": "node aios-server.mjs --http",
|
|
13
|
+
"build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.mjs --external:ts-morph --external:commander",
|
|
14
|
+
"build:standalone": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.mjs",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"ts-morph": "^22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.12.0",
|
|
28
|
+
"esbuild": "^0.21.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { dirname, join, normalize } from "node:path";
|
|
2
|
+
import type { DependencyGraph, ModuleInfo } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export function buildGraph(modules: ModuleInfo[]): DependencyGraph {
|
|
5
|
+
const nodeSet = new Set(modules.map((m) => m.id));
|
|
6
|
+
const edges: Array<{ from: string; to: string }> = [];
|
|
7
|
+
|
|
8
|
+
for (const m of modules) {
|
|
9
|
+
for (const imp of m.imports) {
|
|
10
|
+
const resolved = resolveImport(m.id, imp, nodeSet);
|
|
11
|
+
if (resolved && resolved !== m.id) {
|
|
12
|
+
edges.push({ from: m.id, to: resolved });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cycles = detectCycles(Array.from(nodeSet), edges);
|
|
18
|
+
return { nodes: Array.from(nodeSet), edges, cycles };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveImport(fromModule: string, spec: string, knownNodes: Set<string>): string | null {
|
|
22
|
+
if (!spec.startsWith(".")) return null;
|
|
23
|
+
const fromDir = dirname(fromModule);
|
|
24
|
+
const target = normalize(join(fromDir, spec)).replace(/\\/g, "/");
|
|
25
|
+
const candidates = [
|
|
26
|
+
target,
|
|
27
|
+
target + ".ts",
|
|
28
|
+
target + ".tsx",
|
|
29
|
+
target + ".mts",
|
|
30
|
+
target + ".js",
|
|
31
|
+
target + ".mjs",
|
|
32
|
+
target + ".jsx",
|
|
33
|
+
normalize(join(target, "index.ts")).replace(/\\/g, "/"),
|
|
34
|
+
normalize(join(target, "index.tsx")).replace(/\\/g, "/"),
|
|
35
|
+
normalize(join(target, "index.mts")).replace(/\\/g, "/"),
|
|
36
|
+
normalize(join(target, "index.js")).replace(/\\/g, "/"),
|
|
37
|
+
normalize(join(target, "index.mjs")).replace(/\\/g, "/"),
|
|
38
|
+
];
|
|
39
|
+
for (const c of candidates) {
|
|
40
|
+
if (knownNodes.has(c)) return c;
|
|
41
|
+
}
|
|
42
|
+
// Try stripping .js extension (TS bundler convention)
|
|
43
|
+
if (spec.endsWith(".js")) {
|
|
44
|
+
const base = spec.slice(0, -3);
|
|
45
|
+
return resolveImport(fromModule, base, knownNodes);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectCycles(
|
|
51
|
+
nodes: string[],
|
|
52
|
+
edges: Array<{ from: string; to: string }>,
|
|
53
|
+
): string[][] {
|
|
54
|
+
const adj = new Map<string, string[]>();
|
|
55
|
+
for (const n of nodes) adj.set(n, []);
|
|
56
|
+
for (const e of edges) adj.get(e.from)?.push(e.to);
|
|
57
|
+
|
|
58
|
+
const WHITE = 0;
|
|
59
|
+
const GRAY = 1;
|
|
60
|
+
const BLACK = 2;
|
|
61
|
+
const color = new Map<string, number>();
|
|
62
|
+
for (const n of nodes) color.set(n, WHITE);
|
|
63
|
+
const cycles: string[][] = [];
|
|
64
|
+
const seen = new Set<string>();
|
|
65
|
+
|
|
66
|
+
function dfs(node: string, stack: string[]): void {
|
|
67
|
+
color.set(node, GRAY);
|
|
68
|
+
stack.push(node);
|
|
69
|
+
for (const next of adj.get(node) ?? []) {
|
|
70
|
+
if (color.get(next) === GRAY) {
|
|
71
|
+
const idx = stack.indexOf(next);
|
|
72
|
+
if (idx >= 0) {
|
|
73
|
+
const cycle = stack.slice(idx);
|
|
74
|
+
const key = [...cycle].sort().join("|");
|
|
75
|
+
if (!seen.has(key)) {
|
|
76
|
+
seen.add(key);
|
|
77
|
+
cycles.push([...cycle, next]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else if (color.get(next) === WHITE) {
|
|
81
|
+
dfs(next, stack);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
stack.pop();
|
|
85
|
+
color.set(node, BLACK);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const n of nodes) {
|
|
89
|
+
if (color.get(n) === WHITE) dfs(n, []);
|
|
90
|
+
}
|
|
91
|
+
return cycles;
|
|
92
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ModuleInfo, ProjectState, ProjectStats } from "../types.js";
|
|
4
|
+
import { buildGraph } from "./graph-builder.js";
|
|
5
|
+
import { mapModules } from "./module-mapper.js";
|
|
6
|
+
import { detectSmells } from "./smell-detector.js";
|
|
7
|
+
import { detectStack } from "./stack-detector.js";
|
|
8
|
+
|
|
9
|
+
const RUNTIME_VERSION = "4.1.0";
|
|
10
|
+
|
|
11
|
+
export interface AnalyzeOptions {
|
|
12
|
+
write?: boolean;
|
|
13
|
+
scanDirs?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function analyzeProject(
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
options: AnalyzeOptions = {},
|
|
19
|
+
): Promise<ProjectState> {
|
|
20
|
+
const stack = await detectStack(projectRoot);
|
|
21
|
+
const modules = await mapModules(projectRoot, { scanDirs: options.scanDirs });
|
|
22
|
+
const graph = buildGraph(modules);
|
|
23
|
+
const smells = await detectSmells(modules, graph, projectRoot);
|
|
24
|
+
const stats = await computeStats(projectRoot, modules);
|
|
25
|
+
|
|
26
|
+
const state: ProjectState = {
|
|
27
|
+
version: RUNTIME_VERSION,
|
|
28
|
+
generated_at: new Date().toISOString(),
|
|
29
|
+
project_root: projectRoot,
|
|
30
|
+
stack,
|
|
31
|
+
modules,
|
|
32
|
+
graph,
|
|
33
|
+
smells,
|
|
34
|
+
stats,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (options.write !== false) {
|
|
38
|
+
const ctxDir = join(projectRoot, ".ai", "context");
|
|
39
|
+
await mkdir(ctxDir, { recursive: true });
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(ctxDir, "project-state.json"),
|
|
42
|
+
JSON.stringify(state, null, 2),
|
|
43
|
+
"utf8",
|
|
44
|
+
);
|
|
45
|
+
await writeFile(join(ctxDir, "project-state.md"), renderMarkdown(state), "utf8");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function computeStats(
|
|
52
|
+
projectRoot: string,
|
|
53
|
+
modules: ModuleInfo[],
|
|
54
|
+
): Promise<ProjectStats> {
|
|
55
|
+
const filesByLang: Record<string, number> = {};
|
|
56
|
+
let totalLines = 0;
|
|
57
|
+
for (const m of modules) {
|
|
58
|
+
filesByLang[m.language] = (filesByLang[m.language] ?? 0) + 1;
|
|
59
|
+
totalLines += m.size_lines;
|
|
60
|
+
}
|
|
61
|
+
const largest = [...modules]
|
|
62
|
+
.sort((a, b) => b.size_lines - a.size_lines)
|
|
63
|
+
.slice(0, 10)
|
|
64
|
+
.map((m) => ({ path: m.path, lines: m.size_lines }));
|
|
65
|
+
const totalFiles = await walkCount(projectRoot);
|
|
66
|
+
return {
|
|
67
|
+
total_files: totalFiles,
|
|
68
|
+
total_lines: totalLines,
|
|
69
|
+
files_by_language: filesByLang,
|
|
70
|
+
largest_files: largest,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const COUNT_IGNORED = new Set([
|
|
75
|
+
"node_modules",
|
|
76
|
+
".git",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
".runtime",
|
|
80
|
+
"archive",
|
|
81
|
+
".turbo",
|
|
82
|
+
".vercel",
|
|
83
|
+
".next",
|
|
84
|
+
".nuxt",
|
|
85
|
+
"coverage",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
async function walkCount(dir: string): Promise<number> {
|
|
89
|
+
let count = 0;
|
|
90
|
+
let entries;
|
|
91
|
+
try {
|
|
92
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
93
|
+
} catch {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
if (COUNT_IGNORED.has(e.name)) continue;
|
|
98
|
+
if (e.name.startsWith(".") && e.name !== ".ai" && e.name !== ".claude") continue;
|
|
99
|
+
const p = join(dir, e.name);
|
|
100
|
+
if (e.isDirectory()) count += await walkCount(p);
|
|
101
|
+
else if (e.isFile()) count++;
|
|
102
|
+
}
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderMarkdown(state: ProjectState): string {
|
|
107
|
+
const lines: string[] = [];
|
|
108
|
+
lines.push("# Project State", "");
|
|
109
|
+
lines.push(`**Generated:** ${state.generated_at}`);
|
|
110
|
+
lines.push(`**Runtime:** v${state.version}`);
|
|
111
|
+
lines.push(`**Root:** \`${state.project_root}\``, "");
|
|
112
|
+
|
|
113
|
+
lines.push("## Stack", "");
|
|
114
|
+
lines.push(`- **Languages:** ${state.stack.languages.join(", ") || "(none detected)"}`);
|
|
115
|
+
lines.push(`- **Frameworks:** ${state.stack.frameworks.join(", ") || "(none)"}`);
|
|
116
|
+
if (state.stack.package_manager) {
|
|
117
|
+
lines.push(`- **Package manager:** ${state.stack.package_manager}`);
|
|
118
|
+
}
|
|
119
|
+
if (state.stack.databases && state.stack.databases.length) {
|
|
120
|
+
lines.push(`- **Databases:** ${state.stack.databases.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
if (state.stack.test_frameworks && state.stack.test_frameworks.length) {
|
|
123
|
+
lines.push(`- **Test frameworks:** ${state.stack.test_frameworks.join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
if (state.stack.linters && state.stack.linters.length) {
|
|
126
|
+
lines.push(`- **Linters:** ${state.stack.linters.join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("");
|
|
129
|
+
|
|
130
|
+
lines.push("## Stats", "");
|
|
131
|
+
lines.push(`- **Total files (repo):** ${state.stats.total_files}`);
|
|
132
|
+
lines.push(`- **Modules tracked (AST/regex):** ${state.modules.length}`);
|
|
133
|
+
lines.push(`- **Total lines in modules:** ${state.stats.total_lines}`);
|
|
134
|
+
lines.push(`- **Dependency edges:** ${state.graph.edges.length}`);
|
|
135
|
+
lines.push(`- **Circular dependencies:** ${state.graph.cycles.length}`);
|
|
136
|
+
lines.push(`- **Smells found:** ${state.smells.length}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
|
|
139
|
+
lines.push("### Files by language", "");
|
|
140
|
+
const sorted = Object.entries(state.stats.files_by_language).sort((a, b) => b[1] - a[1]);
|
|
141
|
+
if (sorted.length === 0) {
|
|
142
|
+
lines.push("_(no language buckets — nothing scanned)_");
|
|
143
|
+
} else {
|
|
144
|
+
for (const [lang, n] of sorted) {
|
|
145
|
+
lines.push(`- \`${lang}\`: ${n}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
|
|
150
|
+
if (state.stats.largest_files.length) {
|
|
151
|
+
lines.push("### Largest files", "");
|
|
152
|
+
for (const f of state.stats.largest_files) {
|
|
153
|
+
lines.push(`- \`${f.path}\` — ${f.lines} lines`);
|
|
154
|
+
}
|
|
155
|
+
lines.push("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (state.smells.length) {
|
|
159
|
+
lines.push("## Smells (top 20)", "");
|
|
160
|
+
for (const s of state.smells.slice(0, 20)) {
|
|
161
|
+
const loc = `${s.location.file}${s.location.line ? ":" + s.location.line : ""}`;
|
|
162
|
+
lines.push(`- **${s.kind}** [${s.severity}] \`${loc}\` — ${s.message}`);
|
|
163
|
+
}
|
|
164
|
+
lines.push("");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (state.graph.cycles.length) {
|
|
168
|
+
lines.push("## Cyclic dependencies", "");
|
|
169
|
+
for (const c of state.graph.cycles) {
|
|
170
|
+
lines.push(`- ${c.join(" → ")}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (state.modules.length) {
|
|
176
|
+
lines.push("## Modules", "");
|
|
177
|
+
for (const m of state.modules.slice(0, 50)) {
|
|
178
|
+
lines.push(
|
|
179
|
+
`- \`${m.path}\` (${m.language}, ${m.size_lines} lines, ${m.imports.length} imports, ${m.exports.length} exports)`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (state.modules.length > 50) {
|
|
183
|
+
lines.push(`- … and ${state.modules.length - 50} more`);
|
|
184
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { detectStack, mapModules, buildGraph, detectSmells };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { extname, join, relative } from "node:path";
|
|
4
|
+
import { Project } from "ts-morph";
|
|
5
|
+
import type { ModuleInfo } from "../types.js";
|
|
6
|
+
|
|
7
|
+
const IGNORED_DIRS = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
".next",
|
|
13
|
+
".nuxt",
|
|
14
|
+
"coverage",
|
|
15
|
+
".cache",
|
|
16
|
+
".runtime",
|
|
17
|
+
"archive",
|
|
18
|
+
".turbo",
|
|
19
|
+
".vercel",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
23
|
+
const TEXT_LANG_EXTENSIONS = new Set([
|
|
24
|
+
".py",
|
|
25
|
+
".rb",
|
|
26
|
+
".go",
|
|
27
|
+
".rs",
|
|
28
|
+
".java",
|
|
29
|
+
".kt",
|
|
30
|
+
".php",
|
|
31
|
+
".cs",
|
|
32
|
+
".ex",
|
|
33
|
+
".exs",
|
|
34
|
+
".swift",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export interface MapOptions {
|
|
38
|
+
scanDirs?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function mapModules(
|
|
42
|
+
projectRoot: string,
|
|
43
|
+
options: MapOptions = {},
|
|
44
|
+
): Promise<ModuleInfo[]> {
|
|
45
|
+
const scanDirs = options.scanDirs ?? [".ai/server/src", "src", "lib", "app", "packages"];
|
|
46
|
+
const allFiles: string[] = [];
|
|
47
|
+
|
|
48
|
+
for (const d of scanDirs) {
|
|
49
|
+
const abs = join(projectRoot, d);
|
|
50
|
+
if (!existsSync(abs)) continue;
|
|
51
|
+
await collectFiles(abs, allFiles);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const modules: ModuleInfo[] = [];
|
|
55
|
+
|
|
56
|
+
const tsFiles = allFiles.filter((f) => TS_EXTENSIONS.has(extname(f)));
|
|
57
|
+
if (tsFiles.length > 0) {
|
|
58
|
+
const project = new Project({
|
|
59
|
+
useInMemoryFileSystem: false,
|
|
60
|
+
skipAddingFilesFromTsConfig: true,
|
|
61
|
+
compilerOptions: { allowJs: true, noEmit: true },
|
|
62
|
+
});
|
|
63
|
+
for (const f of tsFiles) {
|
|
64
|
+
try {
|
|
65
|
+
project.addSourceFileAtPath(f);
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore unreadable/odd file
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const sf of project.getSourceFiles()) {
|
|
71
|
+
const path = sf.getFilePath();
|
|
72
|
+
const relPath = relative(projectRoot, path).replace(/\\/g, "/");
|
|
73
|
+
const text = sf.getFullText();
|
|
74
|
+
const lines = text.split("\n").length;
|
|
75
|
+
const imports: string[] = [];
|
|
76
|
+
for (const imp of sf.getImportDeclarations()) {
|
|
77
|
+
const v = imp.getModuleSpecifierValue();
|
|
78
|
+
if (v) imports.push(v);
|
|
79
|
+
}
|
|
80
|
+
for (const exp of sf.getExportDeclarations()) {
|
|
81
|
+
const v = exp.getModuleSpecifierValue();
|
|
82
|
+
if (v) imports.push(v);
|
|
83
|
+
}
|
|
84
|
+
const exportsList: string[] = [];
|
|
85
|
+
for (const [name] of sf.getExportedDeclarations()) {
|
|
86
|
+
exportsList.push(name);
|
|
87
|
+
}
|
|
88
|
+
modules.push({
|
|
89
|
+
id: relPath,
|
|
90
|
+
name: relPath,
|
|
91
|
+
path: relPath,
|
|
92
|
+
kind: "file",
|
|
93
|
+
language: extname(path) === ".tsx" || extname(path) === ".jsx" ? "tsx" : "typescript",
|
|
94
|
+
size_lines: lines,
|
|
95
|
+
imports,
|
|
96
|
+
exports: exportsList,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const f of allFiles) {
|
|
102
|
+
const ext = extname(f);
|
|
103
|
+
if (!TEXT_LANG_EXTENSIONS.has(ext)) continue;
|
|
104
|
+
const text = await readFile(f, "utf8").catch(() => "");
|
|
105
|
+
if (!text) continue;
|
|
106
|
+
const lines = text.split("\n").length;
|
|
107
|
+
const relPath = relative(projectRoot, f).replace(/\\/g, "/");
|
|
108
|
+
const lang = guessLanguage(ext);
|
|
109
|
+
modules.push({
|
|
110
|
+
id: relPath,
|
|
111
|
+
name: relPath,
|
|
112
|
+
path: relPath,
|
|
113
|
+
kind: "file",
|
|
114
|
+
language: lang,
|
|
115
|
+
size_lines: lines,
|
|
116
|
+
imports: extractImportsByLanguage(text, lang),
|
|
117
|
+
exports: [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return modules;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function collectFiles(dir: string, out: string[]): Promise<void> {
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
if (IGNORED_DIRS.has(e.name)) continue;
|
|
133
|
+
if (e.name.startsWith(".") && e.name !== ".ai" && e.name !== ".claude") continue;
|
|
134
|
+
const p = join(dir, e.name);
|
|
135
|
+
if (e.isDirectory()) {
|
|
136
|
+
await collectFiles(p, out);
|
|
137
|
+
} else if (e.isFile()) {
|
|
138
|
+
out.push(p);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function guessLanguage(ext: string): string {
|
|
144
|
+
const map: Record<string, string> = {
|
|
145
|
+
".py": "python",
|
|
146
|
+
".rb": "ruby",
|
|
147
|
+
".go": "go",
|
|
148
|
+
".rs": "rust",
|
|
149
|
+
".java": "java",
|
|
150
|
+
".kt": "kotlin",
|
|
151
|
+
".php": "php",
|
|
152
|
+
".cs": "csharp",
|
|
153
|
+
".ex": "elixir",
|
|
154
|
+
".exs": "elixir",
|
|
155
|
+
".swift": "swift",
|
|
156
|
+
};
|
|
157
|
+
return map[ext] ?? "unknown";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractImportsByLanguage(text: string, lang: string): string[] {
|
|
161
|
+
const imports: string[] = [];
|
|
162
|
+
let re: RegExp | null = null;
|
|
163
|
+
if (lang === "python") re = /^(?:from|import)\s+([\w.]+)/gm;
|
|
164
|
+
else if (lang === "go") re = /import\s+"([^"]+)"/g;
|
|
165
|
+
else if (lang === "rust") re = /^use\s+([\w:]+)/gm;
|
|
166
|
+
else if (lang === "java" || lang === "kotlin") re = /^import\s+([\w.]+)/gm;
|
|
167
|
+
if (!re) return imports;
|
|
168
|
+
let m: RegExpExecArray | null;
|
|
169
|
+
while ((m = re.exec(text))) imports.push(m[1]);
|
|
170
|
+
return imports;
|
|
171
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { DependencyGraph, ModuleInfo, Smell } from "../types.js";
|
|
4
|
+
|
|
5
|
+
const LARGE_FILE_THRESHOLD = 300;
|
|
6
|
+
|
|
7
|
+
export async function detectSmells(
|
|
8
|
+
modules: ModuleInfo[],
|
|
9
|
+
graph: DependencyGraph,
|
|
10
|
+
projectRoot: string,
|
|
11
|
+
): Promise<Smell[]> {
|
|
12
|
+
const smells: Smell[] = [];
|
|
13
|
+
|
|
14
|
+
for (const cycle of graph.cycles) {
|
|
15
|
+
smells.push({
|
|
16
|
+
kind: "circular_dependency",
|
|
17
|
+
severity: "high",
|
|
18
|
+
location: { file: cycle[0] },
|
|
19
|
+
message: `Circular dependency: ${cycle.join(" → ")}`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const m of modules) {
|
|
24
|
+
if (m.size_lines > LARGE_FILE_THRESHOLD) {
|
|
25
|
+
smells.push({
|
|
26
|
+
kind: "large_file",
|
|
27
|
+
severity: m.size_lines > 1000 ? "high" : "medium",
|
|
28
|
+
location: { file: m.path },
|
|
29
|
+
message: `File has ${m.size_lines} lines (soft limit: ${LARGE_FILE_THRESHOLD})`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const m of modules) {
|
|
35
|
+
if (m.language === "unknown" || m.language === "markdown") continue;
|
|
36
|
+
const abs = join(projectRoot, m.path);
|
|
37
|
+
const text = await readFile(abs, "utf8").catch(() => "");
|
|
38
|
+
if (!text) continue;
|
|
39
|
+
const lines = text.split("\n");
|
|
40
|
+
lines.forEach((line, idx) => {
|
|
41
|
+
const isFixme = /\bFIXME\b/i.test(line);
|
|
42
|
+
const isTodo = /\bTODO\b/i.test(line);
|
|
43
|
+
if (!isFixme && !isTodo) return;
|
|
44
|
+
smells.push({
|
|
45
|
+
kind: isFixme ? "fixme_comment" : "todo_comment",
|
|
46
|
+
severity: isFixme ? "medium" : "low",
|
|
47
|
+
location: { file: m.path, line: idx + 1 },
|
|
48
|
+
message: line.trim().slice(0, 140),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return smells;
|
|
54
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { StackInfo } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export async function detectStack(projectRoot: string): Promise<StackInfo> {
|
|
7
|
+
const fallback: StackInfo = { languages: [], frameworks: [] };
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const isWin = process.platform === "win32";
|
|
11
|
+
const aiDir = join(projectRoot, ".ai");
|
|
12
|
+
const scriptPs = join(aiDir, "scripts", "detect-stack.ps1");
|
|
13
|
+
const scriptSh = join(aiDir, "scripts", "detect-stack.sh");
|
|
14
|
+
const useScript = isWin ? scriptPs : existsSync(scriptSh) ? scriptSh : scriptPs;
|
|
15
|
+
|
|
16
|
+
if (!existsSync(useScript)) {
|
|
17
|
+
resolve(fallback);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cmd = isWin ? "powershell" : "bash";
|
|
22
|
+
const args = isWin
|
|
23
|
+
? [
|
|
24
|
+
"-NoProfile",
|
|
25
|
+
"-ExecutionPolicy",
|
|
26
|
+
"Bypass",
|
|
27
|
+
"-File",
|
|
28
|
+
useScript,
|
|
29
|
+
"-TargetPath",
|
|
30
|
+
projectRoot,
|
|
31
|
+
]
|
|
32
|
+
: [useScript, "--target", projectRoot];
|
|
33
|
+
|
|
34
|
+
const child = spawn(cmd, args, { cwd: projectRoot });
|
|
35
|
+
let out = "";
|
|
36
|
+
child.stdout.on("data", (d) => (out += d));
|
|
37
|
+
child.stderr.on("data", () => {});
|
|
38
|
+
|
|
39
|
+
child.on("close", () => {
|
|
40
|
+
try {
|
|
41
|
+
const jsonStart = out.indexOf("{");
|
|
42
|
+
const jsonEnd = out.lastIndexOf("}");
|
|
43
|
+
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
44
|
+
const parsed = JSON.parse(out.slice(jsonStart, jsonEnd + 1)) as Record<string, unknown>;
|
|
45
|
+
resolve(normalize(parsed));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// fall through
|
|
50
|
+
}
|
|
51
|
+
resolve(fallback);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on("error", () => resolve(fallback));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalize(raw: Record<string, unknown>): StackInfo {
|
|
59
|
+
const arr = (v: unknown): string[] | undefined =>
|
|
60
|
+
Array.isArray(v) ? (v as unknown[]).map(String) : undefined;
|
|
61
|
+
return {
|
|
62
|
+
languages: arr(raw.languages) ?? [],
|
|
63
|
+
frameworks: arr(raw.frameworks) ?? [],
|
|
64
|
+
package_manager: typeof raw.package_manager === "string" ? raw.package_manager : undefined,
|
|
65
|
+
databases: arr(raw.databases),
|
|
66
|
+
test_frameworks: arr(raw.test_frameworks),
|
|
67
|
+
linters: arr(raw.linters),
|
|
68
|
+
ci_cd: arr(raw.ci_cd),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * as analyzer from "./analyzer/index.js";
|
|
2
|
+
export * as packager from "./packager/index.js";
|
|
3
|
+
export * as snapshot from "./snapshot/index.js";
|
|
4
|
+
export type {
|
|
5
|
+
ProjectState,
|
|
6
|
+
StackInfo,
|
|
7
|
+
ModuleInfo,
|
|
8
|
+
DependencyGraph,
|
|
9
|
+
Smell,
|
|
10
|
+
ProjectStats,
|
|
11
|
+
ContextPackage,
|
|
12
|
+
SnapshotManifest,
|
|
13
|
+
SnapshotDiff,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
export const RUNTIME_VERSION = "4.1.0";
|