@xcanwin/manyoyo 5.9.3 → 5.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/bin/manyoyo.js +51 -7
- package/lib/image-build.js +160 -0
- package/lib/runtime-resolver.js +25 -1
- package/lib/web/frontend/app.css +121 -55
- package/lib/web/frontend/app.html +12 -6
- package/lib/web/frontend/app.js +214 -34
- package/lib/web/frontend/codemirror-entry.js +13 -0
- package/lib/web/frontend/codemirror.bundle.js +13 -0
- package/lib/web/frontend/file-browser.js +220 -29
- package/lib/web/server.js +179 -10
- package/lib/worktrees.js +132 -0
- package/package.json +2 -1
package/lib/web/server.js
CHANGED
|
@@ -34,7 +34,8 @@ const WEB_TERMINAL_MIN_ROWS = 12;
|
|
|
34
34
|
const WEB_AGENT_CONTEXT_MAX_MESSAGES = 24;
|
|
35
35
|
const WEB_AGENT_CONTEXT_MAX_CHARS = 6000;
|
|
36
36
|
const WEB_AGENT_CONTEXT_PER_MESSAGE_MAX_CHARS = 600;
|
|
37
|
-
const WEB_FILE_PREVIEW_MAX_BYTES =
|
|
37
|
+
const WEB_FILE_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
38
|
+
const WEB_FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
|
|
38
39
|
const WEB_AUTH_COOKIE_NAME = 'manyoyo_web_auth';
|
|
39
40
|
const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
|
|
40
41
|
const WEB_SESSION_KEY_SEPARATOR = '~';
|
|
@@ -2657,7 +2658,12 @@ async function execCommandInWebContainer(ctx, containerName, command, options =
|
|
|
2657
2658
|
const extractedAgentMessage = extractAgentMessageFromStructuredOutput(agentProgram, clippedStdout);
|
|
2658
2659
|
const cleanOutputSource = extractedAgentMessage || clippedRaw;
|
|
2659
2660
|
const output = clipText(stripAnsi(cleanOutputSource).trim() || '(无输出)');
|
|
2660
|
-
resolve({
|
|
2661
|
+
resolve({
|
|
2662
|
+
exitCode,
|
|
2663
|
+
output,
|
|
2664
|
+
stdout: clippedStdout,
|
|
2665
|
+
stderr: clippedStderr
|
|
2666
|
+
});
|
|
2661
2667
|
});
|
|
2662
2668
|
});
|
|
2663
2669
|
}
|
|
@@ -2679,7 +2685,7 @@ async function execJsonCommandInWebContainer(ctx, containerName, command) {
|
|
|
2679
2685
|
throw new Error(result.output || '容器命令执行失败');
|
|
2680
2686
|
}
|
|
2681
2687
|
try {
|
|
2682
|
-
return JSON.parse(String(result.
|
|
2688
|
+
return JSON.parse(String(result.stdout || '{}'));
|
|
2683
2689
|
} catch (e) {
|
|
2684
2690
|
throw new Error('容器返回了无法解析的 JSON');
|
|
2685
2691
|
}
|
|
@@ -2748,13 +2754,17 @@ try {
|
|
|
2748
2754
|
`);
|
|
2749
2755
|
}
|
|
2750
2756
|
|
|
2751
|
-
function buildContainerFileReadCommand(requestedPath) {
|
|
2757
|
+
function buildContainerFileReadCommand(requestedPath, options = {}) {
|
|
2758
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
2759
|
+
const maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes > 0
|
|
2760
|
+
? Math.floor(opts.maxBytes)
|
|
2761
|
+
: 0;
|
|
2752
2762
|
return buildWebContainerNodeCommand(`
|
|
2753
2763
|
// __MANYOYO_FS_READ__
|
|
2754
2764
|
const fs = require('fs');
|
|
2755
2765
|
|
|
2756
2766
|
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2757
|
-
const maxBytes = ${String(
|
|
2767
|
+
const maxBytes = ${String(maxBytes)};
|
|
2758
2768
|
|
|
2759
2769
|
function looksBinary(buffer) {
|
|
2760
2770
|
const length = Math.min(buffer.length, 4096);
|
|
@@ -2779,7 +2789,7 @@ try {
|
|
|
2779
2789
|
}
|
|
2780
2790
|
|
|
2781
2791
|
const size = stat.size;
|
|
2782
|
-
const readBytes = Math.min(size, maxBytes);
|
|
2792
|
+
const readBytes = maxBytes > 0 ? Math.min(size, maxBytes) : size;
|
|
2783
2793
|
const buffer = Buffer.alloc(readBytes);
|
|
2784
2794
|
const fd = fs.openSync(realPath, 'r');
|
|
2785
2795
|
try {
|
|
@@ -2793,14 +2803,14 @@ try {
|
|
|
2793
2803
|
path: realPath,
|
|
2794
2804
|
kind: 'binary',
|
|
2795
2805
|
size,
|
|
2796
|
-
truncated: size > maxBytes
|
|
2806
|
+
truncated: maxBytes > 0 && size > maxBytes
|
|
2797
2807
|
}));
|
|
2798
2808
|
} else {
|
|
2799
2809
|
process.stdout.write(JSON.stringify({
|
|
2800
2810
|
path: realPath,
|
|
2801
2811
|
kind: 'text',
|
|
2802
2812
|
size,
|
|
2803
|
-
truncated: size > maxBytes,
|
|
2813
|
+
truncated: maxBytes > 0 && size > maxBytes,
|
|
2804
2814
|
content: buffer.toString('utf8')
|
|
2805
2815
|
}));
|
|
2806
2816
|
}
|
|
@@ -2812,6 +2822,71 @@ try {
|
|
|
2812
2822
|
`);
|
|
2813
2823
|
}
|
|
2814
2824
|
|
|
2825
|
+
function buildContainerFileWriteCommand(requestedPath, content) {
|
|
2826
|
+
return buildWebContainerNodeCommand(`
|
|
2827
|
+
// __MANYOYO_FS_WRITE__
|
|
2828
|
+
const fs = require('fs');
|
|
2829
|
+
|
|
2830
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2831
|
+
const nextContent = ${JSON.stringify(String(content == null ? '' : content))};
|
|
2832
|
+
|
|
2833
|
+
try {
|
|
2834
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
2835
|
+
const stat = fs.statSync(realPath);
|
|
2836
|
+
if (!stat.isFile()) {
|
|
2837
|
+
throw new Error('目标不是文件: ' + realPath);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
fs.writeFileSync(realPath, nextContent, 'utf8');
|
|
2841
|
+
const savedStat = fs.statSync(realPath);
|
|
2842
|
+
process.stdout.write(JSON.stringify({
|
|
2843
|
+
path: realPath,
|
|
2844
|
+
saved: true,
|
|
2845
|
+
size: savedStat.size
|
|
2846
|
+
}));
|
|
2847
|
+
} catch (e) {
|
|
2848
|
+
process.stdout.write(JSON.stringify({
|
|
2849
|
+
error: e && e.message ? e.message : '保存文件失败'
|
|
2850
|
+
}));
|
|
2851
|
+
}
|
|
2852
|
+
`);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function buildContainerFileMkdirCommand(requestedPath) {
|
|
2856
|
+
return buildWebContainerNodeCommand(`
|
|
2857
|
+
// __MANYOYO_FS_MKDIR__
|
|
2858
|
+
const fs = require('fs');
|
|
2859
|
+
const path = require('path');
|
|
2860
|
+
|
|
2861
|
+
const requestedPath = ${JSON.stringify(String(requestedPath || ''))};
|
|
2862
|
+
|
|
2863
|
+
try {
|
|
2864
|
+
const resolvedPath = path.resolve(requestedPath);
|
|
2865
|
+
const parentPath = path.dirname(resolvedPath);
|
|
2866
|
+
const realParentPath = fs.realpathSync(parentPath);
|
|
2867
|
+
const targetPath = path.join(realParentPath, path.basename(resolvedPath));
|
|
2868
|
+
if (fs.existsSync(targetPath)) {
|
|
2869
|
+
throw new Error('目录已存在: ' + targetPath);
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
2873
|
+
const stat = fs.statSync(targetPath);
|
|
2874
|
+
process.stdout.write(JSON.stringify({
|
|
2875
|
+
path: targetPath,
|
|
2876
|
+
name: path.basename(targetPath),
|
|
2877
|
+
kind: 'directory',
|
|
2878
|
+
size: 0,
|
|
2879
|
+
mtimeMs: stat.mtimeMs,
|
|
2880
|
+
created: true
|
|
2881
|
+
}));
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
process.stdout.write(JSON.stringify({
|
|
2884
|
+
error: e && e.message ? e.message : '创建目录失败'
|
|
2885
|
+
}));
|
|
2886
|
+
}
|
|
2887
|
+
`);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2815
2890
|
async function execAgentInWebContainerStream(ctx, state, sessionRefOrContainerName, command, options = {}) {
|
|
2816
2891
|
const opts = options && typeof options === 'object' ? options : {};
|
|
2817
2892
|
const sessionRef = typeof sessionRefOrContainerName === 'string'
|
|
@@ -3090,7 +3165,12 @@ function buildSessionSummary(ctx, state, containerMap, sessionRef) {
|
|
|
3090
3165
|
defaultCommand: containerInfo.defaultCommand || ''
|
|
3091
3166
|
});
|
|
3092
3167
|
const createdAt = agentSession.createdAt || containerInfo.createdAt || null;
|
|
3093
|
-
const updatedAt = agentSession.updatedAt
|
|
3168
|
+
const updatedAt = agentSession.updatedAt
|
|
3169
|
+
|| (latestMessage && latestMessage.timestamp)
|
|
3170
|
+
|| (agentId === WEB_DEFAULT_AGENT_ID
|
|
3171
|
+
? (history.updatedAt || containerInfo.createdAt)
|
|
3172
|
+
: null)
|
|
3173
|
+
|| null;
|
|
3094
3174
|
return {
|
|
3095
3175
|
name: buildWebSessionKey(containerName, agentId),
|
|
3096
3176
|
containerName,
|
|
@@ -3583,6 +3663,25 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3583
3663
|
});
|
|
3584
3664
|
}
|
|
3585
3665
|
},
|
|
3666
|
+
{
|
|
3667
|
+
method: 'POST',
|
|
3668
|
+
match: currentPath => currentPath === '/api/fs/directories/mkdir' ? [] : null,
|
|
3669
|
+
handler: async () => {
|
|
3670
|
+
const payload = await readJsonBody(req);
|
|
3671
|
+
const requestedPath = expandHomeAliasPath(String(payload && payload.path ? payload.path : '').trim());
|
|
3672
|
+
if (!requestedPath) {
|
|
3673
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
const targetPath = path.resolve(requestedPath);
|
|
3678
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
3679
|
+
sendJson(res, 200, {
|
|
3680
|
+
path: targetPath,
|
|
3681
|
+
created: true
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
},
|
|
3586
3685
|
{
|
|
3587
3686
|
method: 'GET',
|
|
3588
3687
|
match: currentPath => currentPath === '/api/config' ? [] : null,
|
|
@@ -3742,6 +3841,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3742
3841
|
}
|
|
3743
3842
|
const requestUrl = new URL(req.url || '/api/sessions/x/fs/read', 'http://localhost');
|
|
3744
3843
|
const targetPath = String(requestUrl.searchParams.get('path') || '').trim();
|
|
3844
|
+
const fullRequested = ['1', 'true', 'yes'].includes(String(requestUrl.searchParams.get('full') || '').toLowerCase());
|
|
3745
3845
|
if (!targetPath) {
|
|
3746
3846
|
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3747
3847
|
return;
|
|
@@ -3751,7 +3851,9 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3751
3851
|
const payload = await execJsonCommandInWebContainer(
|
|
3752
3852
|
ctx,
|
|
3753
3853
|
sessionRef.containerName,
|
|
3754
|
-
buildContainerFileReadCommand(targetPath
|
|
3854
|
+
buildContainerFileReadCommand(targetPath, {
|
|
3855
|
+
maxBytes: fullRequested ? 0 : WEB_FILE_PREVIEW_MAX_BYTES
|
|
3856
|
+
})
|
|
3755
3857
|
);
|
|
3756
3858
|
if (payload && payload.error) {
|
|
3757
3859
|
sendJson(res, 400, { error: payload.error });
|
|
@@ -3759,10 +3861,77 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
3759
3861
|
}
|
|
3760
3862
|
if (payload && payload.kind === 'text') {
|
|
3761
3863
|
payload.language = inferFileLanguage(payload.path);
|
|
3864
|
+
payload.editable = payload.truncated !== true
|
|
3865
|
+
&& Number(payload.size || 0) < WEB_FILE_EDIT_MAX_BYTES;
|
|
3762
3866
|
}
|
|
3763
3867
|
sendJson(res, 200, payload);
|
|
3764
3868
|
}
|
|
3765
3869
|
},
|
|
3870
|
+
{
|
|
3871
|
+
method: 'PUT',
|
|
3872
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/write$/),
|
|
3873
|
+
handler: async match => {
|
|
3874
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3875
|
+
if (!sessionRef) {
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
const payload = await readJsonBody(req);
|
|
3879
|
+
const targetPath = String(payload && payload.path ? payload.path : '').trim();
|
|
3880
|
+
const content = typeof payload.content === 'string' ? payload.content : null;
|
|
3881
|
+
if (!targetPath) {
|
|
3882
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (content === null) {
|
|
3886
|
+
sendJson(res, 400, { error: 'content 必须是字符串' });
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
if (Buffer.byteLength(content, 'utf8') >= WEB_FILE_EDIT_MAX_BYTES) {
|
|
3890
|
+
sendJson(res, 400, { error: '文件过大,当前仅支持编辑小于 2MB 的文本文件' });
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3895
|
+
const result = await execJsonCommandInWebContainer(
|
|
3896
|
+
ctx,
|
|
3897
|
+
sessionRef.containerName,
|
|
3898
|
+
buildContainerFileWriteCommand(targetPath, content)
|
|
3899
|
+
);
|
|
3900
|
+
if (result && result.error) {
|
|
3901
|
+
sendJson(res, 400, { error: result.error });
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
sendJson(res, 200, result);
|
|
3905
|
+
}
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
method: 'POST',
|
|
3909
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/fs\/mkdir$/),
|
|
3910
|
+
handler: async match => {
|
|
3911
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
3912
|
+
if (!sessionRef) {
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
const payload = await readJsonBody(req);
|
|
3916
|
+
const targetPath = String(payload && payload.path ? payload.path : '').trim();
|
|
3917
|
+
if (!targetPath) {
|
|
3918
|
+
sendJson(res, 400, { error: 'path 不能为空' });
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
await ensureWebContainer(ctx, state, sessionRef.containerName, sessionRef);
|
|
3923
|
+
const result = await execJsonCommandInWebContainer(
|
|
3924
|
+
ctx,
|
|
3925
|
+
sessionRef.containerName,
|
|
3926
|
+
buildContainerFileMkdirCommand(targetPath)
|
|
3927
|
+
);
|
|
3928
|
+
if (result && result.error) {
|
|
3929
|
+
sendJson(res, 400, { error: result.error });
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
sendJson(res, 200, result);
|
|
3933
|
+
}
|
|
3934
|
+
},
|
|
3766
3935
|
{
|
|
3767
3936
|
method: 'GET',
|
|
3768
3937
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/detail$/),
|
package/lib/worktrees.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function defaultRunGitCommand(targetPath, args) {
|
|
8
|
+
const result = spawnSync('git', ['-C', targetPath, ...args], {
|
|
9
|
+
encoding: 'utf-8'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
if (result.status !== 0) {
|
|
13
|
+
const stderr = String(result.stderr || '').trim();
|
|
14
|
+
throw new Error(stderr || `git ${args.join(' ')} 执行失败`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return String(result.stdout || '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeAbsolutePath(targetPath) {
|
|
21
|
+
return path.resolve(String(targetPath || '').trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isDescendantPath(parentPath, targetPath) {
|
|
25
|
+
const relativePath = path.relative(parentPath, targetPath);
|
|
26
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectGitWorktreeContext(targetPath, deps = {}) {
|
|
30
|
+
const fsApi = deps.fs || fs;
|
|
31
|
+
const pathApi = deps.path || path;
|
|
32
|
+
const runGitCommand = deps.runGitCommand || defaultRunGitCommand;
|
|
33
|
+
const absoluteTargetPath = normalizeAbsolutePath(targetPath);
|
|
34
|
+
|
|
35
|
+
if (!fsApi.existsSync(absoluteTargetPath)) {
|
|
36
|
+
throw new Error(`启用 --worktrees 时宿主机路径不存在: ${absoluteTargetPath}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stats = fsApi.statSync(absoluteTargetPath);
|
|
40
|
+
if (!stats.isDirectory()) {
|
|
41
|
+
throw new Error(`启用 --worktrees 时宿主机路径必须为目录: ${absoluteTargetPath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let repoRoot;
|
|
45
|
+
let commonDirRaw;
|
|
46
|
+
try {
|
|
47
|
+
repoRoot = pathApi.resolve(runGitCommand(absoluteTargetPath, ['rev-parse', '--show-toplevel']));
|
|
48
|
+
commonDirRaw = runGitCommand(absoluteTargetPath, ['rev-parse', '--git-common-dir']);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(`启用 --worktrees 失败: ${absoluteTargetPath} 不在 Git 仓库内`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const commonDir = pathApi.isAbsolute(commonDirRaw)
|
|
54
|
+
? pathApi.normalize(commonDirRaw)
|
|
55
|
+
: pathApi.resolve(repoRoot, commonDirRaw);
|
|
56
|
+
const mainRepoRoot = pathApi.dirname(commonDir);
|
|
57
|
+
const projectName = pathApi.basename(mainRepoRoot);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
targetPath: absoluteTargetPath,
|
|
61
|
+
repoRoot,
|
|
62
|
+
mainRepoRoot,
|
|
63
|
+
isWorktree: repoRoot !== mainRepoRoot,
|
|
64
|
+
projectName,
|
|
65
|
+
defaultWorktreesRoot: pathApi.join(pathApi.dirname(mainRepoRoot), 'worktrees', projectName)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shouldAddSamePathMount(hostPath, containerPath, targetPath) {
|
|
70
|
+
if (hostPath !== containerPath) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return !isDescendantPath(hostPath, targetPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveWorktreeSupport(options = {}, deps = {}) {
|
|
77
|
+
const fsApi = deps.fs || fs;
|
|
78
|
+
const pathApi = deps.path || path;
|
|
79
|
+
const enabled = options.enabled === true || Boolean(options.worktreesRoot);
|
|
80
|
+
|
|
81
|
+
if (!enabled) {
|
|
82
|
+
return {
|
|
83
|
+
enabled: false,
|
|
84
|
+
worktreesRoot: null,
|
|
85
|
+
worktreeRepoRoot: null,
|
|
86
|
+
worktreeMainRepoRoot: null,
|
|
87
|
+
extraVolumes: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const hostPath = normalizeAbsolutePath(options.hostPath);
|
|
92
|
+
const containerPath = String(options.containerPath || hostPath).trim() || hostPath;
|
|
93
|
+
const detected = detectGitWorktreeContext(hostPath, deps);
|
|
94
|
+
let worktreesRoot = options.worktreesRoot;
|
|
95
|
+
|
|
96
|
+
if (worktreesRoot !== undefined && worktreesRoot !== null && String(worktreesRoot).trim() !== '') {
|
|
97
|
+
if (!pathApi.isAbsolute(worktreesRoot)) {
|
|
98
|
+
throw new Error(`--worktrees-root 仅支持绝对路径: ${worktreesRoot}`);
|
|
99
|
+
}
|
|
100
|
+
worktreesRoot = pathApi.resolve(worktreesRoot);
|
|
101
|
+
} else {
|
|
102
|
+
worktreesRoot = detected.defaultWorktreesRoot;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fsApi.mkdirSync(worktreesRoot, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const existingVolumes = new Set((options.volumes || []).map(item => String(item)));
|
|
108
|
+
const extraVolumes = [];
|
|
109
|
+
[detected.mainRepoRoot, worktreesRoot].forEach(targetPath => {
|
|
110
|
+
if (!shouldAddSamePathMount(hostPath, containerPath, targetPath)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const volume = `${targetPath}:${targetPath}`;
|
|
114
|
+
if (existingVolumes.has(volume) || extraVolumes.includes(volume)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
extraVolumes.push(volume);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
enabled: true,
|
|
122
|
+
worktreesRoot,
|
|
123
|
+
worktreeRepoRoot: detected.repoRoot,
|
|
124
|
+
worktreeMainRepoRoot: detected.mainRepoRoot,
|
|
125
|
+
extraVolumes
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
detectGitWorktreeContext,
|
|
131
|
+
resolveWorktreeSupport
|
|
132
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.3",
|
|
4
4
|
"imageVersion": "1.9.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.1",
|
|
6
6
|
"description": "AI Agent CLI Security Sandbox for Docker and Podman",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"esbuild": "^0.25.12",
|
|
88
88
|
"glob": "^13.0.6",
|
|
89
89
|
"minimatch": "^10.2.2",
|
|
90
|
+
"postcss": "^8.5.10",
|
|
90
91
|
"test-exclude": "^8.0.0",
|
|
91
92
|
"vite": "^6.4.2"
|
|
92
93
|
},
|