@baixiangguo/dsh-session-folders 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +32 -2
- package/client.js +1202 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +5 -0
- package/index.js +189 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -6
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="12" fill="#247bbf"/>
|
|
3
|
+
<path d="M14 22c0-2.2 1.8-4 4-4h7l4 4.5h17c2.2 0 4 1.8 4 4v13c0 2.2-1.8 4-4 4H18c-2.2 0-4-1.8-4-4V22z" fill="none" stroke="#fff" stroke-width="3.5" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M22 44l5-3 5 3" fill="none" stroke="#ffd65c" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of session-cwd-opener.
|
|
3
|
+
* Serves POST /session-cwd-opener/open as a fallback opener when the
|
|
4
|
+
* official open-in-app routes are unavailable. Security mirrors
|
|
5
|
+
* dsh-host-open-in-app: connection trust fence first, then bounded JSON
|
|
6
|
+
* body, absolute-path + isDirectory checks, then a detached file-manager
|
|
7
|
+
* launch (explorer / open / xdg-open).
|
|
8
|
+
*/
|
|
9
|
+
import { isAbsolute } from 'node:path';
|
|
10
|
+
import { stat } from 'node:fs/promises';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { platform } from 'node:os';
|
|
13
|
+
|
|
14
|
+
export const name = 'session-cwd-opener';
|
|
15
|
+
export const inject = ['webServer', 'connection', 'workspaceRegistry'];
|
|
16
|
+
|
|
17
|
+
const OPEN_PATH = '/session-cwd-opener/open';
|
|
18
|
+
const WORKSPACES_PATH = '/session-cwd-opener/workspaces';
|
|
19
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
function connectionOf(ctx) {
|
|
22
|
+
return ctx.connection ?? ctx.get?.('connection');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sendJson(res, status, payload) {
|
|
26
|
+
res.statusCode = status;
|
|
27
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
28
|
+
res.setHeader('cache-control', 'no-store');
|
|
29
|
+
res.end(JSON.stringify(payload));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readBoundedBody(req) {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let size = 0;
|
|
35
|
+
for await (const chunk of req) {
|
|
36
|
+
size += chunk.byteLength;
|
|
37
|
+
if (size > MAX_BODY_BYTES) {
|
|
38
|
+
req.resume();
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
}
|
|
43
|
+
return Buffer.concat(chunks, size).toString('utf8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function launchFileManager(path) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let cmd;
|
|
49
|
+
let args;
|
|
50
|
+
const p = platform();
|
|
51
|
+
if (p === 'win32') {
|
|
52
|
+
cmd = 'explorer.exe';
|
|
53
|
+
args = [path];
|
|
54
|
+
} else if (p === 'darwin') {
|
|
55
|
+
cmd = 'open';
|
|
56
|
+
args = [path];
|
|
57
|
+
} else {
|
|
58
|
+
cmd = 'xdg-open';
|
|
59
|
+
args = [path];
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
63
|
+
child.on('error', () => resolve(false));
|
|
64
|
+
child.unref();
|
|
65
|
+
// Give the OS a moment; explorer/open rarely fails synchronously.
|
|
66
|
+
setTimeout(() => resolve(true), 300);
|
|
67
|
+
} catch {
|
|
68
|
+
resolve(false);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function workspaceRegistryOf(ctx) {
|
|
74
|
+
return ctx.workspaceRegistry ?? ctx.get?.('workspaceRegistry');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function apply(ctx, _config) {
|
|
78
|
+
ctx.effect(
|
|
79
|
+
() =>
|
|
80
|
+
ctx.webServer.register(
|
|
81
|
+
{
|
|
82
|
+
kind: 'exact',
|
|
83
|
+
path: WORKSPACES_PATH,
|
|
84
|
+
handler: async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const rejection = connectionOf(ctx)?.requestRejection?.(req);
|
|
87
|
+
if (rejection !== undefined) {
|
|
88
|
+
res.statusCode = rejection;
|
|
89
|
+
res.end();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
sendJson(res, 403, { code: 'forbidden', message: 'rejected' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (req.method !== 'GET') {
|
|
97
|
+
res.statusCode = 405;
|
|
98
|
+
res.setHeader('allow', 'GET');
|
|
99
|
+
res.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let items = [];
|
|
103
|
+
try {
|
|
104
|
+
const list = workspaceRegistryOf(ctx)?.list?.() ?? [];
|
|
105
|
+
items = (Array.isArray(list) ? list : []).map((w) => ({
|
|
106
|
+
id: typeof w.workspaceId === 'string' ? w.workspaceId : String(w.workspaceId ?? ''),
|
|
107
|
+
path: typeof w.path === 'string' ? w.path : '',
|
|
108
|
+
title: typeof w.title === 'string' ? w.title : '',
|
|
109
|
+
sessionIds: Array.isArray(w.sessionIds) ? w.sessionIds.filter((v) => typeof v === 'string') : [],
|
|
110
|
+
}));
|
|
111
|
+
} catch {
|
|
112
|
+
items = [];
|
|
113
|
+
}
|
|
114
|
+
sendJson(res, 200, { workspaces: items });
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
'session-cwd-opener: GET /session-cwd-opener/workspaces',
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
ctx.effect(
|
|
121
|
+
() =>
|
|
122
|
+
ctx.webServer.register(
|
|
123
|
+
{
|
|
124
|
+
kind: 'exact',
|
|
125
|
+
path: OPEN_PATH,
|
|
126
|
+
handler: async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const rejection = connectionOf(ctx)?.requestRejection?.(req);
|
|
129
|
+
if (rejection !== undefined) {
|
|
130
|
+
res.statusCode = rejection;
|
|
131
|
+
res.end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// If the fence itself fails, refuse rather than open.
|
|
136
|
+
sendJson(res, 403, { code: 'forbidden', message: 'rejected' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (req.method !== 'POST') {
|
|
140
|
+
res.statusCode = 405;
|
|
141
|
+
res.setHeader('allow', 'POST');
|
|
142
|
+
res.end();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (String(req.headers['content-type']).split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
146
|
+
sendJson(res, 415, { code: 'unsupported-media-type', message: 'content-type must be application/json' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = await readBoundedBody(req);
|
|
152
|
+
} catch {
|
|
153
|
+
sendJson(res, 400, { code: 'bad-request', message: 'request body unreadable' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (text === null) {
|
|
157
|
+
sendJson(res, 413, { code: 'payload-too-large', message: 'request body is too large' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let body;
|
|
161
|
+
try {
|
|
162
|
+
body = JSON.parse(text);
|
|
163
|
+
} catch {
|
|
164
|
+
body = null;
|
|
165
|
+
}
|
|
166
|
+
const path = body && typeof body === 'object' ? body.path : undefined;
|
|
167
|
+
if (typeof path !== 'string' || path === '' || !isAbsolute(path)) {
|
|
168
|
+
sendJson(res, 400, { code: 'bad-request', message: 'path must be an absolute directory path' });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let isDir = false;
|
|
172
|
+
try {
|
|
173
|
+
isDir = (await stat(path)).isDirectory();
|
|
174
|
+
} catch {
|
|
175
|
+
isDir = false;
|
|
176
|
+
}
|
|
177
|
+
if (!isDir) {
|
|
178
|
+
sendJson(res, 404, { code: 'not-found', message: `directory does not exist: ${path}` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const ok = await launchFileManager(path);
|
|
182
|
+
if (ok) sendJson(res, 200, { ok: true });
|
|
183
|
+
else sendJson(res, 502, { code: 'launch-failed', message: `failed to open ${path}` });
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
'session-cwd-opener: POST /session-cwd-opener/open',
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@baixiangguo/dsh-session-folders",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@baixiangguo/dsh-session-folders",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Show each session's working directory (click to open in file manager), plus workspace folder colors with aliases, color filtering, and selection highlight migrated to text.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "git+https://github.com/s17765152890-gif/dsh-session-folders.git",
|
|
8
|
+
"type": "git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./client": "./client.js",
|
|
14
|
+
"./locale/*.json": "./locale/*.json",
|
|
15
|
+
"./icon": "./icon.svg"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"client.js",
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"locale/*.json",
|
|
22
|
+
"icon.svg"
|
|
23
|
+
],
|
|
24
|
+
"dsh": {
|
|
25
|
+
"bundle": {
|
|
26
|
+
"patch": "./cordis.patch.yml"
|
|
27
|
+
},
|
|
28
|
+
"client": {
|
|
29
|
+
"platform": "web",
|
|
30
|
+
"immediately": true,
|
|
31
|
+
"inject": [
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|