clawcontainer 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/CONTRIBUTING.md +76 -0
- package/DOCS.md +370 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/black_logo.png +0 -0
- package/dist/assets/abap-DLDM7-KI.js +1 -0
- package/dist/assets/apex-DNDY2TF8.js +1 -0
- package/dist/assets/azcli-Y6nb8tq_.js +1 -0
- package/dist/assets/bat-BwHxbl9M.js +1 -0
- package/dist/assets/bicep-CFznDFnq.js +2 -0
- package/dist/assets/cameligo-Bf6VGUru.js +1 -0
- package/dist/assets/clojure-Dnu-v4kV.js +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-Bd8akH9Z.js +1 -0
- package/dist/assets/cpp-BbWJElDN.js +1 -0
- package/dist/assets/csharp-Co3qMtFm.js +1 -0
- package/dist/assets/csp-D-4FJmMZ.js +1 -0
- package/dist/assets/css-DdJfP1eB.js +3 -0
- package/dist/assets/css.worker-GxEd3MMM.js +93 -0
- package/dist/assets/cssMode-DM_ONlf-.js +1 -0
- package/dist/assets/cypher-cTPe9QuQ.js +1 -0
- package/dist/assets/dart-BOtBlQCF.js +1 -0
- package/dist/assets/dockerfile-BG73LgW2.js +1 -0
- package/dist/assets/ecl-BEgZUVRK.js +1 -0
- package/dist/assets/elixir-BkW5O-1t.js +1 -0
- package/dist/assets/flow9-BeJ5waoc.js +1 -0
- package/dist/assets/freemarker2-VbwzOQPq.js +3 -0
- package/dist/assets/fsharp-PahG7c26.js +1 -0
- package/dist/assets/go-acbASCJo.js +1 -0
- package/dist/assets/graphql-BxJiqAUM.js +1 -0
- package/dist/assets/handlebars-DLvQ802u.js +1 -0
- package/dist/assets/hcl-DtV1sZF8.js +1 -0
- package/dist/assets/html-DuEPBzmS.js +1 -0
- package/dist/assets/html.worker-lU17Tx2m.js +470 -0
- package/dist/assets/htmlMode-BfeYTJaB.js +1 -0
- package/dist/assets/index-BnBKg8GZ.js +1291 -0
- package/dist/assets/index-Dq3FlPWe.css +32 -0
- package/dist/assets/ini-Kd9XrMLS.js +1 -0
- package/dist/assets/java-CXBNlu9o.js +1 -0
- package/dist/assets/javascript-DQO1Leza.js +1 -0
- package/dist/assets/json.worker-CUJs-dtA.js +58 -0
- package/dist/assets/jsonMode--qsURhHr.js +7 -0
- package/dist/assets/julia-cl7-CwDS.js +1 -0
- package/dist/assets/kotlin-s7OhZKlX.js +1 -0
- package/dist/assets/less-9HpZscsL.js +2 -0
- package/dist/assets/lexon-OrD6JF1K.js +1 -0
- package/dist/assets/liquid-PL6MZtM8.js +1 -0
- package/dist/assets/lspLanguageFeatures-Cy5rDFeq.js +4 -0
- package/dist/assets/lua-Cyyb5UIc.js +1 -0
- package/dist/assets/m3-B8OfTtLu.js +1 -0
- package/dist/assets/markdown-BFxVWTOG.js +1 -0
- package/dist/assets/mdx-Cb3Jy14X.js +1 -0
- package/dist/assets/mips-CiqrrVzr.js +1 -0
- package/dist/assets/msdax-DmeGPVcC.js +1 -0
- package/dist/assets/mysql-C_tMU-Nz.js +1 -0
- package/dist/assets/objective-c-BDtDVThU.js +1 -0
- package/dist/assets/pascal-vHIfCaH5.js +1 -0
- package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
- package/dist/assets/perl-Ub6l9XKa.js +1 -0
- package/dist/assets/pgsql-BlNEE0v7.js +1 -0
- package/dist/assets/php-BBUBE1dy.js +1 -0
- package/dist/assets/pla-DSh2-awV.js +1 -0
- package/dist/assets/postiats-CocnycG-.js +1 -0
- package/dist/assets/powerquery-tScXyioY.js +1 -0
- package/dist/assets/powershell-COWaemsV.js +1 -0
- package/dist/assets/protobuf-Brw8urJB.js +2 -0
- package/dist/assets/pug-8SOpv6rk.js +1 -0
- package/dist/assets/python-Usm4OUwq.js +1 -0
- package/dist/assets/qsharp-Bw9ernYp.js +1 -0
- package/dist/assets/r-j7ic8hl3.js +1 -0
- package/dist/assets/razor-BIOole7a.js +1 -0
- package/dist/assets/redis-Bu5POkcn.js +1 -0
- package/dist/assets/redshift-Bs9aos_-.js +1 -0
- package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
- package/dist/assets/ruby-zBfavPgS.js +1 -0
- package/dist/assets/rust-BzKRNQWT.js +1 -0
- package/dist/assets/sb-BBc9UKZt.js +1 -0
- package/dist/assets/scala-D9hQfWCl.js +1 -0
- package/dist/assets/scheme-BPhDTwHR.js +1 -0
- package/dist/assets/scss-CBJaRo0y.js +3 -0
- package/dist/assets/shell-DiJ1NA_G.js +1 -0
- package/dist/assets/solidity-Db0IVjzk.js +1 -0
- package/dist/assets/sophia-CnS9iZB_.js +1 -0
- package/dist/assets/sparql-CJmd_6j2.js +1 -0
- package/dist/assets/sql-ClhHkBeG.js +1 -0
- package/dist/assets/st-CHwy0fLd.js +1 -0
- package/dist/assets/swift-Bqt4WxQ4.js +3 -0
- package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
- package/dist/assets/tcl-Dm6ycUr_.js +1 -0
- package/dist/assets/ts.worker-Dy9lDQQT.js +67731 -0
- package/dist/assets/tsMode-CDjF3DWK.js +11 -0
- package/dist/assets/twig-Csy3S7wG.js +1 -0
- package/dist/assets/typescript-CJR4sLnG.js +1 -0
- package/dist/assets/typespec-Btyra-wh.js +1 -0
- package/dist/assets/vb-Db0cS2oM.js +1 -0
- package/dist/assets/wgsl-DumH7NcR.js +298 -0
- package/dist/assets/xml-CJZS3uh7.js +1 -0
- package/dist/assets/yaml-DB88cW5z.js +1 -0
- package/dist/audit.d.ts +48 -0
- package/dist/container.d.ts +100 -0
- package/dist/event-emitter.d.ts +7 -0
- package/dist/favicon.png +0 -0
- package/dist/git-service.d.ts +31 -0
- package/dist/index.html +188 -0
- package/dist/logo-sm.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/main.d.ts +1 -0
- package/dist/monaco-editor.d.ts +11 -0
- package/dist/monacoeditorwork/css.worker.bundle.js +54264 -0
- package/dist/monacoeditorwork/editor.worker.bundle.js +14317 -0
- package/dist/monacoeditorwork/html.worker.bundle.js +30449 -0
- package/dist/monacoeditorwork/json.worker.bundle.js +22085 -0
- package/dist/monacoeditorwork/ts.worker.bundle.js +225552 -0
- package/dist/net-intercept.d.ts +2 -0
- package/dist/network-hook.d.ts +1 -0
- package/dist/plugin.d.ts +20 -0
- package/dist/policy.d.ts +58 -0
- package/dist/sdk.d.ts +61 -0
- package/dist/tab-manager.d.ts +11 -0
- package/dist/templates.d.ts +46 -0
- package/dist/terminal.d.ts +19 -0
- package/dist/types.d.ts +109 -0
- package/dist/ui.d.ts +81 -0
- package/dist/workspace.d.ts +16 -0
- package/index.html +159 -0
- package/logo.png +0 -0
- package/package.json +31 -0
- package/public/favicon.png +0 -0
- package/public/logo-sm.png +0 -0
- package/public/logo.png +0 -0
- package/src/audit.ts +196 -0
- package/src/container.ts +723 -0
- package/src/event-emitter.ts +28 -0
- package/src/git-service.ts +202 -0
- package/src/main.ts +9 -0
- package/src/monaco-editor.ts +111 -0
- package/src/net-intercept.ts +74 -0
- package/src/network-hook.ts +248 -0
- package/src/plugin.ts +63 -0
- package/src/policy.ts +403 -0
- package/src/sdk.ts +355 -0
- package/src/style.css +432 -0
- package/src/tab-manager.ts +30 -0
- package/src/templates.ts +271 -0
- package/src/terminal.ts +78 -0
- package/src/types.ts +113 -0
- package/src/ui.ts +1266 -0
- package/src/workspace.ts +107 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +52 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ─── Typed Event Emitter ────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export class TypedEventEmitter<T extends Record<string, unknown[]> = Record<string, unknown[]>> {
|
|
4
|
+
private handlers = new Map<keyof T, Set<(...args: any[]) => void>>();
|
|
5
|
+
|
|
6
|
+
on<K extends keyof T>(event: K, fn: (...args: T[K]) => void): void {
|
|
7
|
+
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
|
|
8
|
+
this.handlers.get(event)!.add(fn);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
off<K extends keyof T>(event: K, fn: (...args: T[K]) => void): void {
|
|
12
|
+
this.handlers.get(event)?.delete(fn);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
once<K extends keyof T>(event: K, fn: (...args: T[K]) => void): void {
|
|
16
|
+
const wrapper = (...args: T[K]) => {
|
|
17
|
+
this.off(event, wrapper);
|
|
18
|
+
fn(...args);
|
|
19
|
+
};
|
|
20
|
+
this.on(event, wrapper);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected emit<K extends keyof T>(event: K, ...args: T[K]): void {
|
|
24
|
+
const set = this.handlers.get(event);
|
|
25
|
+
if (!set) return;
|
|
26
|
+
for (const fn of set) fn(...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// ─── GitHub API Git Service ──────────────────────────────────────────────────
|
|
2
|
+
// Clone repos via Trees API, push changes via Git Data API.
|
|
3
|
+
// Runs entirely in-browser using fetch() — no git binary needed.
|
|
4
|
+
|
|
5
|
+
export interface GitFile {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RepoInfo {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const API_BASE = 'https://api.github.com';
|
|
16
|
+
|
|
17
|
+
export class GitService {
|
|
18
|
+
private token: string;
|
|
19
|
+
private owner: string;
|
|
20
|
+
private repo: string;
|
|
21
|
+
private branch: string;
|
|
22
|
+
|
|
23
|
+
constructor(token: string, owner: string, repo: string, branch = 'main') {
|
|
24
|
+
this.token = token;
|
|
25
|
+
this.owner = owner;
|
|
26
|
+
this.repo = repo;
|
|
27
|
+
this.branch = branch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Extract owner/repo from a GitHub URL. */
|
|
31
|
+
static parseRepoUrl(url: string): RepoInfo {
|
|
32
|
+
// Supports: https://github.com/owner/repo, https://github.com/owner/repo.git,
|
|
33
|
+
// github.com/owner/repo, owner/repo
|
|
34
|
+
const cleaned = url.trim().replace(/\.git\s*$/, '').replace(/\/+$/, '');
|
|
35
|
+
const ghMatch = cleaned.match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^/]+)\/([^/]+)/);
|
|
36
|
+
if (ghMatch) return { owner: ghMatch[1], repo: ghMatch[2] };
|
|
37
|
+
|
|
38
|
+
// owner/repo shorthand
|
|
39
|
+
const shortMatch = cleaned.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
40
|
+
if (shortMatch) return { owner: shortMatch[1], repo: shortMatch[2] };
|
|
41
|
+
|
|
42
|
+
throw new Error(`Invalid GitHub URL: ${url}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private headers(): Record<string, string> {
|
|
46
|
+
return {
|
|
47
|
+
'Authorization': `Bearer ${this.token}`,
|
|
48
|
+
'Accept': 'application/vnd.github+json',
|
|
49
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async api<T>(path: string, init?: RequestInit): Promise<T> {
|
|
54
|
+
const resp = await fetch(`${API_BASE}${path}`, {
|
|
55
|
+
...init,
|
|
56
|
+
headers: { ...this.headers(), ...init?.headers },
|
|
57
|
+
});
|
|
58
|
+
if (!resp.ok) {
|
|
59
|
+
const body = await resp.text().catch(() => '');
|
|
60
|
+
throw new Error(`GitHub API ${resp.status}: ${resp.statusText} — ${body.slice(0, 300)}`);
|
|
61
|
+
}
|
|
62
|
+
return resp.json() as Promise<T>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Detect the default branch of the repo. */
|
|
66
|
+
async detectDefaultBranch(): Promise<string> {
|
|
67
|
+
const repo = await this.api<{ default_branch: string }>(
|
|
68
|
+
`/repos/${this.owner}/${this.repo}`,
|
|
69
|
+
);
|
|
70
|
+
this.branch = repo.default_branch;
|
|
71
|
+
return this.branch;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Fetch the full file tree + contents from the repo. */
|
|
75
|
+
async fetchRepoTree(): Promise<GitFile[]> {
|
|
76
|
+
// Get the tree SHA for the branch
|
|
77
|
+
const ref = await this.api<{ object: { sha: string } }>(
|
|
78
|
+
`/repos/${this.owner}/${this.repo}/git/ref/heads/${this.branch}`,
|
|
79
|
+
);
|
|
80
|
+
const commitSha = ref.object.sha;
|
|
81
|
+
|
|
82
|
+
const commit = await this.api<{ tree: { sha: string } }>(
|
|
83
|
+
`/repos/${this.owner}/${this.repo}/git/commits/${commitSha}`,
|
|
84
|
+
);
|
|
85
|
+
const treeSha = commit.tree.sha;
|
|
86
|
+
|
|
87
|
+
// Get recursive tree
|
|
88
|
+
const tree = await this.api<{
|
|
89
|
+
tree: Array<{ path: string; type: string; sha: string; size?: number }>;
|
|
90
|
+
truncated: boolean;
|
|
91
|
+
}>(`/repos/${this.owner}/${this.repo}/git/trees/${treeSha}?recursive=1`);
|
|
92
|
+
|
|
93
|
+
// Fetch blob contents for files (skip large files > 1MB)
|
|
94
|
+
const files: GitFile[] = [];
|
|
95
|
+
const blobs = tree.tree.filter(
|
|
96
|
+
(n) => n.type === 'blob' && (n.size ?? 0) < 1_048_576,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Batch fetch in parallel (max 10 concurrent)
|
|
100
|
+
const batchSize = 10;
|
|
101
|
+
for (let i = 0; i < blobs.length; i += batchSize) {
|
|
102
|
+
const batch = blobs.slice(i, i + batchSize);
|
|
103
|
+
const results = await Promise.all(
|
|
104
|
+
batch.map(async (blob) => {
|
|
105
|
+
try {
|
|
106
|
+
const data = await this.api<{ content: string; encoding: string }>(
|
|
107
|
+
`/repos/${this.owner}/${this.repo}/git/blobs/${blob.sha}`,
|
|
108
|
+
);
|
|
109
|
+
if (data.encoding === 'base64') {
|
|
110
|
+
return { path: blob.path, content: atob(data.content.replace(/\n/g, '')) };
|
|
111
|
+
}
|
|
112
|
+
return { path: blob.path, content: data.content };
|
|
113
|
+
} catch {
|
|
114
|
+
return null; // Skip files that fail
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
if (r) files.push(r);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Push file changes as a single atomic commit.
|
|
128
|
+
* Creates blobs → tree → commit → updates ref.
|
|
129
|
+
*/
|
|
130
|
+
async pushChanges(files: GitFile[], message: string): Promise<string> {
|
|
131
|
+
// 1. Get current commit SHA for the branch
|
|
132
|
+
const ref = await this.api<{ object: { sha: string } }>(
|
|
133
|
+
`/repos/${this.owner}/${this.repo}/git/ref/heads/${this.branch}`,
|
|
134
|
+
);
|
|
135
|
+
const parentSha = ref.object.sha;
|
|
136
|
+
|
|
137
|
+
// 2. Create blobs for each file
|
|
138
|
+
const treeEntries: Array<{ path: string; mode: string; type: string; sha: string }> = [];
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const blob = await this.api<{ sha: string }>(
|
|
141
|
+
`/repos/${this.owner}/${this.repo}/git/blobs`,
|
|
142
|
+
{
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ content: file.content, encoding: 'utf-8' }),
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
treeEntries.push({
|
|
149
|
+
path: file.path,
|
|
150
|
+
mode: '100644',
|
|
151
|
+
type: 'blob',
|
|
152
|
+
sha: blob.sha,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Create tree based on parent commit's tree
|
|
157
|
+
const parentCommit = await this.api<{ tree: { sha: string } }>(
|
|
158
|
+
`/repos/${this.owner}/${this.repo}/git/commits/${parentSha}`,
|
|
159
|
+
);
|
|
160
|
+
const newTree = await this.api<{ sha: string }>(
|
|
161
|
+
`/repos/${this.owner}/${this.repo}/git/trees`,
|
|
162
|
+
{
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
base_tree: parentCommit.tree.sha,
|
|
167
|
+
tree: treeEntries,
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 4. Create commit
|
|
173
|
+
const newCommit = await this.api<{ sha: string }>(
|
|
174
|
+
`/repos/${this.owner}/${this.repo}/git/commits`,
|
|
175
|
+
{
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
message,
|
|
180
|
+
tree: newTree.sha,
|
|
181
|
+
parents: [parentSha],
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// 5. Update ref
|
|
187
|
+
await this.api(
|
|
188
|
+
`/repos/${this.owner}/${this.repo}/git/refs/heads/${this.branch}`,
|
|
189
|
+
{
|
|
190
|
+
method: 'PATCH',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ sha: newCommit.sha }),
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return newCommit.sha;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get repoOwner(): string { return this.owner; }
|
|
200
|
+
get repoName(): string { return this.repo; }
|
|
201
|
+
get repoBranch(): string { return this.branch; }
|
|
202
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ClawContainer } from './sdk.js';
|
|
2
|
+
|
|
3
|
+
// ─── Boot sequence ───────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const cc = new ClawContainer('#app');
|
|
6
|
+
cc.start().catch(console.error);
|
|
7
|
+
|
|
8
|
+
// Expose SDK globally for console access and external scripts
|
|
9
|
+
(window as any).clawcontainer = cc;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as monaco from 'monaco-editor';
|
|
2
|
+
|
|
3
|
+
// ─── Extension → Monaco language ID ──────────────────────────────────────────
|
|
4
|
+
const EXT_TO_LANG: Record<string, string> = {
|
|
5
|
+
js: 'javascript', jsx: 'javascript',
|
|
6
|
+
ts: 'typescript', tsx: 'typescript',
|
|
7
|
+
json: 'json',
|
|
8
|
+
html: 'html', htm: 'html',
|
|
9
|
+
css: 'css',
|
|
10
|
+
md: 'markdown',
|
|
11
|
+
yaml: 'yaml', yml: 'yaml',
|
|
12
|
+
xml: 'xml', svg: 'xml',
|
|
13
|
+
py: 'python',
|
|
14
|
+
rb: 'ruby',
|
|
15
|
+
go: 'go',
|
|
16
|
+
rs: 'rust',
|
|
17
|
+
sh: 'shell', bash: 'shell',
|
|
18
|
+
sql: 'sql',
|
|
19
|
+
toml: 'ini',
|
|
20
|
+
ini: 'ini', conf: 'ini',
|
|
21
|
+
vue: 'html',
|
|
22
|
+
svelte: 'html',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const THEME_NAME = 'clawchef-dark';
|
|
26
|
+
|
|
27
|
+
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
|
|
28
|
+
const models = new Map<string, monaco.editor.ITextModel>();
|
|
29
|
+
|
|
30
|
+
// ─── Theme ───────────────────────────────────────────────────────────────────
|
|
31
|
+
export function initMonacoTheme(): void {
|
|
32
|
+
monaco.editor.defineTheme(THEME_NAME, {
|
|
33
|
+
base: 'vs-dark',
|
|
34
|
+
inherit: true,
|
|
35
|
+
rules: [],
|
|
36
|
+
colors: {
|
|
37
|
+
'editor.background': '#161b22',
|
|
38
|
+
'editor.foreground': '#e6edf3',
|
|
39
|
+
'editorLineNumber.foreground': '#8b949e',
|
|
40
|
+
'editorLineNumber.activeForeground': '#e6edf3',
|
|
41
|
+
'editor.lineHighlightBackground': '#21262d',
|
|
42
|
+
'editorWidget.background': '#161b22',
|
|
43
|
+
'editorWidget.border': '#30363d',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
49
|
+
function langFromFilename(filename: string): string {
|
|
50
|
+
const ext = filename.includes('.') ? filename.split('.').pop()!.toLowerCase() : '';
|
|
51
|
+
return EXT_TO_LANG[ext] ?? 'plaintext';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Create the single editor instance (call once when the first tab opens). */
|
|
57
|
+
export function createEditorInstance(container: HTMLElement): void {
|
|
58
|
+
if (editorInstance) return;
|
|
59
|
+
editorInstance = monaco.editor.create(container, {
|
|
60
|
+
theme: THEME_NAME,
|
|
61
|
+
readOnly: false,
|
|
62
|
+
automaticLayout: true,
|
|
63
|
+
minimap: { enabled: false },
|
|
64
|
+
contextmenu: false,
|
|
65
|
+
fontFamily: "'SF Mono','Fira Code','Cascadia Code',Menlo,monospace",
|
|
66
|
+
fontSize: 12,
|
|
67
|
+
lineHeight: 1.6 * 12,
|
|
68
|
+
scrollBeyondLastLine: false,
|
|
69
|
+
renderLineHighlight: 'line',
|
|
70
|
+
padding: { top: 12, bottom: 12 },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Create or reuse a model for a file, then switch the editor to it. */
|
|
75
|
+
export function openFileModel(filePath: string, filename: string, content: string): void {
|
|
76
|
+
if (!editorInstance) return;
|
|
77
|
+
|
|
78
|
+
let model = models.get(filePath);
|
|
79
|
+
if (!model) {
|
|
80
|
+
const lang = langFromFilename(filename);
|
|
81
|
+
model = monaco.editor.createModel(content, lang);
|
|
82
|
+
models.set(filePath, model);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
editorInstance.setModel(model);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the current content of a file's model. */
|
|
89
|
+
export function getModelContent(filePath: string): string {
|
|
90
|
+
const model = models.get(filePath);
|
|
91
|
+
return model ? model.getValue() : '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Dispose a single file model (when closing a tab). */
|
|
95
|
+
export function closeFileModel(filePath: string): void {
|
|
96
|
+
const model = models.get(filePath);
|
|
97
|
+
if (model) {
|
|
98
|
+
model.dispose();
|
|
99
|
+
models.delete(filePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Dispose the editor instance and all models. */
|
|
104
|
+
export function disposeAll(): void {
|
|
105
|
+
if (editorInstance) {
|
|
106
|
+
editorInstance.dispose();
|
|
107
|
+
editorInstance = null;
|
|
108
|
+
}
|
|
109
|
+
for (const model of models.values()) model.dispose();
|
|
110
|
+
models.clear();
|
|
111
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ─── Browser-side fetch interceptor ──────────────────────────────────────────
|
|
2
|
+
// Monkey-patches window.fetch to log all outbound requests to the audit log.
|
|
3
|
+
|
|
4
|
+
import { AuditLog } from './audit.js';
|
|
5
|
+
|
|
6
|
+
export function installBrowserFetchInterceptor(audit: AuditLog): void {
|
|
7
|
+
const originalFetch = window.fetch;
|
|
8
|
+
|
|
9
|
+
window.fetch = async function patchedFetch(
|
|
10
|
+
input: RequestInfo | URL,
|
|
11
|
+
init?: RequestInit,
|
|
12
|
+
): Promise<Response> {
|
|
13
|
+
const startMs = performance.now();
|
|
14
|
+
|
|
15
|
+
// Normalize URL
|
|
16
|
+
let url: string;
|
|
17
|
+
if (input instanceof Request) {
|
|
18
|
+
url = input.url;
|
|
19
|
+
} else if (input instanceof URL) {
|
|
20
|
+
url = input.href;
|
|
21
|
+
} else {
|
|
22
|
+
url = input;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Normalize method + headers
|
|
26
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
27
|
+
|
|
28
|
+
const rawHeaders: Record<string, string> = {};
|
|
29
|
+
const headerSource = init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
30
|
+
if (headerSource instanceof Headers) {
|
|
31
|
+
headerSource.forEach((v, k) => { rawHeaders[k] = v; });
|
|
32
|
+
} else if (headerSource && typeof headerSource === 'object') {
|
|
33
|
+
for (const [k, v] of Object.entries(headerSource)) {
|
|
34
|
+
rawHeaders[k] = String(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Capture request body
|
|
39
|
+
let bodyPreview: string | undefined;
|
|
40
|
+
const bodySource = init?.body ?? (input instanceof Request ? input.body : undefined);
|
|
41
|
+
if (typeof bodySource === 'string') {
|
|
42
|
+
bodyPreview = AuditLog.truncateBody(bodySource);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Log request
|
|
46
|
+
audit.log('net.request', url, {
|
|
47
|
+
origin: 'browser',
|
|
48
|
+
method,
|
|
49
|
+
headers: AuditLog.maskHeaders(rawHeaders),
|
|
50
|
+
...(bodyPreview ? { bodyPreview } : {}),
|
|
51
|
+
}, { source: 'system' });
|
|
52
|
+
|
|
53
|
+
// Call original
|
|
54
|
+
const resp = await originalFetch.call(window, input, init);
|
|
55
|
+
|
|
56
|
+
const durationMs = Math.round(performance.now() - startMs);
|
|
57
|
+
|
|
58
|
+
// Collect response headers
|
|
59
|
+
const respHeaders: Record<string, string> = {};
|
|
60
|
+
resp.headers.forEach((v, k) => { respHeaders[k] = v; });
|
|
61
|
+
|
|
62
|
+
// Log response
|
|
63
|
+
audit.log('net.response', url, {
|
|
64
|
+
origin: 'browser',
|
|
65
|
+
method,
|
|
66
|
+
status: resp.status,
|
|
67
|
+
statusText: resp.statusText,
|
|
68
|
+
headers: respHeaders,
|
|
69
|
+
durationMs,
|
|
70
|
+
}, { source: 'system' });
|
|
71
|
+
|
|
72
|
+
return resp;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// ─── Container-side network hook ─────────────────────────────────────────────
|
|
2
|
+
// Exported as a string constant, mounted into the WebContainer as network-hook.cjs
|
|
3
|
+
// and loaded via NODE_OPTIONS=--require ./network-hook.cjs.
|
|
4
|
+
// Patches Node.js http/https/fetch to emit __NET_AUDIT__ markers on stderr.
|
|
5
|
+
|
|
6
|
+
export const NETWORK_HOOK_CJS = `'use strict';
|
|
7
|
+
// ── network-hook.cjs ── injected via NODE_OPTIONS=--require ──
|
|
8
|
+
|
|
9
|
+
// Skip logging when running inside npm to avoid flooding with registry requests
|
|
10
|
+
if (process.env.npm_execpath || process.env.npm_lifecycle_event) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
(function () {
|
|
15
|
+
var MARKER = '__NET_AUDIT__:';
|
|
16
|
+
|
|
17
|
+
function maskVal(v) {
|
|
18
|
+
if (!v || typeof v !== 'string') return '****';
|
|
19
|
+
if (v.length <= 12) return '****';
|
|
20
|
+
return v.slice(0, 7) + '...' + v.slice(-4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var SENSITIVE = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
|
|
24
|
+
var SENSITIVE_PARTIAL = /secret|token/i;
|
|
25
|
+
|
|
26
|
+
function maskHeaders(h) {
|
|
27
|
+
if (!h || typeof h !== 'object') return {};
|
|
28
|
+
var out = {};
|
|
29
|
+
var keys = Object.keys(h);
|
|
30
|
+
for (var i = 0; i < keys.length; i++) {
|
|
31
|
+
var k = keys[i];
|
|
32
|
+
var v = typeof h[k] === 'string' ? h[k] : String(h[k]);
|
|
33
|
+
if (SENSITIVE.test(k) || SENSITIVE_PARTIAL.test(k)) {
|
|
34
|
+
out[k] = maskVal(v);
|
|
35
|
+
} else {
|
|
36
|
+
out[k] = v;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function emit(obj) {
|
|
43
|
+
try {
|
|
44
|
+
process.stderr.write(MARKER + JSON.stringify(obj) + '\\n');
|
|
45
|
+
} catch (_) {
|
|
46
|
+
// never crash the host process
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildUrl(opts) {
|
|
51
|
+
if (typeof opts === 'string') return opts;
|
|
52
|
+
if (opts && opts.href) return opts.href;
|
|
53
|
+
if (!opts) return '<unknown>';
|
|
54
|
+
var proto = opts.protocol || 'https:';
|
|
55
|
+
var host = opts.hostname || opts.host || 'localhost';
|
|
56
|
+
var port = opts.port ? ':' + opts.port : '';
|
|
57
|
+
var path = opts.path || '/';
|
|
58
|
+
return proto + '//' + host + port + path;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var BODY_MAX = 2000;
|
|
62
|
+
|
|
63
|
+
// ── Patch http/https ──
|
|
64
|
+
|
|
65
|
+
function patchModule(modName) {
|
|
66
|
+
try {
|
|
67
|
+
var mod = require(modName);
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var origRequest = mod.request;
|
|
73
|
+
var origGet = mod.get;
|
|
74
|
+
|
|
75
|
+
function wrapRequest(orig) {
|
|
76
|
+
return function patchedRequest(urlOrOpts, optsOrCb, maybeCb) {
|
|
77
|
+
var opts, cb;
|
|
78
|
+
if (typeof urlOrOpts === 'string' || (urlOrOpts && typeof urlOrOpts.href === 'string')) {
|
|
79
|
+
opts = typeof optsOrCb === 'object' ? optsOrCb : {};
|
|
80
|
+
cb = typeof optsOrCb === 'function' ? optsOrCb : maybeCb;
|
|
81
|
+
} else {
|
|
82
|
+
opts = urlOrOpts || {};
|
|
83
|
+
cb = typeof optsOrCb === 'function' ? optsOrCb : maybeCb;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var url = buildUrl(urlOrOpts) !== '<unknown>' ? buildUrl(urlOrOpts) : buildUrl(opts);
|
|
87
|
+
var method = (opts.method || 'GET').toUpperCase();
|
|
88
|
+
var hdrs = opts.headers ? maskHeaders(opts.headers) : {};
|
|
89
|
+
var startTime = Date.now();
|
|
90
|
+
|
|
91
|
+
emit({
|
|
92
|
+
type: 'request',
|
|
93
|
+
url: url,
|
|
94
|
+
method: method,
|
|
95
|
+
headers: hdrs,
|
|
96
|
+
ts: new Date().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
var req = orig.apply(this, arguments);
|
|
100
|
+
|
|
101
|
+
// Capture request body
|
|
102
|
+
var bodyChunks = [];
|
|
103
|
+
var bodyLen = 0;
|
|
104
|
+
var origWrite = req.write;
|
|
105
|
+
var origEnd = req.end;
|
|
106
|
+
|
|
107
|
+
req.write = function (chunk) {
|
|
108
|
+
if (bodyLen < BODY_MAX && chunk) {
|
|
109
|
+
var s = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
110
|
+
bodyChunks.push(s.slice(0, BODY_MAX - bodyLen));
|
|
111
|
+
bodyLen += s.length;
|
|
112
|
+
}
|
|
113
|
+
return origWrite.apply(req, arguments);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
req.end = function (chunk) {
|
|
117
|
+
if (bodyLen < BODY_MAX && chunk && typeof chunk !== 'function') {
|
|
118
|
+
var s = typeof chunk === 'string' ? chunk : (Buffer.isBuffer(chunk) ? chunk.toString('utf-8') : '');
|
|
119
|
+
if (s) {
|
|
120
|
+
bodyChunks.push(s.slice(0, BODY_MAX - bodyLen));
|
|
121
|
+
bodyLen += s.length;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (bodyChunks.length > 0) {
|
|
126
|
+
var body = bodyChunks.join('');
|
|
127
|
+
emit({
|
|
128
|
+
type: 'request.body',
|
|
129
|
+
url: url,
|
|
130
|
+
method: method,
|
|
131
|
+
bodyPreview: body.length > BODY_MAX ? body.slice(0, BODY_MAX) + '...[truncated]' : body,
|
|
132
|
+
ts: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return origEnd.apply(req, arguments);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Capture response
|
|
140
|
+
req.on('response', function (res) {
|
|
141
|
+
var durationMs = Date.now() - startTime;
|
|
142
|
+
emit({
|
|
143
|
+
type: 'response',
|
|
144
|
+
url: url,
|
|
145
|
+
method: method,
|
|
146
|
+
status: res.statusCode,
|
|
147
|
+
headers: maskHeaders(res.headers),
|
|
148
|
+
durationMs: durationMs,
|
|
149
|
+
ts: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
req.on('error', function (err) {
|
|
154
|
+
var durationMs = Date.now() - startTime;
|
|
155
|
+
emit({
|
|
156
|
+
type: 'response',
|
|
157
|
+
url: url,
|
|
158
|
+
method: method,
|
|
159
|
+
error: err.message,
|
|
160
|
+
durationMs: durationMs,
|
|
161
|
+
ts: new Date().toISOString(),
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return req;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
mod.request = wrapRequest(origRequest);
|
|
170
|
+
mod.get = wrapRequest(origGet);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
patchModule('http');
|
|
174
|
+
patchModule('https');
|
|
175
|
+
|
|
176
|
+
// ── Patch globalThis.fetch (Node 18+) ──
|
|
177
|
+
|
|
178
|
+
if (typeof globalThis.fetch === 'function') {
|
|
179
|
+
var origFetch = globalThis.fetch;
|
|
180
|
+
|
|
181
|
+
globalThis.fetch = function patchedFetch(input, init) {
|
|
182
|
+
var url = typeof input === 'string' ? input
|
|
183
|
+
: (input && input.url) ? input.url
|
|
184
|
+
: String(input);
|
|
185
|
+
var method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
186
|
+
|
|
187
|
+
var rawHeaders = {};
|
|
188
|
+
var hdrSrc = (init && init.headers) || (input && input.headers);
|
|
189
|
+
if (hdrSrc && typeof hdrSrc === 'object') {
|
|
190
|
+
if (typeof hdrSrc.forEach === 'function') {
|
|
191
|
+
hdrSrc.forEach(function (v, k) { rawHeaders[k] = v; });
|
|
192
|
+
} else {
|
|
193
|
+
var keys = Object.keys(hdrSrc);
|
|
194
|
+
for (var i = 0; i < keys.length; i++) rawHeaders[keys[i]] = String(hdrSrc[keys[i]]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
var bodyPreview;
|
|
199
|
+
var bodySrc = (init && init.body) || (input && input.body);
|
|
200
|
+
if (typeof bodySrc === 'string') {
|
|
201
|
+
bodyPreview = bodySrc.length > BODY_MAX ? bodySrc.slice(0, BODY_MAX) + '...[truncated]' : bodySrc;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var startTime = Date.now();
|
|
205
|
+
|
|
206
|
+
emit({
|
|
207
|
+
type: 'request',
|
|
208
|
+
url: url,
|
|
209
|
+
method: method,
|
|
210
|
+
headers: maskHeaders(rawHeaders),
|
|
211
|
+
bodyPreview: bodyPreview,
|
|
212
|
+
ts: new Date().toISOString(),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return origFetch.apply(globalThis, arguments).then(function (resp) {
|
|
216
|
+
var durationMs = Date.now() - startTime;
|
|
217
|
+
var respHeaders = {};
|
|
218
|
+
if (resp.headers && typeof resp.headers.forEach === 'function') {
|
|
219
|
+
resp.headers.forEach(function (v, k) { respHeaders[k] = v; });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
emit({
|
|
223
|
+
type: 'response',
|
|
224
|
+
url: url,
|
|
225
|
+
method: method,
|
|
226
|
+
status: resp.status,
|
|
227
|
+
headers: respHeaders,
|
|
228
|
+
durationMs: durationMs,
|
|
229
|
+
ts: new Date().toISOString(),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return resp;
|
|
233
|
+
}, function (err) {
|
|
234
|
+
var durationMs = Date.now() - startTime;
|
|
235
|
+
emit({
|
|
236
|
+
type: 'response',
|
|
237
|
+
url: url,
|
|
238
|
+
method: method,
|
|
239
|
+
error: err.message,
|
|
240
|
+
durationMs: durationMs,
|
|
241
|
+
ts: new Date().toISOString(),
|
|
242
|
+
});
|
|
243
|
+
throw err;
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
`;
|