create-stylus-ide 1.0.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 (135) hide show
  1. package/Readme.MD +1515 -0
  2. package/cli.js +28 -0
  3. package/frontend/.vscode/settings.json +9 -0
  4. package/frontend/app/api/chat/route.ts +101 -0
  5. package/frontend/app/api/check-setup/route.ts +93 -0
  6. package/frontend/app/api/cleanup/route.ts +14 -0
  7. package/frontend/app/api/compile/route.ts +95 -0
  8. package/frontend/app/api/compile-stream/route.ts +98 -0
  9. package/frontend/app/api/complete/route.ts +86 -0
  10. package/frontend/app/api/deploy/route.ts +118 -0
  11. package/frontend/app/api/export-abi/route.ts +58 -0
  12. package/frontend/app/favicon.ico +0 -0
  13. package/frontend/app/globals.css +177 -0
  14. package/frontend/app/layout.tsx +29 -0
  15. package/frontend/app/ml/page.tsx +694 -0
  16. package/frontend/app/page.tsx +1132 -0
  17. package/frontend/app/providers.tsx +18 -0
  18. package/frontend/app/qlearning/page.tsx +188 -0
  19. package/frontend/app/raytracing/page.tsx +268 -0
  20. package/frontend/components/abi/ABIDialog.tsx +132 -0
  21. package/frontend/components/ai/AICompletionPopup.tsx +76 -0
  22. package/frontend/components/ai/ChatPanel.tsx +292 -0
  23. package/frontend/components/ai/QuickActions.tsx +128 -0
  24. package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
  25. package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
  26. package/frontend/components/deploy/DeployDialog.tsx +334 -0
  27. package/frontend/components/editor/FileTabs.tsx +181 -0
  28. package/frontend/components/editor/MonacoEditor.tsx +306 -0
  29. package/frontend/components/file-tree/ContextMenu.tsx +110 -0
  30. package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
  31. package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
  32. package/frontend/components/file-tree/FileNode.tsx +60 -0
  33. package/frontend/components/file-tree/FileTree.tsx +259 -0
  34. package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
  35. package/frontend/components/file-tree/FolderNode.tsx +105 -0
  36. package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
  37. package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
  38. package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
  39. package/frontend/components/github/URLCopyButton.tsx +60 -0
  40. package/frontend/components/interact/ContractInteraction.tsx +323 -0
  41. package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
  42. package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
  43. package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
  44. package/frontend/components/project/ProjectActions.tsx +176 -0
  45. package/frontend/components/q-learning/ContractConfig.tsx +172 -0
  46. package/frontend/components/q-learning/MazeGrid.tsx +346 -0
  47. package/frontend/components/q-learning/PathAnimation.tsx +384 -0
  48. package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
  49. package/frontend/components/q-learning/TrainingForm.tsx +349 -0
  50. package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
  51. package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
  52. package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
  53. package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
  54. package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
  55. package/frontend/components/setup/SetupGuide.tsx +290 -0
  56. package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
  57. package/frontend/components/ui/alert-dialog.tsx +157 -0
  58. package/frontend/components/ui/alert.tsx +66 -0
  59. package/frontend/components/ui/badge.tsx +46 -0
  60. package/frontend/components/ui/button.tsx +62 -0
  61. package/frontend/components/ui/card.tsx +92 -0
  62. package/frontend/components/ui/context-menu.tsx +252 -0
  63. package/frontend/components/ui/dialog.tsx +143 -0
  64. package/frontend/components/ui/dropdown-menu.tsx +257 -0
  65. package/frontend/components/ui/input.tsx +21 -0
  66. package/frontend/components/ui/label.tsx +24 -0
  67. package/frontend/components/ui/progress.tsx +31 -0
  68. package/frontend/components/ui/scroll-area.tsx +58 -0
  69. package/frontend/components/ui/select.tsx +190 -0
  70. package/frontend/components/ui/separator.tsx +28 -0
  71. package/frontend/components/ui/sheet.tsx +139 -0
  72. package/frontend/components/ui/skeleton.tsx +13 -0
  73. package/frontend/components/ui/slider.tsx +63 -0
  74. package/frontend/components/ui/sonner.tsx +40 -0
  75. package/frontend/components/ui/tabs.tsx +66 -0
  76. package/frontend/components/ui/textarea.tsx +18 -0
  77. package/frontend/components/wallet/ConnectButton.tsx +167 -0
  78. package/frontend/components/wallet/FaucetButton.tsx +256 -0
  79. package/frontend/components.json +22 -0
  80. package/frontend/eslint.config.mjs +18 -0
  81. package/frontend/hooks/useAICompletion.ts +75 -0
  82. package/frontend/hooks/useBlockchainLoader.ts +58 -0
  83. package/frontend/hooks/useChats.ts +137 -0
  84. package/frontend/hooks/useCompilation.ts +173 -0
  85. package/frontend/hooks/useFileTabs.ts +178 -0
  86. package/frontend/hooks/useGitHubLoader.ts +50 -0
  87. package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
  88. package/frontend/hooks/usePanelState.ts +115 -0
  89. package/frontend/hooks/useProjectState.ts +276 -0
  90. package/frontend/hooks/useResponsive.ts +29 -0
  91. package/frontend/lib/abi-parser.ts +58 -0
  92. package/frontend/lib/blockchain-api.ts +374 -0
  93. package/frontend/lib/blockchain-explorers.ts +75 -0
  94. package/frontend/lib/blockchain-loader.ts +112 -0
  95. package/frontend/lib/cargo-template.ts +64 -0
  96. package/frontend/lib/compilation.ts +529 -0
  97. package/frontend/lib/constants.ts +31 -0
  98. package/frontend/lib/deployment.ts +176 -0
  99. package/frontend/lib/file-utils.ts +83 -0
  100. package/frontend/lib/github-api.ts +246 -0
  101. package/frontend/lib/github-loader.ts +369 -0
  102. package/frontend/lib/ml-contract-template.txt +900 -0
  103. package/frontend/lib/orbit-chains.ts +181 -0
  104. package/frontend/lib/output-formatter.ts +68 -0
  105. package/frontend/lib/project-manager.ts +632 -0
  106. package/frontend/lib/ray-tracing-abi.ts +206 -0
  107. package/frontend/lib/storage.ts +189 -0
  108. package/frontend/lib/templates.ts +1662 -0
  109. package/frontend/lib/url-parser.ts +188 -0
  110. package/frontend/lib/utils.ts +6 -0
  111. package/frontend/lib/wagmi-config.ts +24 -0
  112. package/frontend/next.config.ts +7 -0
  113. package/frontend/package-lock.json +16259 -0
  114. package/frontend/package.json +60 -0
  115. package/frontend/postcss.config.mjs +7 -0
  116. package/frontend/public/file.svg +1 -0
  117. package/frontend/public/globe.svg +1 -0
  118. package/frontend/public/ml-weights/.gitkeep +0 -0
  119. package/frontend/public/ml-weights/model.pkl +0 -0
  120. package/frontend/public/ml-weights/model_weights.json +27102 -0
  121. package/frontend/public/ml-weights/test_samples.json +7888 -0
  122. package/frontend/public/next.svg +1 -0
  123. package/frontend/public/vercel.svg +1 -0
  124. package/frontend/public/window.svg +1 -0
  125. package/frontend/scripts/check-env.js +52 -0
  126. package/frontend/scripts/setup.js +285 -0
  127. package/frontend/tailwind.config.ts +64 -0
  128. package/frontend/tsconfig.json +34 -0
  129. package/frontend/types/blockchain.ts +63 -0
  130. package/frontend/types/github.ts +54 -0
  131. package/frontend/types/project.ts +106 -0
  132. package/ml-training/README.md +56 -0
  133. package/ml-training/train_tiny_model.py +325 -0
  134. package/ml-training/update_template.py +59 -0
  135. package/package.json +30 -0
@@ -0,0 +1,176 @@
1
+ // deployment.ts
2
+ import { spawn } from "child_process";
3
+ import { COMPILATION_CONSTANTS } from "./constants";
4
+ import type { CompilationOutput } from "./compilation";
5
+
6
+ export interface DeploymentResult {
7
+ success: boolean;
8
+ contractAddress?: string;
9
+ deploymentTxHash?: string;
10
+ activationTxHash?: string;
11
+ rpcUsed?: string;
12
+ error?: string;
13
+ output: CompilationOutput[];
14
+ }
15
+
16
+ function stripAnsi(input: string) {
17
+ return input.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
18
+ }
19
+
20
+ function extractDeployInfo(allText: string) {
21
+ const text = stripAnsi(allText);
22
+
23
+ const addr =
24
+ text.match(/deployed code at address:\s*(0x[a-fA-F0-9]{40})/i)?.[1] ??
25
+ text.match(/contract address:\s*(0x[a-fA-F0-9]{40})/i)?.[1] ??
26
+ text.match(/activated at address:\s*(0x[a-fA-F0-9]{40})/i)?.[1];
27
+
28
+ const deploymentTx =
29
+ text.match(/deployment tx hash:\s*(0x[a-fA-F0-9]{64})/i)?.[1] ??
30
+ text.match(/deployment transaction hash:\s*(0x[a-fA-F0-9]{64})/i)?.[1];
31
+
32
+ const activationTx =
33
+ text.match(/contract activated.*tx hash:\s*(0x[a-fA-F0-9]{64})/i)?.[1] ??
34
+ text.match(/wasm already activated.*(0x[a-fA-F0-9]{64})/i)?.[1];
35
+
36
+ // fallback: first 64-byte hash we can find
37
+ const anyHash = text.match(/0x[a-fA-F0-9]{64}/)?.[0];
38
+
39
+ return {
40
+ contractAddress: addr,
41
+ deploymentTxHash: deploymentTx ?? anyHash,
42
+ activationTxHash: activationTx,
43
+ };
44
+ }
45
+
46
+ export async function deployContract(
47
+ projectPath: string,
48
+ privateKey: string,
49
+ rpcUrl: string,
50
+ onOutput?: (output: CompilationOutput) => void
51
+ ): Promise<DeploymentResult> {
52
+ return new Promise((resolve) => {
53
+ const output: CompilationOutput[] = [];
54
+ const startTime = Date.now();
55
+
56
+ let stdoutBuf = "";
57
+ let stderrBuf = "";
58
+
59
+ const args = [
60
+ "stylus",
61
+ "deploy",
62
+ "--private-key",
63
+ privateKey,
64
+ "--endpoint",
65
+ rpcUrl,
66
+ "--no-verify",
67
+ "--max-fee-per-gas-gwei",
68
+ "0.5",
69
+ ];
70
+
71
+ const proc = spawn("cargo", args, {
72
+ cwd: projectPath,
73
+ shell: false,
74
+ env: {
75
+ ...process.env,
76
+ // ✅ critical: makes parsing stable
77
+ CARGO_TERM_COLOR: "never",
78
+ RUST_LOG: "info",
79
+ },
80
+ });
81
+
82
+ const timeout = setTimeout(() => {
83
+ proc.kill();
84
+ const msg: CompilationOutput = {
85
+ type: "error",
86
+ data: "Deployment timed out after 3 minutes",
87
+ timestamp: Date.now() - startTime,
88
+ };
89
+ output.push(msg);
90
+ onOutput?.(msg);
91
+ resolve({
92
+ success: false,
93
+ error: "Deployment timeout",
94
+ output,
95
+ rpcUsed: rpcUrl,
96
+ });
97
+ }, COMPILATION_CONSTANTS.PROCESS_TIMEOUT);
98
+
99
+ proc.stdout.on("data", (data) => {
100
+ const text = data.toString();
101
+ stdoutBuf += text;
102
+
103
+ const msg: CompilationOutput = {
104
+ type: "stdout",
105
+ data: text,
106
+ timestamp: Date.now() - startTime,
107
+ };
108
+ output.push(msg);
109
+ onOutput?.(msg);
110
+ });
111
+
112
+ proc.stderr.on("data", (data) => {
113
+ const text = data.toString();
114
+ stderrBuf += text;
115
+
116
+ const msg: CompilationOutput = {
117
+ type: "stderr",
118
+ data: text,
119
+ timestamp: Date.now() - startTime,
120
+ };
121
+ output.push(msg);
122
+ onOutput?.(msg);
123
+ });
124
+
125
+ proc.on("error", (error) => {
126
+ clearTimeout(timeout);
127
+ resolve({
128
+ success: false,
129
+ error: error.message,
130
+ output,
131
+ rpcUsed: rpcUrl,
132
+ });
133
+ });
134
+
135
+ proc.on("close", (code) => {
136
+ clearTimeout(timeout);
137
+
138
+ const combined = stdoutBuf + "\n" + stderrBuf;
139
+ const { contractAddress, deploymentTxHash, activationTxHash } =
140
+ extractDeployInfo(combined);
141
+
142
+ if (code === 0 && contractAddress) {
143
+ resolve({
144
+ success: true,
145
+ contractAddress,
146
+ deploymentTxHash,
147
+ activationTxHash,
148
+ output,
149
+ rpcUsed: rpcUrl,
150
+ });
151
+ return;
152
+ }
153
+
154
+ // ✅ handle "success but nonzero exit" (rare) if address exists
155
+ if (contractAddress) {
156
+ resolve({
157
+ success: true,
158
+ contractAddress,
159
+ deploymentTxHash,
160
+ activationTxHash,
161
+ output,
162
+ rpcUsed: rpcUrl,
163
+ error: `Non-zero exit code ${code}, but contract address was found.`,
164
+ });
165
+ return;
166
+ }
167
+
168
+ resolve({
169
+ success: false,
170
+ error: stripAnsi(stderrBuf || "Deployment failed"),
171
+ output,
172
+ rpcUsed: rpcUrl,
173
+ });
174
+ });
175
+ });
176
+ }
@@ -0,0 +1,83 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { COMPILATION_CONSTANTS } from "./constants";
4
+
5
+ export async function createProjectStructure(projectPath: string) {
6
+ const srcPath = path.join(projectPath, "src");
7
+ await fs.mkdir(srcPath, { recursive: true });
8
+ return { projectPath, srcPath };
9
+ }
10
+
11
+ export async function writeProjectFiles(
12
+ projectPath: string,
13
+ srcPath: string,
14
+ code: string,
15
+ cargoToml: string,
16
+ rustToolchain: string,
17
+ mainRs: string,
18
+ gitignore: string
19
+ ) {
20
+ await Promise.all([
21
+ fs.writeFile(path.join(projectPath, "Cargo.toml"), cargoToml),
22
+ fs.writeFile(path.join(projectPath, "rust-toolchain.toml"), rustToolchain),
23
+ fs.writeFile(path.join(srcPath, "lib.rs"), code),
24
+ fs.writeFile(path.join(srcPath, "main.rs"), mainRs),
25
+ fs.writeFile(path.join(projectPath, ".gitignore"), gitignore),
26
+ ]);
27
+ }
28
+
29
+ export async function cleanupProject(sessionId: string) {
30
+ const projectPath = path.join(
31
+ process.cwd(),
32
+ COMPILATION_CONSTANTS.TEMP_BASE,
33
+ sessionId
34
+ );
35
+ try {
36
+ await fs.rm(projectPath, { recursive: true, force: true });
37
+ } catch (error) {
38
+ console.error("Cleanup error:", error);
39
+ }
40
+ }
41
+
42
+ export async function cleanupOldProjects(maxAgeMinutes: number = 30) {
43
+ const tempBase = path.join(process.cwd(), COMPILATION_CONSTANTS.TEMP_BASE);
44
+
45
+ try {
46
+ const entries = await fs.readdir(tempBase, { withFileTypes: true });
47
+ const now = Date.now();
48
+ const maxAgeMs = maxAgeMinutes * 60 * 1000;
49
+
50
+ for (const entry of entries) {
51
+ if (entry.isDirectory()) {
52
+ const projectPath = path.join(tempBase, entry.name);
53
+ try {
54
+ const stats = await fs.stat(projectPath);
55
+ const age = now - stats.mtimeMs;
56
+
57
+ if (age > maxAgeMs) {
58
+ await fs.rm(projectPath, { recursive: true, force: true });
59
+ console.log(`Cleaned up old project: ${entry.name}`);
60
+ }
61
+ } catch {
62
+ // ignore per-project errors
63
+ }
64
+ }
65
+ }
66
+ } catch {
67
+ // ignore if temp directory doesn't exist
68
+ }
69
+ }
70
+
71
+ export function getProjectPath(sessionId: string): string {
72
+ return path.join(process.cwd(), COMPILATION_CONSTANTS.TEMP_BASE, sessionId);
73
+ }
74
+
75
+ export async function projectExists(sessionId: string): Promise<boolean> {
76
+ const projectPath = getProjectPath(sessionId);
77
+ try {
78
+ await fs.access(projectPath);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * GitHub API client for fetching repository contents
3
+ */
4
+
5
+ import {
6
+ GitHubTree,
7
+ GitHubContent,
8
+ GitHubRepo,
9
+ GitHubError,
10
+ } from "@/types/github";
11
+
12
+ const GITHUB_API_BASE = "https://api.github.com";
13
+ const GITHUB_RAW_BASE = "https://raw.githubusercontent.com";
14
+
15
+ export class GitHubAPIError extends Error {
16
+ constructor(
17
+ message: string,
18
+ public statusCode?: number,
19
+ public isRateLimit: boolean = false,
20
+ public isNotFound: boolean = false,
21
+ public isForbidden: boolean = false,
22
+ public resetAt?: Date
23
+ ) {
24
+ super(message);
25
+ this.name = "GitHubAPIError";
26
+ }
27
+ }
28
+
29
+ /**
30
+ * GitHub API client with rate limit handling
31
+ */
32
+ export class GitHubAPIClient {
33
+ private rateLimitRemaining: number | null = null;
34
+ private rateLimitReset: number | null = null;
35
+
36
+ /**
37
+ * Fetch repository information
38
+ */
39
+ async getRepo(owner: string, repo: string): Promise<GitHubRepo> {
40
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}`;
41
+ return this.fetchJSON<GitHubRepo>(url);
42
+ }
43
+
44
+ /**
45
+ * Fetch repository tree (all files recursively)
46
+ */
47
+ async getRepoTree(
48
+ owner: string,
49
+ repo: string,
50
+ branch: string = "main"
51
+ ): Promise<GitHubTree> {
52
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
53
+ return this.fetchJSON<GitHubTree>(url);
54
+ }
55
+
56
+ /**
57
+ * Fetch file content (returns base64 encoded content)
58
+ */
59
+ async getFileContent(
60
+ owner: string,
61
+ repo: string,
62
+ path: string,
63
+ branch?: string
64
+ ): Promise<GitHubContent> {
65
+ let url = `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${path}`;
66
+ if (branch) {
67
+ url += `?ref=${branch}`;
68
+ }
69
+ return this.fetchJSON<GitHubContent>(url);
70
+ }
71
+
72
+ /**
73
+ * Fetch raw file content (direct text)
74
+ */
75
+ async getRawFileContent(
76
+ owner: string,
77
+ repo: string,
78
+ path: string,
79
+ branch: string = "main"
80
+ ): Promise<string> {
81
+ const url = `${GITHUB_RAW_BASE}/${owner}/${repo}/${branch}/${path}`;
82
+
83
+ try {
84
+ const response = await fetch(url);
85
+
86
+ if (!response.ok) {
87
+ if (response.status === 404) {
88
+ throw new GitHubAPIError(`File not found: ${path}`, 404, false, true);
89
+ }
90
+ throw new GitHubAPIError(
91
+ `Failed to fetch file: ${response.statusText}`,
92
+ response.status
93
+ );
94
+ }
95
+
96
+ return response.text();
97
+ } catch (error) {
98
+ if (error instanceof GitHubAPIError) {
99
+ throw error;
100
+ }
101
+
102
+ // Network error
103
+ throw new GitHubAPIError(
104
+ "Network error. Please check your connection and try again.",
105
+ 0
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get default branch for repository
112
+ */
113
+ async getDefaultBranch(owner: string, repo: string): Promise<string> {
114
+ const repo_info = await this.getRepo(owner, repo);
115
+ return repo_info.default_branch;
116
+ }
117
+
118
+ /**
119
+ * Check if branch exists
120
+ */
121
+ async branchExists(
122
+ owner: string,
123
+ repo: string,
124
+ branch: string
125
+ ): Promise<boolean> {
126
+ try {
127
+ await this.getRepoTree(owner, repo, branch);
128
+ return true;
129
+ } catch (error) {
130
+ if (error instanceof GitHubAPIError && error.isNotFound) {
131
+ return false;
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Generic JSON fetch with error handling
139
+ */
140
+ private async fetchJSON<T>(url: string): Promise<T> {
141
+ try {
142
+ const response = await fetch(url, {
143
+ headers: {
144
+ Accept: "application/vnd.github.v3+json",
145
+ // Add GitHub token if available (increases rate limit)
146
+ ...(process.env.NEXT_PUBLIC_GITHUB_TOKEN && {
147
+ Authorization: `token ${process.env.NEXT_PUBLIC_GITHUB_TOKEN}`,
148
+ }),
149
+ },
150
+ });
151
+
152
+ // Update rate limit info
153
+ this.updateRateLimitInfo(response);
154
+
155
+ if (!response.ok) {
156
+ const error: GitHubError = await response.json().catch(() => ({
157
+ message: response.statusText,
158
+ }));
159
+
160
+ // Handle specific error cases
161
+ if (response.status === 404) {
162
+ throw new GitHubAPIError(
163
+ "Repository not found. Please check the URL and try again.",
164
+ 404,
165
+ false,
166
+ true
167
+ );
168
+ } else if (response.status === 403) {
169
+ // Check if it's rate limit or forbidden
170
+ if (this.rateLimitRemaining === 0) {
171
+ const resetDate = this.rateLimitReset
172
+ ? new Date(this.rateLimitReset)
173
+ : undefined;
174
+ throw new GitHubAPIError(
175
+ "GitHub API rate limit exceeded. Please try again later or add a GitHub token.",
176
+ 403,
177
+ true,
178
+ false,
179
+ false,
180
+ resetDate
181
+ );
182
+ } else {
183
+ throw new GitHubAPIError(
184
+ "Access forbidden. This repository may be private or require authentication.",
185
+ 403,
186
+ false,
187
+ false,
188
+ true
189
+ );
190
+ }
191
+ } else if (response.status === 401) {
192
+ throw new GitHubAPIError(
193
+ "Authentication failed. Please check your GitHub token.",
194
+ 401
195
+ );
196
+ } else if (response.status >= 500) {
197
+ throw new GitHubAPIError(
198
+ "GitHub is experiencing issues. Please try again later.",
199
+ response.status
200
+ );
201
+ } else {
202
+ throw new GitHubAPIError(
203
+ error.message || "Failed to fetch from GitHub",
204
+ response.status
205
+ );
206
+ }
207
+ }
208
+
209
+ return response.json();
210
+ } catch (error) {
211
+ if (error instanceof GitHubAPIError) {
212
+ throw error;
213
+ }
214
+
215
+ // Network error or JSON parse error
216
+ throw new GitHubAPIError(
217
+ "Network error. Please check your connection and try again.",
218
+ 0
219
+ );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Update rate limit tracking from response headers
225
+ */
226
+ private updateRateLimitInfo(response: Response): void {
227
+ const remaining = response.headers.get("X-RateLimit-Remaining");
228
+ const reset = response.headers.get("X-RateLimit-Reset");
229
+
230
+ if (remaining) this.rateLimitRemaining = parseInt(remaining, 10);
231
+ if (reset) this.rateLimitReset = parseInt(reset, 10) * 1000; // Convert to ms
232
+ }
233
+
234
+ /**
235
+ * Get current rate limit status
236
+ */
237
+ getRateLimitStatus(): { remaining: number | null; resetAt: Date | null } {
238
+ return {
239
+ remaining: this.rateLimitRemaining,
240
+ resetAt: this.rateLimitReset ? new Date(this.rateLimitReset) : null,
241
+ };
242
+ }
243
+ }
244
+
245
+ // Export singleton instance
246
+ export const githubAPI = new GitHubAPIClient();