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,213 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { generateArchitecture } from "../server/src/generator/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generate excalidraw diagram from analysis JSON
|
|
7
|
+
*/
|
|
8
|
+
export async function handleGenerate(options) {
|
|
9
|
+
const rootDir = process.cwd();
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(chalk.bold.cyan("⚡ ArchByte Generator"));
|
|
12
|
+
console.log(chalk.gray("Generating architecture diagram from analysis..."));
|
|
13
|
+
console.log();
|
|
14
|
+
try {
|
|
15
|
+
// Find input file
|
|
16
|
+
const inputPath = options.input || path.join(rootDir, ".archbyte", "analysis.json");
|
|
17
|
+
if (!fs.existsSync(inputPath)) {
|
|
18
|
+
console.error(chalk.red(`Analysis file not found: ${inputPath}`));
|
|
19
|
+
console.error(chalk.gray("Run /analyze in Claude Code first, or provide --input path"));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Load analysis
|
|
23
|
+
const analysisContent = fs.readFileSync(inputPath, "utf-8");
|
|
24
|
+
let analysis;
|
|
25
|
+
try {
|
|
26
|
+
analysis = JSON.parse(analysisContent);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error(chalk.red(`Invalid JSON in analysis file: ${inputPath}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (options.verbose) {
|
|
33
|
+
console.log(chalk.gray(`Loaded analysis from: ${inputPath}`));
|
|
34
|
+
console.log(chalk.gray(` Components: ${analysis.components.length}`));
|
|
35
|
+
console.log(chalk.gray(` Databases: ${analysis.databases.length}`));
|
|
36
|
+
console.log(chalk.gray(` External Services: ${analysis.externalServices.length}`));
|
|
37
|
+
console.log(chalk.gray(` Connections: ${analysis.connections.length}`));
|
|
38
|
+
}
|
|
39
|
+
// Convert to ParsedProject format
|
|
40
|
+
const project = {
|
|
41
|
+
name: analysis.project.name,
|
|
42
|
+
description: analysis.project.description,
|
|
43
|
+
components: analysis.components.map(c => ({
|
|
44
|
+
id: c.id,
|
|
45
|
+
name: c.name,
|
|
46
|
+
type: c.type,
|
|
47
|
+
path: c.path,
|
|
48
|
+
techStack: c.techStack,
|
|
49
|
+
entryPoints: [],
|
|
50
|
+
dependencies: [],
|
|
51
|
+
devDependencies: [],
|
|
52
|
+
ports: c.ports,
|
|
53
|
+
description: c.description,
|
|
54
|
+
environments: c.environments,
|
|
55
|
+
})),
|
|
56
|
+
services: [],
|
|
57
|
+
databases: analysis.databases.map(d => ({
|
|
58
|
+
id: d.id,
|
|
59
|
+
name: d.name,
|
|
60
|
+
type: d.type,
|
|
61
|
+
})),
|
|
62
|
+
externalServices: analysis.externalServices.map(e => ({
|
|
63
|
+
id: e.id,
|
|
64
|
+
name: e.name,
|
|
65
|
+
type: e.type,
|
|
66
|
+
envVars: [],
|
|
67
|
+
environments: e.environments,
|
|
68
|
+
color: e.color,
|
|
69
|
+
strokeColor: e.strokeColor,
|
|
70
|
+
})),
|
|
71
|
+
connections: analysis.connections.map(c => ({
|
|
72
|
+
from: c.from,
|
|
73
|
+
to: c.to,
|
|
74
|
+
type: c.type,
|
|
75
|
+
label: c.description || c.type.toUpperCase(),
|
|
76
|
+
environments: c.environments,
|
|
77
|
+
color: c.color,
|
|
78
|
+
eventType: c.eventType,
|
|
79
|
+
async: c.async,
|
|
80
|
+
})),
|
|
81
|
+
metadata: {
|
|
82
|
+
isMonorepo: analysis.project.isMonorepo || false,
|
|
83
|
+
workspaces: [],
|
|
84
|
+
hasDocker: true,
|
|
85
|
+
hasCi: false,
|
|
86
|
+
primaryLanguage: analysis.project.primaryLanguage,
|
|
87
|
+
frameworks: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
// Generate diagram
|
|
91
|
+
const diagram = generateArchitecture(project);
|
|
92
|
+
// Determine output path
|
|
93
|
+
const outputPath = options.output || path.join(rootDir, ".archbyte", "architecture.json");
|
|
94
|
+
// Merge with existing if present (preserve user positions)
|
|
95
|
+
if (fs.existsSync(outputPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const existingContent = fs.readFileSync(outputPath, "utf-8");
|
|
98
|
+
const existing = JSON.parse(existingContent);
|
|
99
|
+
// Build lookup of existing nodes
|
|
100
|
+
const existingNodes = new Map();
|
|
101
|
+
for (const node of existing.nodes || []) {
|
|
102
|
+
existingNodes.set(node.id, node);
|
|
103
|
+
}
|
|
104
|
+
// Apply existing positions to new diagram
|
|
105
|
+
for (const node of diagram.nodes) {
|
|
106
|
+
const existingNode = existingNodes.get(node.id);
|
|
107
|
+
if (existingNode) {
|
|
108
|
+
node.x = existingNode.x;
|
|
109
|
+
node.y = existingNode.y;
|
|
110
|
+
node.width = existingNode.width;
|
|
111
|
+
node.height = existingNode.height;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
console.log(chalk.gray(`Merged with existing diagram, preserved ${existingNodes.size} positions`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore merge errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Ensure output directory exists
|
|
123
|
+
const outputDir = path.dirname(outputPath);
|
|
124
|
+
if (!fs.existsSync(outputDir)) {
|
|
125
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
// Add environments if present
|
|
128
|
+
if (analysis.environments && analysis.environments.length > 0) {
|
|
129
|
+
diagram.environments = {};
|
|
130
|
+
for (const env of analysis.environments) {
|
|
131
|
+
diagram.environments[env.name] = {
|
|
132
|
+
label: env.label,
|
|
133
|
+
color: env.color,
|
|
134
|
+
source: env.source,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Add flows if present
|
|
139
|
+
if (analysis.flows && analysis.flows.length > 0) {
|
|
140
|
+
// Build a set of actual edge IDs and a prefix lookup for eventType-suffixed edges
|
|
141
|
+
const edgeIdSet = new Set(diagram.edges.map(e => e.id));
|
|
142
|
+
// Build lookup: for any "edge-A-B", find all edge IDs that start with "edge-A-B"
|
|
143
|
+
// This catches eventType-suffixed edges like "edge-A-B-task-created"
|
|
144
|
+
const findEdgeByPrefix = (prefix) => {
|
|
145
|
+
for (const id of edgeIdSet) {
|
|
146
|
+
if (id === prefix || id.startsWith(prefix + "-"))
|
|
147
|
+
return id;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
diagram.flows = analysis.flows.map(flow => ({
|
|
152
|
+
id: flow.id,
|
|
153
|
+
name: flow.name,
|
|
154
|
+
description: flow.description,
|
|
155
|
+
color: "#3b82f6", // Default blue for flows
|
|
156
|
+
steps: flow.steps
|
|
157
|
+
.map(step => {
|
|
158
|
+
const forwardId = `edge-${step.from}-${step.to}`;
|
|
159
|
+
const reverseId = `edge-${step.to}-${step.from}`;
|
|
160
|
+
// 1. Exact match
|
|
161
|
+
if (edgeIdSet.has(forwardId))
|
|
162
|
+
return { edge: forwardId, label: step.label };
|
|
163
|
+
if (edgeIdSet.has(reverseId))
|
|
164
|
+
return { edge: reverseId, label: step.label };
|
|
165
|
+
// 2. Try eventType-suffixed edges (e.g., edge-A-B-task-created)
|
|
166
|
+
const forwardMatch = findEdgeByPrefix(forwardId);
|
|
167
|
+
if (forwardMatch)
|
|
168
|
+
return { edge: forwardMatch, label: step.label };
|
|
169
|
+
const reverseMatch = findEdgeByPrefix(reverseId);
|
|
170
|
+
if (reverseMatch)
|
|
171
|
+
return { edge: reverseMatch, label: step.label };
|
|
172
|
+
return null; // Skip steps with no matching edge
|
|
173
|
+
})
|
|
174
|
+
.filter((s) => s !== null),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
// Write diagram
|
|
178
|
+
fs.writeFileSync(outputPath, JSON.stringify(diagram, null, 2), "utf-8");
|
|
179
|
+
console.log(chalk.green("✓ Diagram generated successfully!"));
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.bold("Summary:"));
|
|
182
|
+
console.log(chalk.gray(` Components: ${analysis.components.length}`));
|
|
183
|
+
console.log(chalk.gray(` Databases: ${analysis.databases.length}`));
|
|
184
|
+
console.log(chalk.gray(` External Services: ${analysis.externalServices.length}`));
|
|
185
|
+
console.log(chalk.gray(` Connections: ${analysis.connections.length}`));
|
|
186
|
+
if (analysis.environments && analysis.environments.length > 0) {
|
|
187
|
+
console.log(chalk.gray(` Environments: ${analysis.environments.map(e => e.name).join(", ")}`));
|
|
188
|
+
}
|
|
189
|
+
if (analysis.flows && analysis.flows.length > 0) {
|
|
190
|
+
console.log(chalk.gray(` Flows: ${analysis.flows.map(f => f.name).join(", ")}`));
|
|
191
|
+
}
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(chalk.green(`✓ Saved to: ${path.relative(rootDir, outputPath)}`));
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(chalk.bold("Next steps:"));
|
|
196
|
+
console.log(chalk.gray(` 1. Run ${chalk.cyan("archbyte serve")} to start the visualization server`));
|
|
197
|
+
console.log(chalk.gray(` 2. Open ${chalk.cyan("http://localhost:3847")} to view and adjust the diagram`));
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error();
|
|
202
|
+
if (error instanceof Error) {
|
|
203
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
204
|
+
if (options.verbose) {
|
|
205
|
+
console.error(chalk.gray(error.stack));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.error(chalk.red("An unexpected error occurred"));
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type GatedAction = "scan" | "analyze" | "generate";
|
|
2
|
+
/**
|
|
3
|
+
* Pre-flight license check. Must be called before scan/analyze/generate.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Load credentials from ~/.archbyte/credentials.json
|
|
7
|
+
* 2. If not logged in → block with login prompt
|
|
8
|
+
* 3. Call POST /v1/check-usage to verify server-side
|
|
9
|
+
* 4. If blocked → show upgrade message and exit
|
|
10
|
+
* 5. If allowed → cache verified tier and return
|
|
11
|
+
*
|
|
12
|
+
* Offline fallback: If the server is unreachable, check offline action
|
|
13
|
+
* limits. Free tier: 0 offline actions. Premium (cached): max 3 within
|
|
14
|
+
* the 1-hour cache window. Exceeding limits blocks the action.
|
|
15
|
+
*/
|
|
16
|
+
export declare function requireLicense(action: GatedAction): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Record a completed scan with the license server.
|
|
19
|
+
* Called after a successful scan/analyze/generate cycle.
|
|
20
|
+
* Non-blocking — failures are silently ignored.
|
|
21
|
+
*/
|
|
22
|
+
export declare function recordUsage(meta: {
|
|
23
|
+
projectName?: string;
|
|
24
|
+
agentCount?: number;
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadCredentials, cacheVerifiedTier, resetOfflineActions, checkOfflineAction } from "./auth.js";
|
|
3
|
+
const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
|
|
4
|
+
/**
|
|
5
|
+
* Pre-flight license check. Must be called before scan/analyze/generate.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Load credentials from ~/.archbyte/credentials.json
|
|
9
|
+
* 2. If not logged in → block with login prompt
|
|
10
|
+
* 3. Call POST /v1/check-usage to verify server-side
|
|
11
|
+
* 4. If blocked → show upgrade message and exit
|
|
12
|
+
* 5. If allowed → cache verified tier and return
|
|
13
|
+
*
|
|
14
|
+
* Offline fallback: If the server is unreachable, check offline action
|
|
15
|
+
* limits. Free tier: 0 offline actions. Premium (cached): max 3 within
|
|
16
|
+
* the 1-hour cache window. Exceeding limits blocks the action.
|
|
17
|
+
*/
|
|
18
|
+
export async function requireLicense(action) {
|
|
19
|
+
const creds = loadCredentials();
|
|
20
|
+
// Not logged in
|
|
21
|
+
if (!creds) {
|
|
22
|
+
console.error();
|
|
23
|
+
console.error(chalk.red("Authentication required."));
|
|
24
|
+
console.error();
|
|
25
|
+
console.error(chalk.gray("Sign in to use ArchByte:"));
|
|
26
|
+
console.error(chalk.gray(" archbyte login"));
|
|
27
|
+
console.error();
|
|
28
|
+
console.error(chalk.gray("Basic tier includes unlimited scans."));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Token expired locally
|
|
32
|
+
if (new Date(creds.expiresAt) < new Date()) {
|
|
33
|
+
console.error();
|
|
34
|
+
console.error(chalk.red("Session expired."));
|
|
35
|
+
console.error(chalk.gray("Run `archbyte login` to refresh your session."));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
// Check usage with server
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${API_BASE}/api/v1/check-usage`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${creds.token}`,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ action }),
|
|
47
|
+
signal: AbortSignal.timeout(5000),
|
|
48
|
+
});
|
|
49
|
+
if (res.status === 401) {
|
|
50
|
+
console.error();
|
|
51
|
+
console.error(chalk.red("Session invalid. Please log in again."));
|
|
52
|
+
console.error(chalk.gray(" archbyte login"));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
// Server error — enforce offline limits instead of allowing freely
|
|
57
|
+
return handleOfflineFallback("server error");
|
|
58
|
+
}
|
|
59
|
+
const data = (await res.json());
|
|
60
|
+
// Server verified — cache tier and reset offline counter
|
|
61
|
+
const tier = data.tier === "premium" ? "premium" : "free";
|
|
62
|
+
cacheVerifiedTier(tier, creds.email);
|
|
63
|
+
resetOfflineActions();
|
|
64
|
+
if (!data.allowed) {
|
|
65
|
+
console.error();
|
|
66
|
+
console.error(chalk.red.bold("Scan not allowed."));
|
|
67
|
+
console.error(chalk.white(data.message ?? "Check your account status."));
|
|
68
|
+
console.error();
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
// Network error — enforce offline limits
|
|
74
|
+
const reason = err instanceof Error &&
|
|
75
|
+
(err.name === "TimeoutError" || err.name === "AbortError")
|
|
76
|
+
? "timeout" : "network error";
|
|
77
|
+
return handleOfflineFallback(reason);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handle offline fallback with strict action limits.
|
|
82
|
+
* Free tier: 0 offline actions (always blocked).
|
|
83
|
+
* Premium tier (cached): up to 3 offline actions within 1-hour window.
|
|
84
|
+
*/
|
|
85
|
+
function handleOfflineFallback(reason) {
|
|
86
|
+
const { allowed, reason: blockReason } = checkOfflineAction();
|
|
87
|
+
if (!allowed) {
|
|
88
|
+
console.error();
|
|
89
|
+
console.error(chalk.red(`License server unreachable (${reason}).`));
|
|
90
|
+
console.error(chalk.red(blockReason ?? "Offline actions not permitted."));
|
|
91
|
+
console.error();
|
|
92
|
+
console.error(chalk.gray("Check your internet connection and try again."));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// Premium user within offline grace window
|
|
96
|
+
console.warn(chalk.yellow(`⚠ License server unreachable (${reason}). Using offline grace period.`));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Record a completed scan with the license server.
|
|
100
|
+
* Called after a successful scan/analyze/generate cycle.
|
|
101
|
+
* Non-blocking — failures are silently ignored.
|
|
102
|
+
*/
|
|
103
|
+
export async function recordUsage(meta) {
|
|
104
|
+
const creds = loadCredentials();
|
|
105
|
+
if (!creds)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
await fetch(`${API_BASE}/api/v1/scans`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${creds.token}`,
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(meta),
|
|
115
|
+
signal: AbortSignal.timeout(5000),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Silently ignore — usage recording is best-effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface PatrolOptions {
|
|
2
|
+
diagram?: string;
|
|
3
|
+
config?: string;
|
|
4
|
+
interval?: string;
|
|
5
|
+
onViolation?: string;
|
|
6
|
+
daemon?: boolean;
|
|
7
|
+
history?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run the architecture patrol daemon.
|
|
11
|
+
* Inspired by Gastown's patrol loop pattern — cyclic monitoring
|
|
12
|
+
* that detects drift and reports violations.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handlePatrol(options: PatrolOptions): Promise<void>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { runValidation } from "./validate.js";
|
|
5
|
+
const PATROL_DIR = ".archbyte/patrols";
|
|
6
|
+
const HISTORY_FILE = "history.jsonl";
|
|
7
|
+
const LATEST_FILE = "latest.json";
|
|
8
|
+
function parseInterval(str) {
|
|
9
|
+
const match = str.match(/^(\d+)(s|m|h)$/);
|
|
10
|
+
if (!match) {
|
|
11
|
+
console.error(chalk.red(`Invalid interval: "${str}". Use format like 30s, 5m, 1h`));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const [, num, unit] = match;
|
|
15
|
+
const multipliers = { s: 1000, m: 60_000, h: 3_600_000 };
|
|
16
|
+
return parseInt(num) * multipliers[unit];
|
|
17
|
+
}
|
|
18
|
+
function ensurePatrolDir() {
|
|
19
|
+
const dir = path.join(process.cwd(), PATROL_DIR);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
function loadLatestRecord(patrolDir) {
|
|
26
|
+
const latestPath = path.join(patrolDir, LATEST_FILE);
|
|
27
|
+
if (!fs.existsSync(latestPath))
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(latestPath, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function saveRecord(patrolDir, record) {
|
|
37
|
+
// Write latest
|
|
38
|
+
fs.writeFileSync(path.join(patrolDir, LATEST_FILE), JSON.stringify(record, null, 2), "utf-8");
|
|
39
|
+
// Append to history
|
|
40
|
+
fs.appendFileSync(path.join(patrolDir, HISTORY_FILE), JSON.stringify(record) + "\n", "utf-8");
|
|
41
|
+
}
|
|
42
|
+
function diffViolations(previous, current) {
|
|
43
|
+
const key = (v) => `${v.rule}:${v.message}`;
|
|
44
|
+
const prevKeys = new Set(previous.map(key));
|
|
45
|
+
const currKeys = new Set(current.map(key));
|
|
46
|
+
return {
|
|
47
|
+
newViolations: current.filter((v) => !prevKeys.has(key(v))),
|
|
48
|
+
resolvedViolations: previous.filter((v) => !currKeys.has(key(v))),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function runPatrolCycle(options, patrolDir) {
|
|
52
|
+
const previous = loadLatestRecord(patrolDir);
|
|
53
|
+
const result = runValidation({
|
|
54
|
+
diagram: options.diagram,
|
|
55
|
+
config: options.config,
|
|
56
|
+
});
|
|
57
|
+
const { newViolations, resolvedViolations } = previous
|
|
58
|
+
? diffViolations(previous.violations, result.violations)
|
|
59
|
+
: { newViolations: result.violations, resolvedViolations: [] };
|
|
60
|
+
const record = {
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
passed: result.errors === 0,
|
|
63
|
+
errors: result.errors,
|
|
64
|
+
warnings: result.warnings,
|
|
65
|
+
violations: result.violations,
|
|
66
|
+
newViolations,
|
|
67
|
+
resolvedViolations,
|
|
68
|
+
totalNodes: result.totalNodes,
|
|
69
|
+
totalEdges: result.totalEdges,
|
|
70
|
+
};
|
|
71
|
+
saveRecord(patrolDir, record);
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
function printPatrolResult(record, cycleNum) {
|
|
75
|
+
const time = new Date(record.timestamp).toLocaleTimeString();
|
|
76
|
+
const status = record.passed ? chalk.green("HEALTHY") : chalk.red("VIOLATION");
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(chalk.bold.cyan(` Patrol #${cycleNum} — ${time} — ${status}`));
|
|
79
|
+
if (record.newViolations.length > 0) {
|
|
80
|
+
console.log(chalk.red(` ${record.newViolations.length} new violation(s):`));
|
|
81
|
+
for (const v of record.newViolations) {
|
|
82
|
+
const icon = v.level === "error" ? chalk.red("!!") : chalk.yellow("!!");
|
|
83
|
+
console.log(chalk.gray(` ${icon} [${v.rule}] ${v.message}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (record.resolvedViolations.length > 0) {
|
|
87
|
+
console.log(chalk.green(` ${record.resolvedViolations.length} resolved:`));
|
|
88
|
+
for (const v of record.resolvedViolations) {
|
|
89
|
+
console.log(chalk.green(` -- [${v.rule}] ${v.message}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (record.newViolations.length === 0 && record.resolvedViolations.length === 0) {
|
|
93
|
+
console.log(chalk.gray(` No changes — ${record.errors} errors, ${record.warnings} warnings`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function printHistory(patrolDir) {
|
|
97
|
+
const historyPath = path.join(patrolDir, HISTORY_FILE);
|
|
98
|
+
if (!fs.existsSync(historyPath)) {
|
|
99
|
+
console.log(chalk.yellow(" No patrol history found. Run archbyte patrol to start."));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const lines = fs.readFileSync(historyPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
103
|
+
if (lines.length === 0) {
|
|
104
|
+
console.log(chalk.yellow(" No patrol history found."));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.bold.cyan(` ArchByte Patrol History — ${projectName}`));
|
|
110
|
+
console.log();
|
|
111
|
+
// Show last 20 records (skip malformed lines)
|
|
112
|
+
const records = lines.slice(-20).flatMap((l) => {
|
|
113
|
+
try {
|
|
114
|
+
return [JSON.parse(l)];
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// Health sparkline
|
|
121
|
+
const sparkline = records
|
|
122
|
+
.map((r) => (r.passed ? chalk.green("*") : chalk.red("*")))
|
|
123
|
+
.join("");
|
|
124
|
+
console.log(` Health: ${sparkline} (last ${records.length} patrols)`);
|
|
125
|
+
console.log();
|
|
126
|
+
// Table
|
|
127
|
+
console.log(chalk.gray(" Time Status Errors Warnings New Resolved"));
|
|
128
|
+
console.log(chalk.gray(" " + "-".repeat(68)));
|
|
129
|
+
for (const r of records) {
|
|
130
|
+
const time = new Date(r.timestamp).toLocaleString().padEnd(20);
|
|
131
|
+
const status = r.passed ? chalk.green("PASS ") : chalk.red("FAIL ");
|
|
132
|
+
const errors = String(r.errors).padEnd(8);
|
|
133
|
+
const warnings = String(r.warnings).padEnd(10);
|
|
134
|
+
const newV = String(r.newViolations.length).padEnd(5);
|
|
135
|
+
const resolved = String(r.resolvedViolations.length);
|
|
136
|
+
console.log(` ${time} ${status} ${errors}${warnings}${newV}${resolved}`);
|
|
137
|
+
}
|
|
138
|
+
console.log();
|
|
139
|
+
// Summary
|
|
140
|
+
const totalPatrols = lines.length;
|
|
141
|
+
const failedPatrols = lines.filter((l) => !JSON.parse(l).passed).length;
|
|
142
|
+
const healthPct = Math.round(((totalPatrols - failedPatrols) / totalPatrols) * 100);
|
|
143
|
+
console.log(` Total patrols: ${totalPatrols} | Health rate: ${healthPct}% | Failed: ${failedPatrols}`);
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
function handleViolationAction(record, action) {
|
|
147
|
+
if (record.newViolations.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
switch (action) {
|
|
150
|
+
case "json":
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
event: "patrol-violation",
|
|
153
|
+
timestamp: record.timestamp,
|
|
154
|
+
newViolations: record.newViolations,
|
|
155
|
+
}));
|
|
156
|
+
break;
|
|
157
|
+
case "log":
|
|
158
|
+
default:
|
|
159
|
+
// Already printed in printPatrolResult
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run the architecture patrol daemon.
|
|
165
|
+
* Inspired by Gastown's patrol loop pattern — cyclic monitoring
|
|
166
|
+
* that detects drift and reports violations.
|
|
167
|
+
*/
|
|
168
|
+
export async function handlePatrol(options) {
|
|
169
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
170
|
+
const patrolDir = ensurePatrolDir();
|
|
171
|
+
// History mode
|
|
172
|
+
if (options.history) {
|
|
173
|
+
printHistory(patrolDir);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const intervalMs = parseInterval(options.interval || "5m");
|
|
177
|
+
const intervalStr = options.interval || "5m";
|
|
178
|
+
const action = options.onViolation || "log";
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(chalk.bold.cyan(` ArchByte Patrol — ${projectName}`));
|
|
181
|
+
console.log(chalk.gray(` Interval: ${intervalStr} | On violation: ${action}`));
|
|
182
|
+
console.log(chalk.gray(` History: ${path.join(PATROL_DIR, HISTORY_FILE)}`));
|
|
183
|
+
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
|
184
|
+
// Run initial cycle immediately
|
|
185
|
+
let cycleNum = 1;
|
|
186
|
+
const record = runPatrolCycle(options, patrolDir);
|
|
187
|
+
printPatrolResult(record, cycleNum);
|
|
188
|
+
handleViolationAction(record, action);
|
|
189
|
+
// Patrol loop
|
|
190
|
+
const timer = setInterval(() => {
|
|
191
|
+
cycleNum++;
|
|
192
|
+
try {
|
|
193
|
+
const record = runPatrolCycle(options, patrolDir);
|
|
194
|
+
printPatrolResult(record, cycleNum);
|
|
195
|
+
handleViolationAction(record, action);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
console.error(chalk.red(` Patrol cycle #${cycleNum} failed: ${err}`));
|
|
199
|
+
}
|
|
200
|
+
}, intervalMs);
|
|
201
|
+
// Graceful shutdown
|
|
202
|
+
const shutdown = () => {
|
|
203
|
+
clearInterval(timer);
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.gray(` Patrol stopped after ${cycleNum} cycles.`));
|
|
206
|
+
process.exit(0);
|
|
207
|
+
};
|
|
208
|
+
process.on("SIGINT", shutdown);
|
|
209
|
+
process.on("SIGTERM", shutdown);
|
|
210
|
+
// Keep alive
|
|
211
|
+
await new Promise(() => { });
|
|
212
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { handleAnalyze } from "./analyze.js";
|
|
3
|
+
import { handleServe } from "./serve.js";
|
|
4
|
+
export async function handleRun(options) {
|
|
5
|
+
const port = options.port || 3847;
|
|
6
|
+
console.log();
|
|
7
|
+
console.log(chalk.bold.cyan(" ArchByte Run"));
|
|
8
|
+
console.log();
|
|
9
|
+
// 1. Analyze (includes auto-generate)
|
|
10
|
+
await handleAnalyze({
|
|
11
|
+
verbose: options.verbose,
|
|
12
|
+
static: options.static,
|
|
13
|
+
skipLlm: options.skipLlm,
|
|
14
|
+
provider: options.provider,
|
|
15
|
+
apiKey: options.apiKey,
|
|
16
|
+
dryRun: options.dryRun,
|
|
17
|
+
});
|
|
18
|
+
if (options.dryRun)
|
|
19
|
+
return;
|
|
20
|
+
// 2. Serve the UI
|
|
21
|
+
console.log(chalk.bold.cyan(" Starting UI..."));
|
|
22
|
+
console.log();
|
|
23
|
+
await handleServe({ port });
|
|
24
|
+
}
|