@wavexzore/sandbox 0.1.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/Dockerfile +14 -0
- package/LICENSE +661 -0
- package/NOTICE +3 -0
- package/README.md +153 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/sandbox/cli/install.d.ts +5 -0
- package/dist/sandbox/cli/install.js +335 -0
- package/dist/sandbox/cli/local-store.d.ts +87 -0
- package/dist/sandbox/cli/local-store.js +604 -0
- package/dist/sandbox/cli/opencode-config.d.ts +25 -0
- package/dist/sandbox/cli/opencode-config.js +240 -0
- package/dist/sandbox/cli/path.d.ts +64 -0
- package/dist/sandbox/cli/path.js +127 -0
- package/dist/sandbox/cli/types.d.ts +145 -0
- package/dist/sandbox/cli/types.js +6 -0
- package/dist/sandbox/cli/wavexzore-sandbox.d.ts +65 -0
- package/dist/sandbox/cli/wavexzore-sandbox.js +577 -0
- package/dist/sandbox/core/cli-helper.d.ts +19 -0
- package/dist/sandbox/core/cli-helper.js +64 -0
- package/dist/sandbox/core/docker-archive-utils.d.ts +3 -0
- package/dist/sandbox/core/docker-archive-utils.js +50 -0
- package/dist/sandbox/core/docker-sandbox.d.ts +51 -0
- package/dist/sandbox/core/docker-sandbox.js +675 -0
- package/dist/sandbox/core/edit/filediff.d.ts +16 -0
- package/dist/sandbox/core/edit/filediff.js +21 -0
- package/dist/sandbox/core/edit/index.d.ts +5 -0
- package/dist/sandbox/core/edit/index.js +5 -0
- package/dist/sandbox/core/edit/line-endings.d.ts +4 -0
- package/dist/sandbox/core/edit/line-endings.js +10 -0
- package/dist/sandbox/core/edit/lock.d.ts +1 -0
- package/dist/sandbox/core/edit/lock.js +18 -0
- package/dist/sandbox/core/edit/replace.d.ts +10 -0
- package/dist/sandbox/core/edit/replace.js +14 -0
- package/dist/sandbox/core/edit/replacers.d.ts +15 -0
- package/dist/sandbox/core/edit/replacers.js +241 -0
- package/dist/sandbox/core/logger.d.ts +15 -0
- package/dist/sandbox/core/logger.js +59 -0
- package/dist/sandbox/core/lsp/client.d.ts +63 -0
- package/dist/sandbox/core/lsp/client.js +533 -0
- package/dist/sandbox/core/lsp/config.d.ts +13 -0
- package/dist/sandbox/core/lsp/config.js +36 -0
- package/dist/sandbox/core/lsp/diagnostics.d.ts +12 -0
- package/dist/sandbox/core/lsp/diagnostics.js +65 -0
- package/dist/sandbox/core/lsp/index.d.ts +4 -0
- package/dist/sandbox/core/lsp/index.js +4 -0
- package/dist/sandbox/core/lsp/language.d.ts +24 -0
- package/dist/sandbox/core/lsp/language.js +249 -0
- package/dist/sandbox/core/lsp/manager.d.ts +77 -0
- package/dist/sandbox/core/lsp/manager.js +237 -0
- package/dist/sandbox/core/lsp/tooling.d.ts +14 -0
- package/dist/sandbox/core/lsp/tooling.js +78 -0
- package/dist/sandbox/core/patch-parser.d.ts +23 -0
- package/dist/sandbox/core/patch-parser.js +248 -0
- package/dist/sandbox/core/path-map.d.ts +9 -0
- package/dist/sandbox/core/path-map.js +73 -0
- package/dist/sandbox/core/project-data-storage.d.ts +42 -0
- package/dist/sandbox/core/project-data-storage.js +167 -0
- package/dist/sandbox/core/read/binary.d.ts +4 -0
- package/dist/sandbox/core/read/binary.js +80 -0
- package/dist/sandbox/core/read/format.d.ts +38 -0
- package/dist/sandbox/core/read/format.js +85 -0
- package/dist/sandbox/core/read/index.d.ts +3 -0
- package/dist/sandbox/core/read/index.js +3 -0
- package/dist/sandbox/core/read/permissions.d.ts +7 -0
- package/dist/sandbox/core/read/permissions.js +13 -0
- package/dist/sandbox/core/session-manager.d.ts +29 -0
- package/dist/sandbox/core/session-manager.js +338 -0
- package/dist/sandbox/core/shell/config.d.ts +7 -0
- package/dist/sandbox/core/shell/config.js +82 -0
- package/dist/sandbox/core/shell/output.d.ts +35 -0
- package/dist/sandbox/core/shell/output.js +80 -0
- package/dist/sandbox/core/shell/parser.d.ts +7 -0
- package/dist/sandbox/core/shell/parser.js +122 -0
- package/dist/sandbox/core/shell/permissions.d.ts +13 -0
- package/dist/sandbox/core/shell/permissions.js +33 -0
- package/dist/sandbox/core/shell/workdir.d.ts +4 -0
- package/dist/sandbox/core/shell/workdir.js +19 -0
- package/dist/sandbox/core/stream-utils.d.ts +23 -0
- package/dist/sandbox/core/stream-utils.js +97 -0
- package/dist/sandbox/core/toast.d.ts +47 -0
- package/dist/sandbox/core/toast.js +73 -0
- package/dist/sandbox/core/types.d.ts +159 -0
- package/dist/sandbox/core/types.js +11 -0
- package/dist/sandbox/core/write/bom.d.ts +8 -0
- package/dist/sandbox/core/write/bom.js +15 -0
- package/dist/sandbox/core/write/config.d.ts +14 -0
- package/dist/sandbox/core/write/config.js +188 -0
- package/dist/sandbox/core/write/diagnostics.d.ts +19 -0
- package/dist/sandbox/core/write/diagnostics.js +120 -0
- package/dist/sandbox/core/write/diff.d.ts +7 -0
- package/dist/sandbox/core/write/diff.js +21 -0
- package/dist/sandbox/core/write/formatter.d.ts +16 -0
- package/dist/sandbox/core/write/formatter.js +51 -0
- package/dist/sandbox/core/write/index.d.ts +6 -0
- package/dist/sandbox/core/write/index.js +5 -0
- package/dist/sandbox/core/write/permissions.d.ts +13 -0
- package/dist/sandbox/core/write/permissions.js +19 -0
- package/dist/sandbox/core/write/pipeline.d.ts +48 -0
- package/dist/sandbox/core/write/pipeline.js +229 -0
- package/dist/sandbox/core/write/read-tracker.d.ts +13 -0
- package/dist/sandbox/core/write/read-tracker.js +30 -0
- package/dist/sandbox/git/host-git-manager.d.ts +40 -0
- package/dist/sandbox/git/host-git-manager.js +278 -0
- package/dist/sandbox/git/index.d.ts +5 -0
- package/dist/sandbox/git/index.js +5 -0
- package/dist/sandbox/git/sandbox-git-manager.d.ts +14 -0
- package/dist/sandbox/git/sandbox-git-manager.js +54 -0
- package/dist/sandbox/git/session-git-manager.d.ts +18 -0
- package/dist/sandbox/git/session-git-manager.js +85 -0
- package/dist/sandbox/index.d.ts +205 -0
- package/dist/sandbox/index.js +70 -0
- package/dist/sandbox/plugins/custom-tools.d.ts +203 -0
- package/dist/sandbox/plugins/custom-tools.js +15 -0
- package/dist/sandbox/plugins/session-events.d.ts +10 -0
- package/dist/sandbox/plugins/session-events.js +56 -0
- package/dist/sandbox/plugins/system-transform.d.ts +10 -0
- package/dist/sandbox/plugins/system-transform.js +23 -0
- package/dist/sandbox/tools/bash-output.d.ts +17 -0
- package/dist/sandbox/tools/bash-output.js +35 -0
- package/dist/sandbox/tools/bash-status.d.ts +13 -0
- package/dist/sandbox/tools/bash-status.js +29 -0
- package/dist/sandbox/tools/bash-stop.d.ts +13 -0
- package/dist/sandbox/tools/bash-stop.js +28 -0
- package/dist/sandbox/tools/bash.d.ts +26 -0
- package/dist/sandbox/tools/bash.js +120 -0
- package/dist/sandbox/tools/edit.d.ts +20 -0
- package/dist/sandbox/tools/edit.js +87 -0
- package/dist/sandbox/tools/get-preview-url.d.ts +17 -0
- package/dist/sandbox/tools/get-preview-url.js +16 -0
- package/dist/sandbox/tools/glob.d.ts +17 -0
- package/dist/sandbox/tools/glob.js +23 -0
- package/dist/sandbox/tools/grep.d.ts +17 -0
- package/dist/sandbox/tools/grep.js +23 -0
- package/dist/sandbox/tools/ls.d.ts +17 -0
- package/dist/sandbox/tools/ls.js +21 -0
- package/dist/sandbox/tools/lsp.d.ts +41 -0
- package/dist/sandbox/tools/lsp.js +198 -0
- package/dist/sandbox/tools/multiedit.d.ts +24 -0
- package/dist/sandbox/tools/multiedit.js +83 -0
- package/dist/sandbox/tools/patch.d.ts +14 -0
- package/dist/sandbox/tools/patch.js +260 -0
- package/dist/sandbox/tools/read.d.ts +22 -0
- package/dist/sandbox/tools/read.js +105 -0
- package/dist/sandbox/tools/write.d.ts +16 -0
- package/dist/sandbox/tools/write.js +27 -0
- package/dist/sandbox/tools.d.ts +200 -0
- package/dist/sandbox/tools.js +43 -0
- package/package.json +55 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// ─── OpenCode Constants ─────────────────────────
|
|
6
|
+
export const EVENT_TYPE_SESSION_DELETED = 'session.deleted';
|
|
7
|
+
export const EVENT_TYPE_SESSION_IDLE = 'session.idle';
|
|
8
|
+
// ─── Plugin Constants ───────────────────────────
|
|
9
|
+
export const LOG_LEVEL_INFO = 'INFO';
|
|
10
|
+
export const LOG_LEVEL_ERROR = 'ERROR';
|
|
11
|
+
export const LOG_LEVEL_WARN = 'WARN';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type BomSplit = {
|
|
2
|
+
bom: boolean;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function splitBom(content: string): BomSplit;
|
|
6
|
+
export declare function joinBom(content: string, bom: boolean): string;
|
|
7
|
+
export declare function decodeUtf8(buffer: Buffer): string;
|
|
8
|
+
export declare function encodeUtf8(content: string): Buffer<ArrayBuffer>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const UTF8_BOM = '\uFEFF';
|
|
2
|
+
export function splitBom(content) {
|
|
3
|
+
if (!content.startsWith(UTF8_BOM))
|
|
4
|
+
return { bom: false, text: content };
|
|
5
|
+
return { bom: true, text: content.slice(1) };
|
|
6
|
+
}
|
|
7
|
+
export function joinBom(content, bom) {
|
|
8
|
+
return bom ? `${UTF8_BOM}${splitBom(content).text}` : splitBom(content).text;
|
|
9
|
+
}
|
|
10
|
+
export function decodeUtf8(buffer) {
|
|
11
|
+
return buffer.toString('utf8');
|
|
12
|
+
}
|
|
13
|
+
export function encodeUtf8(content) {
|
|
14
|
+
return Buffer.from(content, 'utf8');
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type FormatterCwd = 'container' | 'host';
|
|
2
|
+
export type FormatterConfig = {
|
|
3
|
+
id: string;
|
|
4
|
+
command: string[];
|
|
5
|
+
extensions: string[];
|
|
6
|
+
environment: Record<string, string>;
|
|
7
|
+
cwd: FormatterCwd;
|
|
8
|
+
};
|
|
9
|
+
export type WriteToolConfig = {
|
|
10
|
+
requireReadBeforeOverwrite: boolean;
|
|
11
|
+
allowUnmappedWrites: boolean;
|
|
12
|
+
formatters: FormatterConfig[];
|
|
13
|
+
};
|
|
14
|
+
export declare function loadWriteToolConfig(worktree: string): WriteToolConfig;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
requireReadBeforeOverwrite: true,
|
|
5
|
+
allowUnmappedWrites: false,
|
|
6
|
+
formatters: [],
|
|
7
|
+
};
|
|
8
|
+
const BUILT_IN_FORMATTERS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'prettier',
|
|
11
|
+
command: ['npx', 'prettier', '--write', '$FILE'],
|
|
12
|
+
extensions: ['.cjs', '.css', '.html', '.js', '.json', '.jsonc', '.jsx', '.md', '.mjs', '.ts', '.tsx', '.yaml', '.yml'],
|
|
13
|
+
environment: {},
|
|
14
|
+
cwd: 'container',
|
|
15
|
+
},
|
|
16
|
+
{ id: 'gofmt', command: ['gofmt', '-w', '$FILE'], extensions: ['.go'], environment: {}, cwd: 'container' },
|
|
17
|
+
{ id: 'ruff', command: ['ruff', 'format', '$FILE'], extensions: ['.py', '.pyi'], environment: {}, cwd: 'container' },
|
|
18
|
+
];
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function parseBoolean(value, fallback) {
|
|
23
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
24
|
+
}
|
|
25
|
+
function parseStringMap(value) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return {};
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
|
|
29
|
+
}
|
|
30
|
+
function stripJsonComments(input) {
|
|
31
|
+
let output = '';
|
|
32
|
+
let inString = false;
|
|
33
|
+
let stringQuote = '';
|
|
34
|
+
let escaped = false;
|
|
35
|
+
for (let i = 0; i < input.length; i++) {
|
|
36
|
+
const char = input[i];
|
|
37
|
+
const next = input[i + 1];
|
|
38
|
+
if (inString) {
|
|
39
|
+
output += char;
|
|
40
|
+
if (escaped)
|
|
41
|
+
escaped = false;
|
|
42
|
+
else if (char === '\\')
|
|
43
|
+
escaped = true;
|
|
44
|
+
else if (char === stringQuote)
|
|
45
|
+
inString = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === '"' || char === "'") {
|
|
49
|
+
inString = true;
|
|
50
|
+
stringQuote = char;
|
|
51
|
+
output += char;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '/' && next === '/') {
|
|
55
|
+
while (i < input.length && input[i] !== '\n')
|
|
56
|
+
i++;
|
|
57
|
+
output += '\n';
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === '/' && next === '*') {
|
|
61
|
+
i += 2;
|
|
62
|
+
while (i < input.length && !(input[i] === '*' && input[i + 1] === '/'))
|
|
63
|
+
i++;
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
output += char;
|
|
68
|
+
}
|
|
69
|
+
return output.replace(/,\s*([}\]])/g, '$1');
|
|
70
|
+
}
|
|
71
|
+
function readJsonFile(filePath) {
|
|
72
|
+
if (!existsSync(filePath))
|
|
73
|
+
return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(stripJsonComments(readFileSync(filePath, 'utf8')));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function splitCommand(command) {
|
|
82
|
+
const result = [];
|
|
83
|
+
let current = '';
|
|
84
|
+
let quote;
|
|
85
|
+
let escaped = false;
|
|
86
|
+
for (const char of command) {
|
|
87
|
+
if (escaped) {
|
|
88
|
+
current += char;
|
|
89
|
+
escaped = false;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '\\') {
|
|
93
|
+
escaped = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (quote) {
|
|
97
|
+
if (char === quote)
|
|
98
|
+
quote = undefined;
|
|
99
|
+
else
|
|
100
|
+
current += char;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === '"' || char === "'") {
|
|
104
|
+
quote = char;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (/\s/.test(char)) {
|
|
108
|
+
if (current) {
|
|
109
|
+
result.push(current);
|
|
110
|
+
current = '';
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
current += char;
|
|
115
|
+
}
|
|
116
|
+
if (current)
|
|
117
|
+
result.push(current);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function inferExtensionsFromKey(key) {
|
|
121
|
+
const extMatches = key.match(/\.[a-z0-9+_-]+/gi) ?? [];
|
|
122
|
+
return [...new Set(extMatches.map((item) => item.toLowerCase()))];
|
|
123
|
+
}
|
|
124
|
+
function commandArray(value) {
|
|
125
|
+
if (typeof value === 'string')
|
|
126
|
+
return splitCommand(value);
|
|
127
|
+
if (Array.isArray(value))
|
|
128
|
+
return value.map(String).filter(Boolean);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
function parseFormatterEntry(id, item) {
|
|
132
|
+
if (typeof item === 'string' || Array.isArray(item)) {
|
|
133
|
+
const command = commandArray(item);
|
|
134
|
+
if (!command.length)
|
|
135
|
+
return [];
|
|
136
|
+
return [{ id, command, extensions: inferExtensionsFromKey(id), environment: {}, cwd: 'container' }];
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(item) || item.disabled === true)
|
|
139
|
+
return [];
|
|
140
|
+
const command = commandArray(item.command);
|
|
141
|
+
if (command.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const extensions = Array.isArray(item.extensions)
|
|
144
|
+
? item.extensions.map(String).filter(Boolean)
|
|
145
|
+
: inferExtensionsFromKey(id);
|
|
146
|
+
const cwd = item.cwd === 'host' ? 'host' : 'container';
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
id,
|
|
150
|
+
command,
|
|
151
|
+
extensions,
|
|
152
|
+
environment: parseStringMap(item.environment ?? item.env),
|
|
153
|
+
cwd,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
function parseFormatters(value) {
|
|
158
|
+
if (value === true)
|
|
159
|
+
return BUILT_IN_FORMATTERS;
|
|
160
|
+
if (value === false || value == null)
|
|
161
|
+
return [];
|
|
162
|
+
if (typeof value === 'string' || Array.isArray(value))
|
|
163
|
+
return parseFormatterEntry('formatter', value);
|
|
164
|
+
if (!isRecord(value))
|
|
165
|
+
return [];
|
|
166
|
+
if ('command' in value)
|
|
167
|
+
return parseFormatterEntry('formatter', value);
|
|
168
|
+
return Object.entries(value).flatMap(([id, item]) => parseFormatterEntry(id, item));
|
|
169
|
+
}
|
|
170
|
+
function loadOpenCodeFormatters(worktree) {
|
|
171
|
+
const raw = readJsonFile(path.join(worktree, 'opencode.json')) ?? readJsonFile(path.join(worktree, 'opencode.jsonc'));
|
|
172
|
+
if (!raw || !('formatter' in raw))
|
|
173
|
+
return [];
|
|
174
|
+
return parseFormatters(raw.formatter);
|
|
175
|
+
}
|
|
176
|
+
export function loadWriteToolConfig(worktree) {
|
|
177
|
+
const opencodeFormatters = loadOpenCodeFormatters(worktree);
|
|
178
|
+
const configPath = path.join(worktree, '.opencode', 'sandbox.json');
|
|
179
|
+
const raw = readJsonFile(configPath);
|
|
180
|
+
if (!raw)
|
|
181
|
+
return { ...DEFAULT_CONFIG, formatters: opencodeFormatters };
|
|
182
|
+
const write = isRecord(raw.write) ? raw.write : {};
|
|
183
|
+
return {
|
|
184
|
+
requireReadBeforeOverwrite: parseBoolean(write.requireReadBeforeOverwrite, DEFAULT_CONFIG.requireReadBeforeOverwrite),
|
|
185
|
+
allowUnmappedWrites: parseBoolean(write.allowUnmappedWrites, DEFAULT_CONFIG.allowUnmappedWrites),
|
|
186
|
+
formatters: 'formatter' in raw ? parseFormatters(raw.formatter) : opencodeFormatters,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Diagnostic } from 'vscode-languageserver-types';
|
|
2
|
+
import type { PathMapper } from '../path-map.js';
|
|
3
|
+
export type WriteDiagnosticSummary = {
|
|
4
|
+
current: Diagnostic[];
|
|
5
|
+
other: Array<{
|
|
6
|
+
filePath: string;
|
|
7
|
+
relativePath: string;
|
|
8
|
+
diagnostics: Diagnostic[];
|
|
9
|
+
}>;
|
|
10
|
+
output?: string;
|
|
11
|
+
unavailable?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function collectWriteDiagnostics(input: {
|
|
14
|
+
sessionID: string;
|
|
15
|
+
worktree: string;
|
|
16
|
+
paths: PathMapper;
|
|
17
|
+
hostPath: string;
|
|
18
|
+
expectedContent?: string;
|
|
19
|
+
}): Promise<WriteDiagnosticSummary>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
3
|
+
import { formatLspUnavailable, severityLabel } from '../lsp/diagnostics.js';
|
|
4
|
+
import { hostLspManager } from '../lsp/manager.js';
|
|
5
|
+
import { waitForHostFile } from '../lsp/tooling.js';
|
|
6
|
+
const MAX_OTHER_DIAGNOSTIC_FILES = 5;
|
|
7
|
+
function isActionable(diagnostic) {
|
|
8
|
+
return (diagnostic.severity == null ||
|
|
9
|
+
diagnostic.severity === DiagnosticSeverity.Error ||
|
|
10
|
+
diagnostic.severity === DiagnosticSeverity.Warning);
|
|
11
|
+
}
|
|
12
|
+
function diagnosticKey(diagnostic) {
|
|
13
|
+
return JSON.stringify({
|
|
14
|
+
code: diagnostic.code,
|
|
15
|
+
severity: diagnostic.severity,
|
|
16
|
+
message: diagnostic.message,
|
|
17
|
+
source: diagnostic.source,
|
|
18
|
+
range: diagnostic.range,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function dedupeDiagnostics(diagnostics) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
return diagnostics.filter((diagnostic) => {
|
|
24
|
+
const key = diagnosticKey(diagnostic);
|
|
25
|
+
if (seen.has(key))
|
|
26
|
+
return false;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function sortDiagnostics(diagnostics) {
|
|
32
|
+
return [...diagnostics].sort((a, b) => {
|
|
33
|
+
const line = a.range.start.line - b.range.start.line;
|
|
34
|
+
if (line !== 0)
|
|
35
|
+
return line;
|
|
36
|
+
return a.range.start.character - b.range.start.character;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function formatLine(relativePath, diagnostic) {
|
|
40
|
+
const line = diagnostic.range.start.line + 1;
|
|
41
|
+
const character = diagnostic.range.start.character + 1;
|
|
42
|
+
const source = diagnostic.source ? ` [${diagnostic.source}]` : '';
|
|
43
|
+
const code = diagnostic.code != null ? ` (${String(diagnostic.code)})` : '';
|
|
44
|
+
return ` ${severityLabel(diagnostic.severity)} ${relativePath}:${line}:${character}${source}${code}: ${diagnostic.message}`;
|
|
45
|
+
}
|
|
46
|
+
function formatCurrentDiagnostics(relativePath, diagnostics) {
|
|
47
|
+
const actionable = sortDiagnostics(dedupeDiagnostics(diagnostics.filter(isActionable)));
|
|
48
|
+
if (actionable.length === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const hasErrors = actionable.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
51
|
+
const header = hasErrors
|
|
52
|
+
? 'LSP errors detected in this file, please fix:'
|
|
53
|
+
: 'LSP warnings detected in this file, please review:';
|
|
54
|
+
return `${header}\n${actionable.map((diagnostic) => formatLine(relativePath, diagnostic)).join('\n')}`;
|
|
55
|
+
}
|
|
56
|
+
function formatOtherDiagnostics(items) {
|
|
57
|
+
const groups = items
|
|
58
|
+
.map((item) => ({
|
|
59
|
+
...item,
|
|
60
|
+
diagnostics: sortDiagnostics(dedupeDiagnostics(item.diagnostics.filter(isActionable))),
|
|
61
|
+
}))
|
|
62
|
+
.filter((item) => item.diagnostics.length > 0)
|
|
63
|
+
.slice(0, MAX_OTHER_DIAGNOSTIC_FILES);
|
|
64
|
+
if (groups.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
return [
|
|
67
|
+
'LSP errors detected in other files:',
|
|
68
|
+
...groups.flatMap((item) => [
|
|
69
|
+
` ${item.relativePath}:`,
|
|
70
|
+
...item.diagnostics.map((diagnostic) => ` ${formatLine(item.relativePath, diagnostic)}`),
|
|
71
|
+
]),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
function toOtherDiagnostics(input) {
|
|
75
|
+
const currentHostPath = path.resolve(input.currentHostPath);
|
|
76
|
+
return [...input.allDiagnostics.entries()]
|
|
77
|
+
.filter(([filePath]) => path.resolve(filePath) !== currentHostPath)
|
|
78
|
+
.filter(([filePath]) => input.paths.isMapped(filePath))
|
|
79
|
+
.map(([filePath, diagnostics]) => ({
|
|
80
|
+
filePath,
|
|
81
|
+
relativePath: input.paths.toRelative(filePath),
|
|
82
|
+
diagnostics: dedupeDiagnostics(diagnostics),
|
|
83
|
+
}))
|
|
84
|
+
.filter((item) => item.diagnostics.some(isActionable))
|
|
85
|
+
.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
86
|
+
}
|
|
87
|
+
export async function collectWriteDiagnostics(input) {
|
|
88
|
+
if (!input.paths.isMapped(input.hostPath))
|
|
89
|
+
return { current: [], other: [] };
|
|
90
|
+
try {
|
|
91
|
+
await waitForHostFile(input.hostPath, input.expectedContent);
|
|
92
|
+
await hostLspManager.touchFile({
|
|
93
|
+
sessionID: input.sessionID,
|
|
94
|
+
worktree: input.worktree,
|
|
95
|
+
filePath: input.hostPath,
|
|
96
|
+
diagnostics: 'document',
|
|
97
|
+
});
|
|
98
|
+
const current = dedupeDiagnostics(hostLspManager.diagnosticsForFile({
|
|
99
|
+
sessionID: input.sessionID,
|
|
100
|
+
worktree: input.worktree,
|
|
101
|
+
filePath: input.hostPath,
|
|
102
|
+
}));
|
|
103
|
+
const other = toOtherDiagnostics({
|
|
104
|
+
allDiagnostics: hostLspManager.allDiagnostics({ sessionID: input.sessionID, worktree: input.worktree }),
|
|
105
|
+
currentHostPath: input.hostPath,
|
|
106
|
+
paths: input.paths,
|
|
107
|
+
});
|
|
108
|
+
const output = [
|
|
109
|
+
formatCurrentDiagnostics(input.paths.toRelative(input.hostPath), current),
|
|
110
|
+
formatOtherDiagnostics(other),
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join('\n\n');
|
|
114
|
+
return { current, other, output: output || undefined };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const unavailable = formatLspUnavailable(error);
|
|
118
|
+
return { current: [], other: [], output: unavailable, unavailable };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createTwoFilesPatch } from 'diff';
|
|
2
|
+
const MAX_DIFF_LINES = 400;
|
|
3
|
+
const MAX_DIFF_CHARS = 30000;
|
|
4
|
+
function normalizeDiffPath(filePath) {
|
|
5
|
+
return filePath.replace(/\\/g, '/');
|
|
6
|
+
}
|
|
7
|
+
export function trimDiff(diff) {
|
|
8
|
+
if (diff.length <= MAX_DIFF_CHARS)
|
|
9
|
+
return diff;
|
|
10
|
+
const lines = diff.split('\n');
|
|
11
|
+
const head = lines.slice(0, Math.floor(MAX_DIFF_LINES / 2));
|
|
12
|
+
const tail = lines.slice(-Math.floor(MAX_DIFF_LINES / 2));
|
|
13
|
+
const omitted = Math.max(0, lines.length - head.length - tail.length);
|
|
14
|
+
return [...head, `... Diff truncated: ${omitted} lines omitted ...`, ...tail].join('\n').slice(0, MAX_DIFF_CHARS);
|
|
15
|
+
}
|
|
16
|
+
export function createWriteDiff(input) {
|
|
17
|
+
const oldPath = normalizeDiffPath(input.oldPath);
|
|
18
|
+
const newPath = normalizeDiffPath(input.newPath ?? input.oldPath);
|
|
19
|
+
const diff = createTwoFilesPatch(oldPath, newPath, input.oldContent, input.newContent, 'before', 'after');
|
|
20
|
+
return trimDiff(diff || `No textual changes for ${newPath}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Sandbox } from '../types.js';
|
|
2
|
+
import type { PathMapper } from '../path-map.js';
|
|
3
|
+
import type { FormatterConfig } from './config.js';
|
|
4
|
+
export type FormatterResult = {
|
|
5
|
+
formatted: boolean;
|
|
6
|
+
formatter?: string;
|
|
7
|
+
output?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function runConfiguredFormatter(input: {
|
|
10
|
+
sandbox: Sandbox;
|
|
11
|
+
paths: PathMapper;
|
|
12
|
+
worktree: string;
|
|
13
|
+
hostPath: string;
|
|
14
|
+
containerPath: string;
|
|
15
|
+
formatters: FormatterConfig[];
|
|
16
|
+
}): Promise<FormatterResult>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
function shellQuote(value) {
|
|
6
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
function replaceTokens(arg, input) {
|
|
9
|
+
return arg
|
|
10
|
+
.replace(/\$HOST_FILE/g, input.hostPath)
|
|
11
|
+
.replace(/\$CONTAINER_FILE/g, input.containerPath)
|
|
12
|
+
.replace(/\$FILE/g, input.filePath);
|
|
13
|
+
}
|
|
14
|
+
function matchesFormatter(formatter, hostPath) {
|
|
15
|
+
if (formatter.extensions.length === 0)
|
|
16
|
+
return true;
|
|
17
|
+
const ext = path.extname(hostPath).toLowerCase();
|
|
18
|
+
return formatter.extensions.map((item) => item.toLowerCase()).includes(ext);
|
|
19
|
+
}
|
|
20
|
+
function commandFor(formatter, input) {
|
|
21
|
+
const filePath = formatter.cwd === 'host' ? input.hostPath : input.containerPath;
|
|
22
|
+
return formatter.command.map((arg) => replaceTokens(arg, { ...input, filePath }));
|
|
23
|
+
}
|
|
24
|
+
async function runHostFormatter(formatter, input) {
|
|
25
|
+
const command = commandFor(formatter, { hostPath: input.hostPath, containerPath: input.containerPath });
|
|
26
|
+
const [file, ...args] = command;
|
|
27
|
+
const result = await execFileAsync(file, args, {
|
|
28
|
+
cwd: input.worktree,
|
|
29
|
+
env: { ...process.env, ...formatter.environment },
|
|
30
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
31
|
+
});
|
|
32
|
+
return `${result.stdout}${result.stderr}`.trim();
|
|
33
|
+
}
|
|
34
|
+
async function runContainerFormatter(formatter, input) {
|
|
35
|
+
const command = commandFor(formatter, { hostPath: input.hostPath, containerPath: input.containerPath });
|
|
36
|
+
const envPrefix = Object.entries(formatter.environment)
|
|
37
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
38
|
+
.join(' ');
|
|
39
|
+
const shellCommand = `${envPrefix ? `${envPrefix} ` : ''}${command.map(shellQuote).join(' ')}`;
|
|
40
|
+
const result = await input.sandbox.process.executeCommand(shellCommand, input.paths.repoPath);
|
|
41
|
+
if (result.exitCode !== 0)
|
|
42
|
+
throw new Error(result.result.trim() || `Formatter ${formatter.id} failed`);
|
|
43
|
+
return result.result.trim();
|
|
44
|
+
}
|
|
45
|
+
export async function runConfiguredFormatter(input) {
|
|
46
|
+
const formatter = input.formatters.find((item) => matchesFormatter(item, input.hostPath));
|
|
47
|
+
if (!formatter)
|
|
48
|
+
return { formatted: false };
|
|
49
|
+
const output = formatter.cwd === 'host' ? await runHostFormatter(formatter, input) : await runContainerFormatter(formatter, input);
|
|
50
|
+
return { formatted: true, formatter: formatter.id, output };
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { splitBom, joinBom, decodeUtf8, encodeUtf8 } from './bom.js';
|
|
2
|
+
export { loadWriteToolConfig } from './config.js';
|
|
3
|
+
export { createWriteDiff, trimDiff } from './diff.js';
|
|
4
|
+
export { readRegistry } from './read-tracker.js';
|
|
5
|
+
export { sandboxWriteFile, sandboxDeleteFile } from './pipeline.js';
|
|
6
|
+
export type { SandboxWriteResult, SandboxWriteOperation } from './pipeline.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { splitBom, joinBom, decodeUtf8, encodeUtf8 } from './bom.js';
|
|
2
|
+
export { loadWriteToolConfig } from './config.js';
|
|
3
|
+
export { createWriteDiff, trimDiff } from './diff.js';
|
|
4
|
+
export { readRegistry } from './read-tracker.js';
|
|
5
|
+
export { sandboxWriteFile, sandboxDeleteFile } from './pipeline.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
2
|
+
export declare function requestEditPermission(input: {
|
|
3
|
+
ctx: ToolContext;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
hostPath: string;
|
|
6
|
+
containerPath: string;
|
|
7
|
+
diff: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function setToolMetadata(input: {
|
|
10
|
+
ctx: ToolContext;
|
|
11
|
+
title: string;
|
|
12
|
+
metadata: Record<string, unknown>;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function requestEditPermission(input) {
|
|
2
|
+
if (typeof input.ctx.ask !== 'function')
|
|
3
|
+
return;
|
|
4
|
+
await input.ctx.ask({
|
|
5
|
+
permission: 'edit',
|
|
6
|
+
patterns: [input.relativePath],
|
|
7
|
+
always: ['*'],
|
|
8
|
+
metadata: {
|
|
9
|
+
filepath: input.hostPath,
|
|
10
|
+
containerPath: input.containerPath,
|
|
11
|
+
diff: input.diff,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function setToolMetadata(input) {
|
|
16
|
+
if (typeof input.ctx.metadata !== 'function')
|
|
17
|
+
return;
|
|
18
|
+
input.ctx.metadata({ title: input.title, metadata: input.metadata });
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ToolContext } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import type { Sandbox } from '../types.js';
|
|
3
|
+
import type { PathMapper } from '../path-map.js';
|
|
4
|
+
import { type WriteDiagnosticSummary } from './diagnostics.js';
|
|
5
|
+
import { type FormatterResult } from './formatter.js';
|
|
6
|
+
export type SandboxWriteOperation = 'write' | 'edit' | 'multiedit' | 'patch';
|
|
7
|
+
export type SandboxWriteResult = {
|
|
8
|
+
hostPath: string;
|
|
9
|
+
containerPath: string;
|
|
10
|
+
relativePath: string;
|
|
11
|
+
exists: boolean;
|
|
12
|
+
diff: string;
|
|
13
|
+
finalContent: string;
|
|
14
|
+
formatted: FormatterResult;
|
|
15
|
+
diagnostics: WriteDiagnosticSummary;
|
|
16
|
+
};
|
|
17
|
+
export declare function sandboxWriteFile(input: {
|
|
18
|
+
ctx: ToolContext;
|
|
19
|
+
sandbox: Sandbox;
|
|
20
|
+
sessionID: string;
|
|
21
|
+
worktree: string;
|
|
22
|
+
paths: PathMapper;
|
|
23
|
+
filePath: string;
|
|
24
|
+
content: string;
|
|
25
|
+
operation: SandboxWriteOperation;
|
|
26
|
+
oldContentBuffer?: Buffer;
|
|
27
|
+
oldExists?: boolean;
|
|
28
|
+
diffOldPath?: string;
|
|
29
|
+
requireFreshRead?: boolean;
|
|
30
|
+
skipPermission?: boolean;
|
|
31
|
+
setMetadata?: boolean;
|
|
32
|
+
}): Promise<SandboxWriteResult>;
|
|
33
|
+
export declare function sandboxDeleteFile(input: {
|
|
34
|
+
ctx: ToolContext;
|
|
35
|
+
sandbox: Sandbox;
|
|
36
|
+
sessionID: string;
|
|
37
|
+
worktree: string;
|
|
38
|
+
paths: PathMapper;
|
|
39
|
+
filePath: string;
|
|
40
|
+
skipPermission?: boolean;
|
|
41
|
+
setMetadata?: boolean;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
hostPath: string;
|
|
44
|
+
containerPath: string;
|
|
45
|
+
relativePath: string;
|
|
46
|
+
exists: boolean;
|
|
47
|
+
diff: string;
|
|
48
|
+
}>;
|