@illuma-ai/code-sandbox 1.1.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/components/Workbench.d.ts +3 -2
- package/dist/hooks/useRuntime.d.ts +24 -14
- package/dist/index.cjs +81 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +6121 -6364
- package/dist/index.js.map +1 -1
- package/dist/services/runtime.d.ts +1 -1
- package/dist/types.d.ts +104 -41
- package/package.json +1 -1
- package/src/components/Workbench.tsx +133 -87
- package/src/hooks/useRuntime.ts +182 -216
- package/src/index.ts +1 -5
- package/src/services/runtime.ts +1 -1
- package/src/types.ts +110 -42
- package/dist/services/git.d.ts +0 -57
- package/src/services/git.ts +0 -415
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @illuma-ai/code-sandbox — Type definitions
|
|
3
3
|
*
|
|
4
4
|
* Core types for the browser-native code sandbox.
|
|
5
|
-
* Covers file maps, runtime states,
|
|
5
|
+
* Covers file maps, runtime states, imperative handle, and component props.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -138,11 +138,9 @@ export interface RuntimeState {
|
|
|
138
138
|
files: FileMap;
|
|
139
139
|
/**
|
|
140
140
|
* Baseline file snapshot — the files as they were before the latest
|
|
141
|
-
* update (
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* (everything is "new"), populated after the first file update
|
|
145
|
-
* (GitHub poll, updateFiles, etc.).
|
|
141
|
+
* update (via updateFiles). Used by the diff viewer to show what changed.
|
|
142
|
+
* Empty on first load (everything is "new"), populated after the first
|
|
143
|
+
* file update.
|
|
146
144
|
*/
|
|
147
145
|
originalFiles: FileMap;
|
|
148
146
|
/**
|
|
@@ -163,53 +161,117 @@ export interface RuntimeState {
|
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
// ---------------------------------------------------------------------------
|
|
166
|
-
//
|
|
164
|
+
// Imperative Handle
|
|
167
165
|
// ---------------------------------------------------------------------------
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Imperative handle exposed by CodeSandbox via React.forwardRef.
|
|
169
|
+
*
|
|
170
|
+
* This is the primary API for host applications (like Ranger) to
|
|
171
|
+
* control the sandbox programmatically. The sandbox is a "dumb renderer"
|
|
172
|
+
* — the host pushes files in and reads state out.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```tsx
|
|
176
|
+
* const ref = useRef<CodeSandboxHandle>(null);
|
|
177
|
+
*
|
|
178
|
+
* // Push files from any source
|
|
179
|
+
* ref.current?.updateFiles(fileMap);
|
|
180
|
+
*
|
|
181
|
+
* // Push a single file (e.g., agent modified one file)
|
|
182
|
+
* ref.current?.updateFile('server.js', newContent);
|
|
183
|
+
*
|
|
184
|
+
* // Read current state
|
|
185
|
+
* const files = ref.current?.getFiles();
|
|
186
|
+
* const changes = ref.current?.getChangedFiles();
|
|
187
|
+
* const errors = ref.current?.getErrors();
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export interface CodeSandboxHandle {
|
|
191
|
+
/**
|
|
192
|
+
* Replace the entire file set. Diffs against current files, writes only
|
|
193
|
+
* changed files to Nodepod FS, and restarts the server if anything changed.
|
|
194
|
+
*
|
|
195
|
+
* The previous file set becomes `originalFiles` for diff tracking.
|
|
196
|
+
*
|
|
197
|
+
* @param files - Complete new file set
|
|
198
|
+
* @param options - Optional: restartServer (default true)
|
|
199
|
+
*/
|
|
200
|
+
updateFiles: (
|
|
201
|
+
files: FileMap,
|
|
202
|
+
options?: { restartServer?: boolean },
|
|
203
|
+
) => Promise<void>;
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/** Commit message */
|
|
186
|
-
message: string;
|
|
187
|
-
}
|
|
205
|
+
/**
|
|
206
|
+
* Update a single file. Writes to Nodepod FS and updates state.
|
|
207
|
+
* Does NOT restart the server — call `restart()` manually if needed,
|
|
208
|
+
* or use `updateFiles()` for bulk updates with auto-restart.
|
|
209
|
+
*
|
|
210
|
+
* @param path - File path (e.g., "server.js", "public/index.html")
|
|
211
|
+
* @param content - New file content
|
|
212
|
+
*/
|
|
213
|
+
updateFile: (path: string, content: string) => Promise<void>;
|
|
188
214
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
215
|
+
/**
|
|
216
|
+
* Force restart the server process. Kills the current process
|
|
217
|
+
* and re-runs the entry command.
|
|
218
|
+
*/
|
|
219
|
+
restart: () => Promise<void>;
|
|
195
220
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Get the current file map (all files including edits).
|
|
223
|
+
*/
|
|
224
|
+
getFiles: () => FileMap;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get only files that have been modified relative to originalFiles.
|
|
228
|
+
* Returns a FileMap containing only the changed files.
|
|
229
|
+
*/
|
|
230
|
+
getChangedFiles: () => FileMap;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the per-file change status map (new/modified/deleted).
|
|
234
|
+
* Missing keys should be treated as "unchanged".
|
|
235
|
+
*/
|
|
236
|
+
getFileChanges: () => Record<string, FileChangeStatus>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get all structured errors collected during this session.
|
|
240
|
+
*/
|
|
241
|
+
getErrors: () => SandboxError[];
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the current runtime state snapshot.
|
|
245
|
+
*/
|
|
246
|
+
getState: () => RuntimeState;
|
|
200
247
|
}
|
|
201
248
|
|
|
202
249
|
// ---------------------------------------------------------------------------
|
|
203
250
|
// Component Props
|
|
204
251
|
// ---------------------------------------------------------------------------
|
|
205
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Props for the CodeSandbox component.
|
|
255
|
+
*
|
|
256
|
+
* The sandbox is a pure renderer — it receives files via props and
|
|
257
|
+
* exposes an imperative handle for programmatic updates. It does NOT
|
|
258
|
+
* interact with any storage backend directly.
|
|
259
|
+
*
|
|
260
|
+
* File sources (for initial load, pick ONE):
|
|
261
|
+
* - `files` — pass a FileMap directly
|
|
262
|
+
* - `template` — use a built-in template name (e.g., "fullstack-starter")
|
|
263
|
+
*
|
|
264
|
+
* For live updates after mount, use the imperative handle:
|
|
265
|
+
* ```tsx
|
|
266
|
+
* const ref = useRef<CodeSandboxHandle>(null);
|
|
267
|
+
* <CodeSandbox ref={ref} files={initialFiles} />
|
|
268
|
+
* // Later:
|
|
269
|
+
* ref.current.updateFiles(newFiles);
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
206
272
|
export interface CodeSandboxProps {
|
|
207
|
-
/** Pass files directly */
|
|
273
|
+
/** Pass files directly (from any source) */
|
|
208
274
|
files?: FileMap;
|
|
209
|
-
/** OR load from GitHub */
|
|
210
|
-
github?: GitHubRepo;
|
|
211
|
-
/** GitHub personal access token (for private repos and write-back) */
|
|
212
|
-
gitToken?: string;
|
|
213
275
|
/** OR use a built-in template name */
|
|
214
276
|
template?: string;
|
|
215
277
|
/** Command to start the dev server (default inferred from package.json or template) */
|
|
@@ -220,9 +282,9 @@ export interface CodeSandboxProps {
|
|
|
220
282
|
env?: Record<string, string>;
|
|
221
283
|
|
|
222
284
|
// Callbacks
|
|
223
|
-
/** Fires when a file is modified in the editor */
|
|
285
|
+
/** Fires when a file is modified in the editor by the user */
|
|
224
286
|
onFileChange?: (path: string, content: string) => void;
|
|
225
|
-
/** Fires when the dev server is ready */
|
|
287
|
+
/** Fires when the dev server is ready (initial boot or after restart) */
|
|
226
288
|
onServerReady?: (port: number, url: string) => void;
|
|
227
289
|
/** Fires on boot progress changes */
|
|
228
290
|
onProgress?: (progress: BootProgress) => void;
|
|
@@ -238,6 +300,12 @@ export interface CodeSandboxProps {
|
|
|
238
300
|
* Max 2 auto-fix attempts per error is recommended (see Ranger pattern).
|
|
239
301
|
*/
|
|
240
302
|
onSandboxError?: (error: SandboxError) => void;
|
|
303
|
+
/**
|
|
304
|
+
* Fires after updateFiles() completes (files written + server restarted).
|
|
305
|
+
* Useful for the host to know when the sandbox has finished processing
|
|
306
|
+
* a file push.
|
|
307
|
+
*/
|
|
308
|
+
onFilesUpdated?: (fileChanges: Record<string, FileChangeStatus>) => void;
|
|
241
309
|
|
|
242
310
|
// Layout
|
|
243
311
|
/** CSS class name for the root element */
|
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
|
-
}
|