@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.
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: session-folders
3
+ name: '@baixiangguo/dsh-session-folders'
4
+ config: {}
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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Session Working Directory",
4
+ "description": "Shows 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
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "会话工作目录",
4
+ "description": "显示每个会话的工作目录(一键打开文件管理器),并可给工作区文件夹标色、起别名、按颜色筛选;选中行的高亮迁移到文字,避免盖住自定义色。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
- {
2
- "name": "@baixiangguo/dsh-session-folders",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }