archbyte 0.1.0
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 +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
/**
|
|
5
|
+
* Start the ArchByte UI server
|
|
6
|
+
*/
|
|
7
|
+
export async function handleServe(options) {
|
|
8
|
+
const rootDir = process.cwd();
|
|
9
|
+
const port = options.port || 3847;
|
|
10
|
+
const diagramPath = options.diagram
|
|
11
|
+
? path.resolve(rootDir, options.diagram)
|
|
12
|
+
: path.join(rootDir, ".archbyte", "architecture.json");
|
|
13
|
+
// Auto-detect project name from package.json or folder
|
|
14
|
+
let projectName = path.basename(rootDir);
|
|
15
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
16
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
19
|
+
projectName = pkg.name || projectName;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
console.log(chalk.cyan(`🏗️ ArchByte - ${projectName}`));
|
|
26
|
+
console.log(chalk.gray(`Diagram: ${path.relative(rootDir, diagramPath)}`));
|
|
27
|
+
console.log(chalk.gray(`Port: ${port}`));
|
|
28
|
+
console.log();
|
|
29
|
+
// Check if diagram exists
|
|
30
|
+
if (!fs.existsSync(diagramPath)) {
|
|
31
|
+
console.log(chalk.yellow("No architecture diagram found."));
|
|
32
|
+
console.log(chalk.gray("Run 'archbyte analyze' or 'archbyte run' to generate one."));
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
const { startServer } = await import("../server/src/index.js");
|
|
36
|
+
const maxRetries = 10;
|
|
37
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
38
|
+
const tryPort = port + attempt;
|
|
39
|
+
try {
|
|
40
|
+
if (attempt > 0) {
|
|
41
|
+
console.log(chalk.gray(`Port ${tryPort - 1} in use, trying ${tryPort}...`));
|
|
42
|
+
}
|
|
43
|
+
console.log(chalk.green(`UI: http://localhost:${tryPort}`));
|
|
44
|
+
console.log();
|
|
45
|
+
await startServer({
|
|
46
|
+
name: projectName,
|
|
47
|
+
diagramPath,
|
|
48
|
+
workspaceRoot: rootDir,
|
|
49
|
+
port: tryPort,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof Error && error.code === "EADDRINUSE") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (error instanceof Error) {
|
|
58
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
59
|
+
}
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.error(chalk.red(`All ports ${port}-${port + maxRetries - 1} are in use.`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleSetup(): Promise<void>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { resolveModel, MODEL_MAP } from "../agents/runtime/types.js";
|
|
6
|
+
import { createProvider } from "../agents/providers/router.js";
|
|
7
|
+
import { select, spinner, confirm } from "./ui.js";
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".archbyte");
|
|
11
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
12
|
+
const PROVIDERS = [
|
|
13
|
+
{ name: "anthropic", label: "Anthropic", hint: "Claude Opus / Sonnet" },
|
|
14
|
+
{ name: "openai", label: "OpenAI", hint: "GPT-4o / o1" },
|
|
15
|
+
{ name: "google", label: "Google", hint: "Gemini Flash / Pro" },
|
|
16
|
+
{ name: "ollama", label: "Ollama", hint: "Local models (no key needed)" },
|
|
17
|
+
];
|
|
18
|
+
function loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// ignore
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
function saveConfig(config) {
|
|
30
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
function maskKey(key) {
|
|
36
|
+
if (key.length <= 8)
|
|
37
|
+
return "****";
|
|
38
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
39
|
+
}
|
|
40
|
+
function askHidden(prompt) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
process.stdout.write(prompt);
|
|
43
|
+
const stdin = process.stdin;
|
|
44
|
+
const wasRaw = stdin.isRaw;
|
|
45
|
+
stdin.setRawMode(true);
|
|
46
|
+
stdin.resume();
|
|
47
|
+
stdin.setEncoding("utf8");
|
|
48
|
+
let input = "";
|
|
49
|
+
const onData = (data) => {
|
|
50
|
+
for (const ch of data) {
|
|
51
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
52
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
53
|
+
stdin.pause();
|
|
54
|
+
stdin.removeListener("data", onData);
|
|
55
|
+
process.stdout.write("\n");
|
|
56
|
+
resolve(input);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
else if (ch === "\u0003") {
|
|
60
|
+
process.stdout.write("\n");
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
else if (ch === "\u007F" || ch === "\b") {
|
|
64
|
+
if (input.length > 0) {
|
|
65
|
+
input = input.slice(0, -1);
|
|
66
|
+
process.stdout.write("\b \b");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
input += ch;
|
|
71
|
+
process.stdout.write("*");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
stdin.on("data", onData);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function validateProviderSilent(providerName, apiKey, ollamaBaseUrl) {
|
|
79
|
+
try {
|
|
80
|
+
if (providerName === "ollama") {
|
|
81
|
+
const url = ollamaBaseUrl ?? "http://localhost:11434";
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
return false;
|
|
89
|
+
// Check installed models
|
|
90
|
+
try {
|
|
91
|
+
const tagsController = new AbortController();
|
|
92
|
+
const tagsTimeout = setTimeout(() => tagsController.abort(), 5000);
|
|
93
|
+
const tagsRes = await fetch(`${url}/api/tags`, { signal: tagsController.signal });
|
|
94
|
+
clearTimeout(tagsTimeout);
|
|
95
|
+
if (tagsRes.ok) {
|
|
96
|
+
const data = await tagsRes.json();
|
|
97
|
+
const installedModels = (data.models ?? []).map((m) => m.name.split(":")[0]);
|
|
98
|
+
const requiredModels = Object.values(MODEL_MAP.ollama);
|
|
99
|
+
const uniqueRequired = [...new Set(requiredModels)];
|
|
100
|
+
const compatible = uniqueRequired.filter((m) => installedModels.some((installed) => installed === m));
|
|
101
|
+
if (compatible.length > 0) {
|
|
102
|
+
console.log(chalk.gray(` Compatible models: ${compatible.join(", ")}`));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(chalk.yellow(` Warning: no compatible models found.`));
|
|
106
|
+
console.log(chalk.gray(` Run: ollama pull ${uniqueRequired[0]}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Model check is best-effort
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const provider = createProvider({ provider: providerName, apiKey });
|
|
121
|
+
const model = resolveModel(providerName, "fast");
|
|
122
|
+
await provider.chat({
|
|
123
|
+
model,
|
|
124
|
+
system: "Reply with 'ok'.",
|
|
125
|
+
messages: [{ role: "user", content: "ping" }],
|
|
126
|
+
maxTokens: 8,
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function handleSetup() {
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(chalk.bold.cyan("ArchByte Setup"));
|
|
137
|
+
console.log(chalk.gray("Configure your model provider and API key.\n"));
|
|
138
|
+
const config = loadConfig();
|
|
139
|
+
// Show current config if exists
|
|
140
|
+
if (config.provider) {
|
|
141
|
+
console.log(chalk.gray(` Current provider: ${config.provider}`));
|
|
142
|
+
if (config.apiKey) {
|
|
143
|
+
console.log(chalk.gray(` Current API key: ${maskKey(config.apiKey)}`));
|
|
144
|
+
}
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
// Step 1: Choose provider with arrow-key selection
|
|
148
|
+
const idx = await select("Choose your model provider:", PROVIDERS.map((p) => {
|
|
149
|
+
const current = config.provider === p.name ? chalk.green(" (current)") : "";
|
|
150
|
+
return `${p.label} ${chalk.gray("— " + p.hint)}${current}`;
|
|
151
|
+
}));
|
|
152
|
+
const provider = PROVIDERS[idx].name;
|
|
153
|
+
config.provider = provider;
|
|
154
|
+
const selected = PROVIDERS[idx];
|
|
155
|
+
console.log(chalk.green(`\n ✓ Provider: ${selected.label}`));
|
|
156
|
+
// Step 2: API key (skip for Ollama)
|
|
157
|
+
if (provider === "ollama") {
|
|
158
|
+
config.ollamaBaseUrl = config.ollamaBaseUrl ?? "http://localhost:11434";
|
|
159
|
+
console.log(chalk.gray(` Ollama URL: ${config.ollamaBaseUrl}`));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log();
|
|
163
|
+
const apiKey = await askHidden(chalk.bold(" API key: "));
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
console.log(chalk.red(" No API key entered. Setup cancelled."));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
config.apiKey = apiKey;
|
|
169
|
+
console.log(chalk.green(` ✓ API key: ${maskKey(apiKey)}`));
|
|
170
|
+
}
|
|
171
|
+
// Validate provider with spinner
|
|
172
|
+
const s = spinner("Validating credentials");
|
|
173
|
+
let isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
|
|
174
|
+
s.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
|
|
175
|
+
// Retry loop on failure
|
|
176
|
+
if (!isValid) {
|
|
177
|
+
let retries = 0;
|
|
178
|
+
while (!isValid && retries < 2) {
|
|
179
|
+
if (!await confirm(" Retry?"))
|
|
180
|
+
break;
|
|
181
|
+
retries++;
|
|
182
|
+
if (provider !== "ollama") {
|
|
183
|
+
const newKey = await askHidden(chalk.bold(" API key: "));
|
|
184
|
+
if (newKey) {
|
|
185
|
+
config.apiKey = newKey;
|
|
186
|
+
console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const s2 = spinner("Validating credentials");
|
|
190
|
+
isValid = await validateProviderSilent(provider, config.apiKey ?? "", config.ollamaBaseUrl);
|
|
191
|
+
s2.stop(isValid ? "valid" : "invalid", isValid ? "green" : "red");
|
|
192
|
+
}
|
|
193
|
+
if (!isValid) {
|
|
194
|
+
console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Save config
|
|
198
|
+
saveConfig(config);
|
|
199
|
+
console.log(chalk.gray(`\n Config saved to ${CONFIG_PATH}`));
|
|
200
|
+
// Generate archbyte.yaml in .archbyte/ if it doesn't exist
|
|
201
|
+
const projectDir = process.cwd();
|
|
202
|
+
const archbyteDir = path.join(projectDir, ".archbyte");
|
|
203
|
+
const yamlPath = path.join(archbyteDir, "archbyte.yaml");
|
|
204
|
+
if (!fs.existsSync(yamlPath)) {
|
|
205
|
+
if (!fs.existsSync(archbyteDir)) {
|
|
206
|
+
fs.mkdirSync(archbyteDir, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
// Detect project name
|
|
209
|
+
let projectName = path.basename(projectDir);
|
|
210
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
211
|
+
if (fs.existsSync(pkgPath)) {
|
|
212
|
+
try {
|
|
213
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
214
|
+
if (pkg.name)
|
|
215
|
+
projectName = pkg.name;
|
|
216
|
+
}
|
|
217
|
+
catch { /* ignore */ }
|
|
218
|
+
}
|
|
219
|
+
const templatePath = path.resolve(__dirname, "../../templates/archbyte.yaml");
|
|
220
|
+
let template = fs.readFileSync(templatePath, "utf-8");
|
|
221
|
+
template = template.replace('name: "My Project"', `name: "${projectName}"`);
|
|
222
|
+
fs.writeFileSync(yamlPath, template, "utf-8");
|
|
223
|
+
console.log(chalk.green(` Created .archbyte/archbyte.yaml`));
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
console.log(chalk.gray(` .archbyte/archbyte.yaml already exists`));
|
|
227
|
+
}
|
|
228
|
+
console.log();
|
|
229
|
+
console.log(chalk.green(" Setup complete!"));
|
|
230
|
+
console.log();
|
|
231
|
+
console.log(chalk.bold(" Next: ") + chalk.cyan("archbyte run") + chalk.bold(" to analyze your codebase."));
|
|
232
|
+
console.log();
|
|
233
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Architecture } from "../server/src/generator/index.js";
|
|
2
|
+
export interface DiagramOptions {
|
|
3
|
+
diagram?: string;
|
|
4
|
+
}
|
|
5
|
+
export type RuleLevel = "error" | "warn" | "off";
|
|
6
|
+
export interface RuleConfig {
|
|
7
|
+
"no-layer-bypass"?: RuleLevel | {
|
|
8
|
+
level: RuleLevel;
|
|
9
|
+
};
|
|
10
|
+
"max-connections"?: RuleLevel | {
|
|
11
|
+
level: RuleLevel;
|
|
12
|
+
threshold?: number;
|
|
13
|
+
};
|
|
14
|
+
"no-orphans"?: RuleLevel | {
|
|
15
|
+
level: RuleLevel;
|
|
16
|
+
};
|
|
17
|
+
"no-circular-deps"?: RuleLevel | {
|
|
18
|
+
level: RuleLevel;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface CustomRuleMatcher {
|
|
22
|
+
type?: string;
|
|
23
|
+
layer?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
not?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface CustomRule {
|
|
28
|
+
name: string;
|
|
29
|
+
from: CustomRuleMatcher;
|
|
30
|
+
to: CustomRuleMatcher;
|
|
31
|
+
level: RuleLevel;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the path to the architecture JSON file from CLI options or defaults.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveArchitecturePath(options: DiagramOptions): string;
|
|
37
|
+
/**
|
|
38
|
+
* Load and parse the architecture JSON file.
|
|
39
|
+
* Exits with an error message if the file is missing or invalid.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadArchitectureFile(filePath: string): Architecture;
|
|
42
|
+
/**
|
|
43
|
+
* Load rules configuration from archbyte.yaml.
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadRulesConfig(configPath?: string): RuleConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Minimal YAML parser for the rules section only.
|
|
48
|
+
* Handles:
|
|
49
|
+
* rules:
|
|
50
|
+
* rule-name: error
|
|
51
|
+
* rule-name:
|
|
52
|
+
* level: warn
|
|
53
|
+
* threshold: 6
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseRulesFromYaml(content: string): RuleConfig;
|
|
56
|
+
/**
|
|
57
|
+
* Parse custom rules from the YAML config file.
|
|
58
|
+
* Handles:
|
|
59
|
+
* rules:
|
|
60
|
+
* custom:
|
|
61
|
+
* - name: "rule-name"
|
|
62
|
+
* from: { type: "component", layer: "presentation" }
|
|
63
|
+
* to: { type: "database" }
|
|
64
|
+
* level: error
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseCustomRulesFromYaml(content: string): CustomRule[];
|
|
67
|
+
export declare function getRuleLevel(config: RuleConfig, rule: keyof RuleConfig, defaultLevel: RuleLevel): RuleLevel;
|
|
68
|
+
export declare function getThreshold(config: RuleConfig, rule: "max-connections", defaultVal: number): number;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the path to the architecture JSON file from CLI options or defaults.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveArchitecturePath(options) {
|
|
8
|
+
const rootDir = process.cwd();
|
|
9
|
+
return options.diagram
|
|
10
|
+
? path.resolve(rootDir, options.diagram)
|
|
11
|
+
: path.join(rootDir, ".archbyte", "architecture.json");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Load and parse the architecture JSON file.
|
|
15
|
+
* Exits with an error message if the file is missing or invalid.
|
|
16
|
+
*/
|
|
17
|
+
export function loadArchitectureFile(filePath) {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
console.error(chalk.red(`Architecture file not found: ${filePath}`));
|
|
20
|
+
console.error(chalk.gray("Run archbyte generate first, or provide --diagram <path>"));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error(chalk.red(`Failed to parse architecture file: ${filePath}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load rules configuration from archbyte.yaml.
|
|
34
|
+
*/
|
|
35
|
+
export function loadRulesConfig(configPath) {
|
|
36
|
+
const rootDir = process.cwd();
|
|
37
|
+
const yamlPath = configPath
|
|
38
|
+
? path.resolve(rootDir, configPath)
|
|
39
|
+
: path.join(rootDir, ".archbyte", "archbyte.yaml");
|
|
40
|
+
if (!fs.existsSync(yamlPath)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(yamlPath, "utf-8");
|
|
45
|
+
// Simple YAML parser for the rules section — avoids adding a dependency
|
|
46
|
+
return parseRulesFromYaml(content);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Minimal YAML parser for the rules section only.
|
|
54
|
+
* Handles:
|
|
55
|
+
* rules:
|
|
56
|
+
* rule-name: error
|
|
57
|
+
* rule-name:
|
|
58
|
+
* level: warn
|
|
59
|
+
* threshold: 6
|
|
60
|
+
*/
|
|
61
|
+
export function parseRulesFromYaml(content) {
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
const rules = {};
|
|
64
|
+
let inRules = false;
|
|
65
|
+
let currentRule = null;
|
|
66
|
+
let currentObj = {};
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const trimmed = line.trimEnd();
|
|
69
|
+
// Detect start of rules section
|
|
70
|
+
if (/^rules:\s*$/.test(trimmed)) {
|
|
71
|
+
inRules = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Detect start of another top-level section (end of rules)
|
|
75
|
+
if (inRules && /^\S/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
76
|
+
// Flush current rule
|
|
77
|
+
if (currentRule && Object.keys(currentObj).length > 0) {
|
|
78
|
+
rules[currentRule] = currentObj;
|
|
79
|
+
}
|
|
80
|
+
inRules = false;
|
|
81
|
+
currentRule = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!inRules)
|
|
85
|
+
continue;
|
|
86
|
+
// Skip empty lines and comments
|
|
87
|
+
if (trimmed === "" || trimmed.trim().startsWith("#"))
|
|
88
|
+
continue;
|
|
89
|
+
// Indented rule entry: " rule-name: value" or " rule-name:"
|
|
90
|
+
const ruleMatch = trimmed.match(/^ {2}(\S[\w-]+):\s*(.*)$/);
|
|
91
|
+
if (ruleMatch) {
|
|
92
|
+
// Flush previous rule object
|
|
93
|
+
if (currentRule && Object.keys(currentObj).length > 0) {
|
|
94
|
+
rules[currentRule] = currentObj;
|
|
95
|
+
}
|
|
96
|
+
const [, name, value] = ruleMatch;
|
|
97
|
+
if (value && value.trim() !== "") {
|
|
98
|
+
// Simple value: "rule-name: error"
|
|
99
|
+
rules[name] = value.trim();
|
|
100
|
+
currentRule = null;
|
|
101
|
+
currentObj = {};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Block value — will have sub-keys
|
|
105
|
+
currentRule = name;
|
|
106
|
+
currentObj = {};
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Sub-key: " level: warn" or " threshold: 6"
|
|
111
|
+
const subMatch = trimmed.match(/^ {4,}(\w+):\s*(.+)$/);
|
|
112
|
+
if (subMatch && currentRule) {
|
|
113
|
+
const [, key, val] = subMatch;
|
|
114
|
+
const numVal = Number(val);
|
|
115
|
+
currentObj[key] = isNaN(numVal) ? val.trim() : numVal;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Flush last rule
|
|
119
|
+
if (currentRule && Object.keys(currentObj).length > 0) {
|
|
120
|
+
rules[currentRule] = currentObj;
|
|
121
|
+
}
|
|
122
|
+
return rules;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse custom rules from the YAML config file.
|
|
126
|
+
* Handles:
|
|
127
|
+
* rules:
|
|
128
|
+
* custom:
|
|
129
|
+
* - name: "rule-name"
|
|
130
|
+
* from: { type: "component", layer: "presentation" }
|
|
131
|
+
* to: { type: "database" }
|
|
132
|
+
* level: error
|
|
133
|
+
*/
|
|
134
|
+
export function parseCustomRulesFromYaml(content) {
|
|
135
|
+
const rules = [];
|
|
136
|
+
const lines = content.split("\n");
|
|
137
|
+
let inRules = false;
|
|
138
|
+
let inCustom = false;
|
|
139
|
+
let inItem = false;
|
|
140
|
+
let currentItem = {};
|
|
141
|
+
let currentFrom = {};
|
|
142
|
+
let currentTo = {};
|
|
143
|
+
let inFrom = false;
|
|
144
|
+
let inTo = false;
|
|
145
|
+
const flushItem = () => {
|
|
146
|
+
if (currentItem.name && Object.keys(currentFrom).length > 0 && Object.keys(currentTo).length > 0) {
|
|
147
|
+
const fromMatcher = {};
|
|
148
|
+
if (currentFrom.type)
|
|
149
|
+
fromMatcher.type = currentFrom.type;
|
|
150
|
+
if (currentFrom.layer)
|
|
151
|
+
fromMatcher.layer = currentFrom.layer;
|
|
152
|
+
if (currentFrom.id)
|
|
153
|
+
fromMatcher.id = currentFrom.id;
|
|
154
|
+
if (currentFrom.not === "true")
|
|
155
|
+
fromMatcher.not = true;
|
|
156
|
+
const toMatcher = {};
|
|
157
|
+
if (currentTo.type)
|
|
158
|
+
toMatcher.type = currentTo.type;
|
|
159
|
+
if (currentTo.layer)
|
|
160
|
+
toMatcher.layer = currentTo.layer;
|
|
161
|
+
if (currentTo.id)
|
|
162
|
+
toMatcher.id = currentTo.id;
|
|
163
|
+
if (currentTo.not === "true")
|
|
164
|
+
toMatcher.not = true;
|
|
165
|
+
rules.push({
|
|
166
|
+
name: currentItem.name,
|
|
167
|
+
from: fromMatcher,
|
|
168
|
+
to: toMatcher,
|
|
169
|
+
level: currentItem.level || "error",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
currentItem = {};
|
|
173
|
+
currentFrom = {};
|
|
174
|
+
currentTo = {};
|
|
175
|
+
inFrom = false;
|
|
176
|
+
inTo = false;
|
|
177
|
+
inItem = false;
|
|
178
|
+
};
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const trimmed = line.trimEnd();
|
|
181
|
+
if (/^rules:\s*$/.test(trimmed)) {
|
|
182
|
+
inRules = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (inRules && /^\S/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
186
|
+
if (inItem)
|
|
187
|
+
flushItem();
|
|
188
|
+
inRules = false;
|
|
189
|
+
inCustom = false;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!inRules)
|
|
193
|
+
continue;
|
|
194
|
+
if (trimmed === "" || trimmed.trim().startsWith("#"))
|
|
195
|
+
continue;
|
|
196
|
+
// Detect " custom:" section
|
|
197
|
+
if (/^ {2}custom:\s*$/.test(trimmed)) {
|
|
198
|
+
inCustom = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// If we hit another 2-space rule name, leave custom
|
|
202
|
+
if (inCustom && /^ {2}[a-zA-Z]/.test(trimmed) && !/^ {2}custom:/.test(trimmed) && !trimmed.trim().startsWith("-")) {
|
|
203
|
+
if (inItem)
|
|
204
|
+
flushItem();
|
|
205
|
+
inCustom = false;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!inCustom)
|
|
209
|
+
continue;
|
|
210
|
+
// New list item: " - name: ..."
|
|
211
|
+
const listItemMatch = trimmed.match(/^ {4}- name:\s*"?([^"]+)"?\s*$/);
|
|
212
|
+
if (listItemMatch) {
|
|
213
|
+
if (inItem)
|
|
214
|
+
flushItem();
|
|
215
|
+
inItem = true;
|
|
216
|
+
currentItem.name = listItemMatch[1].trim();
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!inItem)
|
|
220
|
+
continue;
|
|
221
|
+
// Inline from/to: " from: { type: "component", layer: "presentation" }"
|
|
222
|
+
const inlineMatch = trimmed.match(/^ {6}(from|to):\s*\{(.+)\}\s*$/);
|
|
223
|
+
if (inlineMatch) {
|
|
224
|
+
const target = inlineMatch[1] === "from" ? currentFrom : currentTo;
|
|
225
|
+
const pairs = inlineMatch[2].split(",");
|
|
226
|
+
for (const pair of pairs) {
|
|
227
|
+
const kv = pair.match(/(\w+):\s*"?([^",]+)"?/);
|
|
228
|
+
if (kv)
|
|
229
|
+
target[kv[1].trim()] = kv[2].trim();
|
|
230
|
+
}
|
|
231
|
+
inFrom = false;
|
|
232
|
+
inTo = false;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Block from/to start: " from:" or " to:"
|
|
236
|
+
const blockMatch = trimmed.match(/^ {6}(from|to):\s*$/);
|
|
237
|
+
if (blockMatch) {
|
|
238
|
+
inFrom = blockMatch[1] === "from";
|
|
239
|
+
inTo = blockMatch[1] === "to";
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Sub-keys under from/to: " type: component"
|
|
243
|
+
const subKeyMatch = trimmed.match(/^ {8}(\w+):\s*"?([^"]+)"?\s*$/);
|
|
244
|
+
if (subKeyMatch) {
|
|
245
|
+
const target = inFrom ? currentFrom : inTo ? currentTo : null;
|
|
246
|
+
if (target)
|
|
247
|
+
target[subKeyMatch[1].trim()] = subKeyMatch[2].trim();
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
// Top-level item property: " level: error"
|
|
251
|
+
const propMatch = trimmed.match(/^ {6}(\w+):\s*"?([^"{]+)"?\s*$/);
|
|
252
|
+
if (propMatch && propMatch[1] !== "from" && propMatch[1] !== "to") {
|
|
253
|
+
currentItem[propMatch[1].trim()] = propMatch[2].trim();
|
|
254
|
+
inFrom = false;
|
|
255
|
+
inTo = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (inItem)
|
|
259
|
+
flushItem();
|
|
260
|
+
return rules;
|
|
261
|
+
}
|
|
262
|
+
export function getRuleLevel(config, rule, defaultLevel) {
|
|
263
|
+
const entry = config[rule];
|
|
264
|
+
if (!entry)
|
|
265
|
+
return defaultLevel;
|
|
266
|
+
if (typeof entry === "string")
|
|
267
|
+
return entry;
|
|
268
|
+
return entry.level ?? defaultLevel;
|
|
269
|
+
}
|
|
270
|
+
export function getThreshold(config, rule, defaultVal) {
|
|
271
|
+
const entry = config[rule];
|
|
272
|
+
if (!entry || typeof entry === "string")
|
|
273
|
+
return defaultVal;
|
|
274
|
+
return entry.threshold ?? defaultVal;
|
|
275
|
+
}
|