@illuma-ai/code-sandbox 1.2.0 → 1.2.1
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/dist/hooks/useRuntime.d.ts +2 -2
- package/dist/index.cjs +79 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +6144 -6412
- package/dist/index.js.map +1 -1
- package/dist/services/runtime.d.ts +1 -1
- package/dist/types.d.ts +3 -32
- package/package.json +1 -1
- package/src/hooks/useRuntime.ts +3 -3
- package/src/index.ts +0 -5
- package/src/services/runtime.ts +1 -1
- package/src/types.ts +3 -40
- package/dist/services/git.d.ts +0 -57
- package/src/services/git.ts +0 -415
|
@@ -24,7 +24,7 @@ type ErrorCallback = (error: SandboxError) => void;
|
|
|
24
24
|
* - Write project files into the virtual filesystem
|
|
25
25
|
* - Install npm dependencies from package.json
|
|
26
26
|
* - Start the entry command (e.g., "node server.js")
|
|
27
|
-
* - Track file modifications for
|
|
27
|
+
* - Track file modifications for diffing
|
|
28
28
|
* - Provide preview URL for the iframe
|
|
29
29
|
*/
|
|
30
30
|
export declare class NodepodRuntime {
|
package/dist/types.d.ts
CHANGED
|
@@ -123,35 +123,6 @@ export interface RuntimeState {
|
|
|
123
123
|
*/
|
|
124
124
|
errors: SandboxError[];
|
|
125
125
|
}
|
|
126
|
-
export interface GitHubRepo {
|
|
127
|
-
owner: string;
|
|
128
|
-
repo: string;
|
|
129
|
-
branch?: string;
|
|
130
|
-
path?: string;
|
|
131
|
-
}
|
|
132
|
-
export interface GitCommitRequest {
|
|
133
|
-
owner: string;
|
|
134
|
-
repo: string;
|
|
135
|
-
/** Branch to create for the commit */
|
|
136
|
-
branch: string;
|
|
137
|
-
/** Base branch to fork from (default: 'main') */
|
|
138
|
-
baseBranch?: string;
|
|
139
|
-
/** Map of file paths to new contents (only changed files) */
|
|
140
|
-
changes: FileMap;
|
|
141
|
-
/** Commit message */
|
|
142
|
-
message: string;
|
|
143
|
-
}
|
|
144
|
-
export interface GitPRRequest extends GitCommitRequest {
|
|
145
|
-
/** PR title */
|
|
146
|
-
title: string;
|
|
147
|
-
/** PR body/description */
|
|
148
|
-
body?: string;
|
|
149
|
-
}
|
|
150
|
-
export interface GitPRResult {
|
|
151
|
-
number: number;
|
|
152
|
-
url: string;
|
|
153
|
-
branch: string;
|
|
154
|
-
}
|
|
155
126
|
/**
|
|
156
127
|
* Imperative handle exposed by CodeSandbox via React.forwardRef.
|
|
157
128
|
*
|
|
@@ -163,7 +134,7 @@ export interface GitPRResult {
|
|
|
163
134
|
* ```tsx
|
|
164
135
|
* const ref = useRef<CodeSandboxHandle>(null);
|
|
165
136
|
*
|
|
166
|
-
* // Push files from any source
|
|
137
|
+
* // Push files from any source
|
|
167
138
|
* ref.current?.updateFiles(fileMap);
|
|
168
139
|
*
|
|
169
140
|
* // Push a single file (e.g., agent modified one file)
|
|
@@ -230,7 +201,7 @@ export interface CodeSandboxHandle {
|
|
|
230
201
|
*
|
|
231
202
|
* The sandbox is a pure renderer — it receives files via props and
|
|
232
203
|
* exposes an imperative handle for programmatic updates. It does NOT
|
|
233
|
-
* interact with any storage backend
|
|
204
|
+
* interact with any storage backend directly.
|
|
234
205
|
*
|
|
235
206
|
* File sources (for initial load, pick ONE):
|
|
236
207
|
* - `files` — pass a FileMap directly
|
|
@@ -245,7 +216,7 @@ export interface CodeSandboxHandle {
|
|
|
245
216
|
* ```
|
|
246
217
|
*/
|
|
247
218
|
export interface CodeSandboxProps {
|
|
248
|
-
/** Pass files directly (from any source
|
|
219
|
+
/** Pass files directly (from any source) */
|
|
249
220
|
files?: FileMap;
|
|
250
221
|
/** OR use a built-in template name */
|
|
251
222
|
template?: string;
|
package/package.json
CHANGED
package/src/hooks/useRuntime.ts
CHANGED
|
@@ -64,8 +64,8 @@ function computeFileChanges(
|
|
|
64
64
|
/**
|
|
65
65
|
* Hook that manages the full Nodepod runtime lifecycle.
|
|
66
66
|
*
|
|
67
|
-
* The sandbox is a pure renderer —
|
|
68
|
-
* imperative handle. No
|
|
67
|
+
* The sandbox is a pure renderer — it receives files via props or the
|
|
68
|
+
* imperative handle. No polling, no storage backends.
|
|
69
69
|
*
|
|
70
70
|
* @param props - CodeSandbox component props
|
|
71
71
|
* @returns Reactive runtime state + control functions + imperative methods
|
|
@@ -301,7 +301,7 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
301
301
|
*
|
|
302
302
|
* Called by the imperative handle's updateFiles() method.
|
|
303
303
|
* Also usable as the initial boot path when files arrive asynchronously
|
|
304
|
-
* (e.g., host fetches from
|
|
304
|
+
* (e.g., host fetches from a remote source, then calls updateFiles).
|
|
305
305
|
*/
|
|
306
306
|
const updateFiles = useCallback(
|
|
307
307
|
async (newFiles: FileMap, options?: { restartServer?: boolean }) => {
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,6 @@ export type { WorkbenchView } from "./components/ViewSlider";
|
|
|
19
19
|
|
|
20
20
|
// Services
|
|
21
21
|
export { NodepodRuntime } from "./services/runtime";
|
|
22
|
-
export { GitService } from "./services/git";
|
|
23
22
|
|
|
24
23
|
// Hooks
|
|
25
24
|
export { useRuntime } from "./hooks/useRuntime";
|
|
@@ -38,10 +37,6 @@ export type {
|
|
|
38
37
|
BootProgress,
|
|
39
38
|
RuntimeConfig,
|
|
40
39
|
RuntimeState,
|
|
41
|
-
GitHubRepo,
|
|
42
|
-
GitCommitRequest,
|
|
43
|
-
GitPRRequest,
|
|
44
|
-
GitPRResult,
|
|
45
40
|
CodeSandboxProps,
|
|
46
41
|
CodeSandboxHandle,
|
|
47
42
|
FileTreeProps,
|
package/src/services/runtime.ts
CHANGED
|
@@ -38,7 +38,7 @@ const DBG = "[CodeSandbox:Runtime]";
|
|
|
38
38
|
* - Write project files into the virtual filesystem
|
|
39
39
|
* - Install npm dependencies from package.json
|
|
40
40
|
* - Start the entry command (e.g., "node server.js")
|
|
41
|
-
* - Track file modifications for
|
|
41
|
+
* - Track file modifications for diffing
|
|
42
42
|
* - Provide preview URL for the iframe
|
|
43
43
|
*/
|
|
44
44
|
export class NodepodRuntime {
|
package/src/types.ts
CHANGED
|
@@ -160,43 +160,6 @@ export interface RuntimeState {
|
|
|
160
160
|
errors: SandboxError[];
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Git (GitHub API integration — utility, NOT used internally by sandbox)
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
export interface GitHubRepo {
|
|
168
|
-
owner: string;
|
|
169
|
-
repo: string;
|
|
170
|
-
branch?: string; // default: 'main'
|
|
171
|
-
path?: string; // subdirectory to clone (default: root)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export interface GitCommitRequest {
|
|
175
|
-
owner: string;
|
|
176
|
-
repo: string;
|
|
177
|
-
/** Branch to create for the commit */
|
|
178
|
-
branch: string;
|
|
179
|
-
/** Base branch to fork from (default: 'main') */
|
|
180
|
-
baseBranch?: string;
|
|
181
|
-
/** Map of file paths to new contents (only changed files) */
|
|
182
|
-
changes: FileMap;
|
|
183
|
-
/** Commit message */
|
|
184
|
-
message: string;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface GitPRRequest extends GitCommitRequest {
|
|
188
|
-
/** PR title */
|
|
189
|
-
title: string;
|
|
190
|
-
/** PR body/description */
|
|
191
|
-
body?: string;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface GitPRResult {
|
|
195
|
-
number: number;
|
|
196
|
-
url: string;
|
|
197
|
-
branch: string;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
163
|
// ---------------------------------------------------------------------------
|
|
201
164
|
// Imperative Handle
|
|
202
165
|
// ---------------------------------------------------------------------------
|
|
@@ -212,7 +175,7 @@ export interface GitPRResult {
|
|
|
212
175
|
* ```tsx
|
|
213
176
|
* const ref = useRef<CodeSandboxHandle>(null);
|
|
214
177
|
*
|
|
215
|
-
* // Push files from any source
|
|
178
|
+
* // Push files from any source
|
|
216
179
|
* ref.current?.updateFiles(fileMap);
|
|
217
180
|
*
|
|
218
181
|
* // Push a single file (e.g., agent modified one file)
|
|
@@ -292,7 +255,7 @@ export interface CodeSandboxHandle {
|
|
|
292
255
|
*
|
|
293
256
|
* The sandbox is a pure renderer — it receives files via props and
|
|
294
257
|
* exposes an imperative handle for programmatic updates. It does NOT
|
|
295
|
-
* interact with any storage backend
|
|
258
|
+
* interact with any storage backend directly.
|
|
296
259
|
*
|
|
297
260
|
* File sources (for initial load, pick ONE):
|
|
298
261
|
* - `files` — pass a FileMap directly
|
|
@@ -307,7 +270,7 @@ export interface CodeSandboxHandle {
|
|
|
307
270
|
* ```
|
|
308
271
|
*/
|
|
309
272
|
export interface CodeSandboxProps {
|
|
310
|
-
/** Pass files directly (from any source
|
|
273
|
+
/** Pass files directly (from any source) */
|
|
311
274
|
files?: FileMap;
|
|
312
275
|
/** OR use a built-in template name */
|
|
313
276
|
template?: string;
|
package/dist/services/git.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitService — GitHub API integration for loading and saving project files.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* - Clone: GET repo tree → GET file contents → return FileMap
|
|
6
|
-
* - Commit: POST branch → PUT files → POST PR
|
|
7
|
-
*
|
|
8
|
-
* All operations use the GitHub REST API (no git CLI needed).
|
|
9
|
-
* Works entirely in the browser — no server required.
|
|
10
|
-
*/
|
|
11
|
-
import type { FileMap, GitCommitRequest, GitHubRepo, GitPRRequest, GitPRResult } from "../types";
|
|
12
|
-
/**
|
|
13
|
-
* GitHub API service for cloning repos and creating PRs.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* const git = new GitService('ghp_xxxxx');
|
|
18
|
-
* const files = await git.cloneRepo({ owner: 'user', repo: 'my-app' });
|
|
19
|
-
* // ... user edits files ...
|
|
20
|
-
* const pr = await git.createPR({
|
|
21
|
-
* owner: 'user', repo: 'my-app',
|
|
22
|
-
* branch: 'sandbox/changes', changes: editedFiles,
|
|
23
|
-
* message: 'Update from sandbox', title: 'Sandbox changes',
|
|
24
|
-
* });
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export declare class GitService {
|
|
28
|
-
private token;
|
|
29
|
-
constructor(token: string);
|
|
30
|
-
/**
|
|
31
|
-
* Clone a GitHub repository into a flat file map.
|
|
32
|
-
*
|
|
33
|
-
* Uses the Git Trees API for a single request to get the full file listing,
|
|
34
|
-
* then fetches text file contents in parallel batches.
|
|
35
|
-
*
|
|
36
|
-
* @param repo - GitHub repository coordinates
|
|
37
|
-
* @param onProgress - Optional progress callback (0-100)
|
|
38
|
-
* @returns FileMap of path → content
|
|
39
|
-
*/
|
|
40
|
-
cloneRepo(repo: GitHubRepo, onProgress?: (percent: number, message: string) => void): Promise<FileMap>;
|
|
41
|
-
/**
|
|
42
|
-
* Create a new branch with file changes and open a pull request.
|
|
43
|
-
*
|
|
44
|
-
* Steps:
|
|
45
|
-
* 1. Get the base branch's HEAD SHA
|
|
46
|
-
* 2. Create a new branch from that SHA
|
|
47
|
-
* 3. For each changed file, update its contents on the new branch
|
|
48
|
-
* 4. Create a pull request
|
|
49
|
-
*/
|
|
50
|
-
createPR(request: GitPRRequest): Promise<GitPRResult>;
|
|
51
|
-
/**
|
|
52
|
-
* Commit changes without creating a PR.
|
|
53
|
-
*/
|
|
54
|
-
commit(request: GitCommitRequest): Promise<string>;
|
|
55
|
-
/** Fetch with auth headers */
|
|
56
|
-
private fetch;
|
|
57
|
-
}
|
package/src/services/git.ts
DELETED
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitService — GitHub API integration for loading and saving project files.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* - Clone: GET repo tree → GET file contents → return FileMap
|
|
6
|
-
* - Commit: POST branch → PUT files → POST PR
|
|
7
|
-
*
|
|
8
|
-
* All operations use the GitHub REST API (no git CLI needed).
|
|
9
|
-
* Works entirely in the browser — no server required.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
FileMap,
|
|
14
|
-
GitCommitRequest,
|
|
15
|
-
GitHubRepo,
|
|
16
|
-
GitPRRequest,
|
|
17
|
-
GitPRResult,
|
|
18
|
-
} from "../types";
|
|
19
|
-
|
|
20
|
-
const GITHUB_API = "https://api.github.com";
|
|
21
|
-
|
|
22
|
-
/** File extensions to treat as text (others are skipped) */
|
|
23
|
-
const TEXT_EXTENSIONS = new Set([
|
|
24
|
-
".js",
|
|
25
|
-
".jsx",
|
|
26
|
-
".ts",
|
|
27
|
-
".tsx",
|
|
28
|
-
".json",
|
|
29
|
-
".html",
|
|
30
|
-
".htm",
|
|
31
|
-
".css",
|
|
32
|
-
".scss",
|
|
33
|
-
".sass",
|
|
34
|
-
".less",
|
|
35
|
-
".md",
|
|
36
|
-
".mdx",
|
|
37
|
-
".txt",
|
|
38
|
-
".yml",
|
|
39
|
-
".yaml",
|
|
40
|
-
".toml",
|
|
41
|
-
".xml",
|
|
42
|
-
".svg",
|
|
43
|
-
".env",
|
|
44
|
-
".gitignore",
|
|
45
|
-
".eslintrc",
|
|
46
|
-
".prettierrc",
|
|
47
|
-
".sh",
|
|
48
|
-
".bash",
|
|
49
|
-
".zsh",
|
|
50
|
-
".py",
|
|
51
|
-
".rb",
|
|
52
|
-
".go",
|
|
53
|
-
".rs",
|
|
54
|
-
".java",
|
|
55
|
-
".kt",
|
|
56
|
-
".c",
|
|
57
|
-
".cpp",
|
|
58
|
-
".h",
|
|
59
|
-
".hpp",
|
|
60
|
-
".cs",
|
|
61
|
-
".php",
|
|
62
|
-
".sql",
|
|
63
|
-
".graphql",
|
|
64
|
-
".lock",
|
|
65
|
-
".mjs",
|
|
66
|
-
".cjs",
|
|
67
|
-
".mts",
|
|
68
|
-
".cts",
|
|
69
|
-
".vue",
|
|
70
|
-
".svelte",
|
|
71
|
-
".astro",
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
/** Directories to skip when cloning */
|
|
75
|
-
const SKIP_DIRS = new Set([
|
|
76
|
-
"node_modules",
|
|
77
|
-
".git",
|
|
78
|
-
"dist",
|
|
79
|
-
"build",
|
|
80
|
-
".next",
|
|
81
|
-
".cache",
|
|
82
|
-
"__pycache__",
|
|
83
|
-
".venv",
|
|
84
|
-
"venv",
|
|
85
|
-
"coverage",
|
|
86
|
-
".nyc_output",
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
/** Max file size to fetch (skip large files) */
|
|
90
|
-
const MAX_FILE_SIZE = 500_000; // 500KB
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* GitHub API service for cloning repos and creating PRs.
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```ts
|
|
97
|
-
* const git = new GitService('ghp_xxxxx');
|
|
98
|
-
* const files = await git.cloneRepo({ owner: 'user', repo: 'my-app' });
|
|
99
|
-
* // ... user edits files ...
|
|
100
|
-
* const pr = await git.createPR({
|
|
101
|
-
* owner: 'user', repo: 'my-app',
|
|
102
|
-
* branch: 'sandbox/changes', changes: editedFiles,
|
|
103
|
-
* message: 'Update from sandbox', title: 'Sandbox changes',
|
|
104
|
-
* });
|
|
105
|
-
* ```
|
|
106
|
-
*/
|
|
107
|
-
export class GitService {
|
|
108
|
-
private token: string;
|
|
109
|
-
|
|
110
|
-
constructor(token: string) {
|
|
111
|
-
this.token = token;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// -------------------------------------------------------------------------
|
|
115
|
-
// Clone: fetch repo files as a FileMap
|
|
116
|
-
// -------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Clone a GitHub repository into a flat file map.
|
|
120
|
-
*
|
|
121
|
-
* Uses the Git Trees API for a single request to get the full file listing,
|
|
122
|
-
* then fetches text file contents in parallel batches.
|
|
123
|
-
*
|
|
124
|
-
* @param repo - GitHub repository coordinates
|
|
125
|
-
* @param onProgress - Optional progress callback (0-100)
|
|
126
|
-
* @returns FileMap of path → content
|
|
127
|
-
*/
|
|
128
|
-
async cloneRepo(
|
|
129
|
-
repo: GitHubRepo,
|
|
130
|
-
onProgress?: (percent: number, message: string) => void,
|
|
131
|
-
): Promise<FileMap> {
|
|
132
|
-
const { owner, repo: repoName, branch = "main", path: subPath } = repo;
|
|
133
|
-
|
|
134
|
-
onProgress?.(5, "Fetching repository structure...");
|
|
135
|
-
|
|
136
|
-
// 1. Get the tree (recursive) for the branch
|
|
137
|
-
const treeUrl = `${GITHUB_API}/repos/${owner}/${repoName}/git/trees/${branch}?recursive=1`;
|
|
138
|
-
const treeRes = await this.fetch(treeUrl);
|
|
139
|
-
const treeData = await treeRes.json();
|
|
140
|
-
|
|
141
|
-
if (!treeData.tree) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Failed to fetch repo tree: ${treeData.message || "Unknown error"}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 2. Filter to text files, skip large/binary/excluded dirs
|
|
148
|
-
const filesToFetch: Array<{ path: string; sha: string; size: number }> = [];
|
|
149
|
-
|
|
150
|
-
for (const item of treeData.tree) {
|
|
151
|
-
if (item.type !== "blob") {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Apply subPath filter if specified
|
|
156
|
-
if (subPath && !item.path.startsWith(subPath)) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Skip excluded directories
|
|
161
|
-
const parts = item.path.split("/");
|
|
162
|
-
if (parts.some((p: string) => SKIP_DIRS.has(p))) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Skip files that are too large
|
|
167
|
-
if (item.size > MAX_FILE_SIZE) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Only include text files
|
|
172
|
-
const ext = "." + item.path.split(".").pop()?.toLowerCase();
|
|
173
|
-
const basename = parts[parts.length - 1];
|
|
174
|
-
// Include files with known extensions, dotfiles, or extensionless config files
|
|
175
|
-
if (
|
|
176
|
-
TEXT_EXTENSIONS.has(ext) ||
|
|
177
|
-
basename.startsWith(".") ||
|
|
178
|
-
!basename.includes(".")
|
|
179
|
-
) {
|
|
180
|
-
filesToFetch.push({ path: item.path, sha: item.sha, size: item.size });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
onProgress?.(15, `Found ${filesToFetch.length} files to fetch...`);
|
|
185
|
-
|
|
186
|
-
// 3. Fetch file contents in parallel batches
|
|
187
|
-
const files: FileMap = {};
|
|
188
|
-
const batchSize = 20;
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < filesToFetch.length; i += batchSize) {
|
|
191
|
-
const batch = filesToFetch.slice(i, i + batchSize);
|
|
192
|
-
|
|
193
|
-
const results = await Promise.all(
|
|
194
|
-
batch.map(async (file) => {
|
|
195
|
-
try {
|
|
196
|
-
const contentUrl = `${GITHUB_API}/repos/${owner}/${repoName}/contents/${file.path}?ref=${branch}`;
|
|
197
|
-
const res = await this.fetch(contentUrl);
|
|
198
|
-
const data = await res.json();
|
|
199
|
-
|
|
200
|
-
if (data.encoding === "base64" && data.content) {
|
|
201
|
-
const content = atob(data.content.replace(/\n/g, ""));
|
|
202
|
-
// Strip subPath prefix if specified
|
|
203
|
-
const relativePath = subPath
|
|
204
|
-
? file.path.replace(new RegExp(`^${subPath}/?`), "")
|
|
205
|
-
: file.path;
|
|
206
|
-
return { path: relativePath, content };
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
for (const result of results) {
|
|
216
|
-
if (result) {
|
|
217
|
-
files[result.path] = result.content;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const percent =
|
|
222
|
-
15 + Math.round(((i + batch.length) / filesToFetch.length) * 80);
|
|
223
|
-
onProgress?.(
|
|
224
|
-
percent,
|
|
225
|
-
`Fetching files... (${Math.min(i + batchSize, filesToFetch.length)}/${filesToFetch.length})`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
onProgress?.(100, `Loaded ${Object.keys(files).length} files`);
|
|
230
|
-
|
|
231
|
-
return files;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// -------------------------------------------------------------------------
|
|
235
|
-
// Commit + PR: write changes back to GitHub
|
|
236
|
-
// -------------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Create a new branch with file changes and open a pull request.
|
|
240
|
-
*
|
|
241
|
-
* Steps:
|
|
242
|
-
* 1. Get the base branch's HEAD SHA
|
|
243
|
-
* 2. Create a new branch from that SHA
|
|
244
|
-
* 3. For each changed file, update its contents on the new branch
|
|
245
|
-
* 4. Create a pull request
|
|
246
|
-
*/
|
|
247
|
-
async createPR(request: GitPRRequest): Promise<GitPRResult> {
|
|
248
|
-
const {
|
|
249
|
-
owner,
|
|
250
|
-
repo,
|
|
251
|
-
branch,
|
|
252
|
-
baseBranch = "main",
|
|
253
|
-
changes,
|
|
254
|
-
message,
|
|
255
|
-
title,
|
|
256
|
-
body,
|
|
257
|
-
} = request;
|
|
258
|
-
|
|
259
|
-
// 1. Get base branch HEAD SHA
|
|
260
|
-
const refRes = await this.fetch(
|
|
261
|
-
`${GITHUB_API}/repos/${owner}/${repo}/git/ref/heads/${baseBranch}`,
|
|
262
|
-
);
|
|
263
|
-
const refData = await refRes.json();
|
|
264
|
-
const baseSha = refData.object.sha;
|
|
265
|
-
|
|
266
|
-
// 2. Create new branch
|
|
267
|
-
await this.fetch(`${GITHUB_API}/repos/${owner}/${repo}/git/refs`, {
|
|
268
|
-
method: "POST",
|
|
269
|
-
body: JSON.stringify({
|
|
270
|
-
ref: `refs/heads/${branch}`,
|
|
271
|
-
sha: baseSha,
|
|
272
|
-
}),
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// 3. Commit each changed file
|
|
276
|
-
for (const [path, content] of Object.entries(changes)) {
|
|
277
|
-
// Get current file SHA (if it exists)
|
|
278
|
-
let fileSha: string | undefined;
|
|
279
|
-
try {
|
|
280
|
-
const fileRes = await this.fetch(
|
|
281
|
-
`${GITHUB_API}/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
282
|
-
);
|
|
283
|
-
const fileData = await fileRes.json();
|
|
284
|
-
fileSha = fileData.sha;
|
|
285
|
-
} catch {
|
|
286
|
-
// File doesn't exist yet — that's fine, we'll create it
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
await this.fetch(
|
|
290
|
-
`${GITHUB_API}/repos/${owner}/${repo}/contents/${path}`,
|
|
291
|
-
{
|
|
292
|
-
method: "PUT",
|
|
293
|
-
body: JSON.stringify({
|
|
294
|
-
message,
|
|
295
|
-
content: btoa(content),
|
|
296
|
-
branch,
|
|
297
|
-
...(fileSha ? { sha: fileSha } : {}),
|
|
298
|
-
}),
|
|
299
|
-
},
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// 4. Create pull request
|
|
304
|
-
const prRes = await this.fetch(
|
|
305
|
-
`${GITHUB_API}/repos/${owner}/${repo}/pulls`,
|
|
306
|
-
{
|
|
307
|
-
method: "POST",
|
|
308
|
-
body: JSON.stringify({
|
|
309
|
-
title,
|
|
310
|
-
body:
|
|
311
|
-
body ||
|
|
312
|
-
`Changes from code sandbox.\n\nModified files:\n${Object.keys(
|
|
313
|
-
changes,
|
|
314
|
-
)
|
|
315
|
-
.map((p) => `- ${p}`)
|
|
316
|
-
.join("\n")}`,
|
|
317
|
-
head: branch,
|
|
318
|
-
base: baseBranch,
|
|
319
|
-
}),
|
|
320
|
-
},
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
const prData = await prRes.json();
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
number: prData.number,
|
|
327
|
-
url: prData.html_url,
|
|
328
|
-
branch,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Commit changes without creating a PR.
|
|
334
|
-
*/
|
|
335
|
-
async commit(request: GitCommitRequest): Promise<string> {
|
|
336
|
-
const {
|
|
337
|
-
owner,
|
|
338
|
-
repo,
|
|
339
|
-
branch,
|
|
340
|
-
baseBranch = "main",
|
|
341
|
-
changes,
|
|
342
|
-
message,
|
|
343
|
-
} = request;
|
|
344
|
-
|
|
345
|
-
// Get base SHA
|
|
346
|
-
const refRes = await this.fetch(
|
|
347
|
-
`${GITHUB_API}/repos/${owner}/${repo}/git/ref/heads/${baseBranch}`,
|
|
348
|
-
);
|
|
349
|
-
const refData = await refRes.json();
|
|
350
|
-
const baseSha = refData.object.sha;
|
|
351
|
-
|
|
352
|
-
// Create branch (may already exist)
|
|
353
|
-
try {
|
|
354
|
-
await this.fetch(`${GITHUB_API}/repos/${owner}/${repo}/git/refs`, {
|
|
355
|
-
method: "POST",
|
|
356
|
-
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: baseSha }),
|
|
357
|
-
});
|
|
358
|
-
} catch {
|
|
359
|
-
// Branch already exists — continue
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Commit files
|
|
363
|
-
for (const [path, content] of Object.entries(changes)) {
|
|
364
|
-
let fileSha: string | undefined;
|
|
365
|
-
try {
|
|
366
|
-
const fileRes = await this.fetch(
|
|
367
|
-
`${GITHUB_API}/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
368
|
-
);
|
|
369
|
-
const fileData = await fileRes.json();
|
|
370
|
-
fileSha = fileData.sha;
|
|
371
|
-
} catch {
|
|
372
|
-
// New file
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
await this.fetch(
|
|
376
|
-
`${GITHUB_API}/repos/${owner}/${repo}/contents/${path}`,
|
|
377
|
-
{
|
|
378
|
-
method: "PUT",
|
|
379
|
-
body: JSON.stringify({
|
|
380
|
-
message,
|
|
381
|
-
content: btoa(content),
|
|
382
|
-
branch,
|
|
383
|
-
...(fileSha ? { sha: fileSha } : {}),
|
|
384
|
-
}),
|
|
385
|
-
},
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return branch;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// -------------------------------------------------------------------------
|
|
393
|
-
// Private
|
|
394
|
-
// -------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
/** Fetch with auth headers */
|
|
397
|
-
private async fetch(url: string, init?: RequestInit): Promise<Response> {
|
|
398
|
-
const res = await fetch(url, {
|
|
399
|
-
...init,
|
|
400
|
-
headers: {
|
|
401
|
-
Authorization: `Bearer ${this.token}`,
|
|
402
|
-
Accept: "application/vnd.github.v3+json",
|
|
403
|
-
"Content-Type": "application/json",
|
|
404
|
-
...(init?.headers || {}),
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
if (!res.ok) {
|
|
409
|
-
const body = await res.text();
|
|
410
|
-
throw new Error(`GitHub API error ${res.status}: ${body}`);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return res;
|
|
414
|
-
}
|
|
415
|
-
}
|