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.
- package/Readme.MD +1515 -0
- package/cli.js +28 -0
- package/frontend/.vscode/settings.json +9 -0
- package/frontend/app/api/chat/route.ts +101 -0
- package/frontend/app/api/check-setup/route.ts +93 -0
- package/frontend/app/api/cleanup/route.ts +14 -0
- package/frontend/app/api/compile/route.ts +95 -0
- package/frontend/app/api/compile-stream/route.ts +98 -0
- package/frontend/app/api/complete/route.ts +86 -0
- package/frontend/app/api/deploy/route.ts +118 -0
- package/frontend/app/api/export-abi/route.ts +58 -0
- package/frontend/app/favicon.ico +0 -0
- package/frontend/app/globals.css +177 -0
- package/frontend/app/layout.tsx +29 -0
- package/frontend/app/ml/page.tsx +694 -0
- package/frontend/app/page.tsx +1132 -0
- package/frontend/app/providers.tsx +18 -0
- package/frontend/app/qlearning/page.tsx +188 -0
- package/frontend/app/raytracing/page.tsx +268 -0
- package/frontend/components/abi/ABIDialog.tsx +132 -0
- package/frontend/components/ai/AICompletionPopup.tsx +76 -0
- package/frontend/components/ai/ChatPanel.tsx +292 -0
- package/frontend/components/ai/QuickActions.tsx +128 -0
- package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
- package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
- package/frontend/components/deploy/DeployDialog.tsx +334 -0
- package/frontend/components/editor/FileTabs.tsx +181 -0
- package/frontend/components/editor/MonacoEditor.tsx +306 -0
- package/frontend/components/file-tree/ContextMenu.tsx +110 -0
- package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
- package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
- package/frontend/components/file-tree/FileNode.tsx +60 -0
- package/frontend/components/file-tree/FileTree.tsx +259 -0
- package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
- package/frontend/components/file-tree/FolderNode.tsx +105 -0
- package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
- package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
- package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
- package/frontend/components/github/URLCopyButton.tsx +60 -0
- package/frontend/components/interact/ContractInteraction.tsx +323 -0
- package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
- package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
- package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
- package/frontend/components/project/ProjectActions.tsx +176 -0
- package/frontend/components/q-learning/ContractConfig.tsx +172 -0
- package/frontend/components/q-learning/MazeGrid.tsx +346 -0
- package/frontend/components/q-learning/PathAnimation.tsx +384 -0
- package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
- package/frontend/components/q-learning/TrainingForm.tsx +349 -0
- package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
- package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
- package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
- package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
- package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
- package/frontend/components/setup/SetupGuide.tsx +290 -0
- package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
- package/frontend/components/ui/alert-dialog.tsx +157 -0
- package/frontend/components/ui/alert.tsx +66 -0
- package/frontend/components/ui/badge.tsx +46 -0
- package/frontend/components/ui/button.tsx +62 -0
- package/frontend/components/ui/card.tsx +92 -0
- package/frontend/components/ui/context-menu.tsx +252 -0
- package/frontend/components/ui/dialog.tsx +143 -0
- package/frontend/components/ui/dropdown-menu.tsx +257 -0
- package/frontend/components/ui/input.tsx +21 -0
- package/frontend/components/ui/label.tsx +24 -0
- package/frontend/components/ui/progress.tsx +31 -0
- package/frontend/components/ui/scroll-area.tsx +58 -0
- package/frontend/components/ui/select.tsx +190 -0
- package/frontend/components/ui/separator.tsx +28 -0
- package/frontend/components/ui/sheet.tsx +139 -0
- package/frontend/components/ui/skeleton.tsx +13 -0
- package/frontend/components/ui/slider.tsx +63 -0
- package/frontend/components/ui/sonner.tsx +40 -0
- package/frontend/components/ui/tabs.tsx +66 -0
- package/frontend/components/ui/textarea.tsx +18 -0
- package/frontend/components/wallet/ConnectButton.tsx +167 -0
- package/frontend/components/wallet/FaucetButton.tsx +256 -0
- package/frontend/components.json +22 -0
- package/frontend/eslint.config.mjs +18 -0
- package/frontend/hooks/useAICompletion.ts +75 -0
- package/frontend/hooks/useBlockchainLoader.ts +58 -0
- package/frontend/hooks/useChats.ts +137 -0
- package/frontend/hooks/useCompilation.ts +173 -0
- package/frontend/hooks/useFileTabs.ts +178 -0
- package/frontend/hooks/useGitHubLoader.ts +50 -0
- package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
- package/frontend/hooks/usePanelState.ts +115 -0
- package/frontend/hooks/useProjectState.ts +276 -0
- package/frontend/hooks/useResponsive.ts +29 -0
- package/frontend/lib/abi-parser.ts +58 -0
- package/frontend/lib/blockchain-api.ts +374 -0
- package/frontend/lib/blockchain-explorers.ts +75 -0
- package/frontend/lib/blockchain-loader.ts +112 -0
- package/frontend/lib/cargo-template.ts +64 -0
- package/frontend/lib/compilation.ts +529 -0
- package/frontend/lib/constants.ts +31 -0
- package/frontend/lib/deployment.ts +176 -0
- package/frontend/lib/file-utils.ts +83 -0
- package/frontend/lib/github-api.ts +246 -0
- package/frontend/lib/github-loader.ts +369 -0
- package/frontend/lib/ml-contract-template.txt +900 -0
- package/frontend/lib/orbit-chains.ts +181 -0
- package/frontend/lib/output-formatter.ts +68 -0
- package/frontend/lib/project-manager.ts +632 -0
- package/frontend/lib/ray-tracing-abi.ts +206 -0
- package/frontend/lib/storage.ts +189 -0
- package/frontend/lib/templates.ts +1662 -0
- package/frontend/lib/url-parser.ts +188 -0
- package/frontend/lib/utils.ts +6 -0
- package/frontend/lib/wagmi-config.ts +24 -0
- package/frontend/next.config.ts +7 -0
- package/frontend/package-lock.json +16259 -0
- package/frontend/package.json +60 -0
- package/frontend/postcss.config.mjs +7 -0
- package/frontend/public/file.svg +1 -0
- package/frontend/public/globe.svg +1 -0
- package/frontend/public/ml-weights/.gitkeep +0 -0
- package/frontend/public/ml-weights/model.pkl +0 -0
- package/frontend/public/ml-weights/model_weights.json +27102 -0
- package/frontend/public/ml-weights/test_samples.json +7888 -0
- package/frontend/public/next.svg +1 -0
- package/frontend/public/vercel.svg +1 -0
- package/frontend/public/window.svg +1 -0
- package/frontend/scripts/check-env.js +52 -0
- package/frontend/scripts/setup.js +285 -0
- package/frontend/tailwind.config.ts +64 -0
- package/frontend/tsconfig.json +34 -0
- package/frontend/types/blockchain.ts +63 -0
- package/frontend/types/github.ts +54 -0
- package/frontend/types/project.ts +106 -0
- package/ml-training/README.md +56 -0
- package/ml-training/train_tiny_model.py +325 -0
- package/ml-training/update_template.py +59 -0
- 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();
|