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.
Files changed (142) hide show
  1. package/README.md +282 -0
  2. package/bin/archbyte.js +213 -0
  3. package/dist/agents/core/component-detector.d.ts +2 -0
  4. package/dist/agents/core/component-detector.js +57 -0
  5. package/dist/agents/core/connection-mapper.d.ts +2 -0
  6. package/dist/agents/core/connection-mapper.js +77 -0
  7. package/dist/agents/core/doc-parser.d.ts +2 -0
  8. package/dist/agents/core/doc-parser.js +64 -0
  9. package/dist/agents/core/env-detector.d.ts +2 -0
  10. package/dist/agents/core/env-detector.js +51 -0
  11. package/dist/agents/core/event-detector.d.ts +2 -0
  12. package/dist/agents/core/event-detector.js +59 -0
  13. package/dist/agents/core/infra-analyzer.d.ts +2 -0
  14. package/dist/agents/core/infra-analyzer.js +72 -0
  15. package/dist/agents/core/structure-scanner.d.ts +2 -0
  16. package/dist/agents/core/structure-scanner.js +55 -0
  17. package/dist/agents/core/validator.d.ts +2 -0
  18. package/dist/agents/core/validator.js +74 -0
  19. package/dist/agents/index.d.ts +24 -0
  20. package/dist/agents/index.js +73 -0
  21. package/dist/agents/llm/index.d.ts +8 -0
  22. package/dist/agents/llm/index.js +185 -0
  23. package/dist/agents/llm/prompt-builder.d.ts +3 -0
  24. package/dist/agents/llm/prompt-builder.js +251 -0
  25. package/dist/agents/llm/response-parser.d.ts +6 -0
  26. package/dist/agents/llm/response-parser.js +174 -0
  27. package/dist/agents/llm/types.d.ts +31 -0
  28. package/dist/agents/llm/types.js +2 -0
  29. package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
  30. package/dist/agents/pipeline/agents/component-identifier.js +102 -0
  31. package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
  32. package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
  33. package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
  34. package/dist/agents/pipeline/agents/flow-detector.js +101 -0
  35. package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
  36. package/dist/agents/pipeline/agents/service-describer.js +100 -0
  37. package/dist/agents/pipeline/agents/validator.d.ts +3 -0
  38. package/dist/agents/pipeline/agents/validator.js +102 -0
  39. package/dist/agents/pipeline/index.d.ts +13 -0
  40. package/dist/agents/pipeline/index.js +128 -0
  41. package/dist/agents/pipeline/merger.d.ts +7 -0
  42. package/dist/agents/pipeline/merger.js +212 -0
  43. package/dist/agents/pipeline/response-parser.d.ts +5 -0
  44. package/dist/agents/pipeline/response-parser.js +43 -0
  45. package/dist/agents/pipeline/types.d.ts +92 -0
  46. package/dist/agents/pipeline/types.js +3 -0
  47. package/dist/agents/prompt-data.d.ts +1 -0
  48. package/dist/agents/prompt-data.js +15 -0
  49. package/dist/agents/prompts-encode.d.ts +9 -0
  50. package/dist/agents/prompts-encode.js +26 -0
  51. package/dist/agents/prompts.d.ts +12 -0
  52. package/dist/agents/prompts.js +30 -0
  53. package/dist/agents/providers/anthropic.d.ts +10 -0
  54. package/dist/agents/providers/anthropic.js +117 -0
  55. package/dist/agents/providers/google.d.ts +10 -0
  56. package/dist/agents/providers/google.js +136 -0
  57. package/dist/agents/providers/ollama.d.ts +9 -0
  58. package/dist/agents/providers/ollama.js +162 -0
  59. package/dist/agents/providers/openai.d.ts +9 -0
  60. package/dist/agents/providers/openai.js +142 -0
  61. package/dist/agents/providers/router.d.ts +7 -0
  62. package/dist/agents/providers/router.js +55 -0
  63. package/dist/agents/runtime/orchestrator.d.ts +34 -0
  64. package/dist/agents/runtime/orchestrator.js +193 -0
  65. package/dist/agents/runtime/registry.d.ts +23 -0
  66. package/dist/agents/runtime/registry.js +56 -0
  67. package/dist/agents/runtime/types.d.ts +117 -0
  68. package/dist/agents/runtime/types.js +29 -0
  69. package/dist/agents/static/code-sampler.d.ts +3 -0
  70. package/dist/agents/static/code-sampler.js +153 -0
  71. package/dist/agents/static/component-detector.d.ts +3 -0
  72. package/dist/agents/static/component-detector.js +404 -0
  73. package/dist/agents/static/connection-mapper.d.ts +3 -0
  74. package/dist/agents/static/connection-mapper.js +280 -0
  75. package/dist/agents/static/doc-parser.d.ts +3 -0
  76. package/dist/agents/static/doc-parser.js +358 -0
  77. package/dist/agents/static/env-detector.d.ts +3 -0
  78. package/dist/agents/static/env-detector.js +73 -0
  79. package/dist/agents/static/event-detector.d.ts +3 -0
  80. package/dist/agents/static/event-detector.js +70 -0
  81. package/dist/agents/static/file-tree-collector.d.ts +3 -0
  82. package/dist/agents/static/file-tree-collector.js +51 -0
  83. package/dist/agents/static/index.d.ts +19 -0
  84. package/dist/agents/static/index.js +307 -0
  85. package/dist/agents/static/infra-analyzer.d.ts +3 -0
  86. package/dist/agents/static/infra-analyzer.js +208 -0
  87. package/dist/agents/static/structure-scanner.d.ts +3 -0
  88. package/dist/agents/static/structure-scanner.js +195 -0
  89. package/dist/agents/static/types.d.ts +165 -0
  90. package/dist/agents/static/types.js +2 -0
  91. package/dist/agents/static/utils.d.ts +21 -0
  92. package/dist/agents/static/utils.js +146 -0
  93. package/dist/agents/static/validator.d.ts +2 -0
  94. package/dist/agents/static/validator.js +75 -0
  95. package/dist/agents/tools/claude-code.d.ts +38 -0
  96. package/dist/agents/tools/claude-code.js +129 -0
  97. package/dist/agents/tools/local-fs.d.ts +12 -0
  98. package/dist/agents/tools/local-fs.js +112 -0
  99. package/dist/agents/tools/tool-definitions.d.ts +6 -0
  100. package/dist/agents/tools/tool-definitions.js +66 -0
  101. package/dist/cli/analyze.d.ts +27 -0
  102. package/dist/cli/analyze.js +586 -0
  103. package/dist/cli/auth.d.ts +46 -0
  104. package/dist/cli/auth.js +397 -0
  105. package/dist/cli/config.d.ts +11 -0
  106. package/dist/cli/config.js +177 -0
  107. package/dist/cli/diff.d.ts +10 -0
  108. package/dist/cli/diff.js +144 -0
  109. package/dist/cli/export.d.ts +10 -0
  110. package/dist/cli/export.js +321 -0
  111. package/dist/cli/gate.d.ts +13 -0
  112. package/dist/cli/gate.js +131 -0
  113. package/dist/cli/generate.d.ts +10 -0
  114. package/dist/cli/generate.js +213 -0
  115. package/dist/cli/license-gate.d.ts +27 -0
  116. package/dist/cli/license-gate.js +121 -0
  117. package/dist/cli/patrol.d.ts +15 -0
  118. package/dist/cli/patrol.js +212 -0
  119. package/dist/cli/run.d.ts +11 -0
  120. package/dist/cli/run.js +24 -0
  121. package/dist/cli/serve.d.ts +9 -0
  122. package/dist/cli/serve.js +65 -0
  123. package/dist/cli/setup.d.ts +1 -0
  124. package/dist/cli/setup.js +233 -0
  125. package/dist/cli/shared.d.ts +68 -0
  126. package/dist/cli/shared.js +275 -0
  127. package/dist/cli/stats.d.ts +9 -0
  128. package/dist/cli/stats.js +158 -0
  129. package/dist/cli/ui.d.ts +18 -0
  130. package/dist/cli/ui.js +144 -0
  131. package/dist/cli/validate.d.ts +54 -0
  132. package/dist/cli/validate.js +315 -0
  133. package/dist/cli/workflow.d.ts +10 -0
  134. package/dist/cli/workflow.js +594 -0
  135. package/dist/server/src/generator/index.d.ts +123 -0
  136. package/dist/server/src/generator/index.js +254 -0
  137. package/dist/server/src/index.d.ts +8 -0
  138. package/dist/server/src/index.js +1311 -0
  139. package/package.json +62 -0
  140. package/ui/dist/assets/index-B66Til39.js +70 -0
  141. package/ui/dist/assets/index-BE2OWbzu.css +1 -0
  142. 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
+ }
@@ -0,0 +1,11 @@
1
+ interface RunOptions {
2
+ static?: boolean;
3
+ skipLlm?: boolean;
4
+ provider?: string;
5
+ apiKey?: string;
6
+ port?: number;
7
+ verbose?: boolean;
8
+ dryRun?: boolean;
9
+ }
10
+ export declare function handleRun(options: RunOptions): Promise<void>;
11
+ export {};
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ interface ServeOptions {
2
+ port?: number;
3
+ diagram?: string;
4
+ }
5
+ /**
6
+ * Start the ArchByte UI server
7
+ */
8
+ export declare function handleServe(options: ServeOptions): Promise<void>;
9
+ export {};