@lijian-ui/dsh-file-manager 0.2.2 → 0.2.4
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 +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27354 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +0 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +23 -10
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/DockItem.tsx +1 -1
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +45 -17
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -507
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +8 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +62 -8
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +18 -11
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -367
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- package/lib/types/client/mention.d.ts.map +0 -1
- package/src/client/styles/chip.module.css +0 -38
package/src/core/types.ts
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wire vocabulary shared by the host data services and the browser client:
|
|
3
|
-
* the request/response shapes of the /filemgr/* routes and the stable
|
|
4
|
-
* error codes the client maps onto copy. Pure types — no runtime code.
|
|
5
|
-
*
|
|
6
|
-
* The design follows FileManager's right-panel system (Apache-2.0, re-implemented
|
|
7
|
-
* from measured behavior, not copied code): the explorer column is a lazy
|
|
8
|
-
* directory tree, the preview column opens files as tabs, and the SCM tab
|
|
9
|
-
* reads the repository's real working-tree state.
|
|
10
|
-
* @module dsh-filemgr/core/types
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Envelope every /filemgr JSON response carries. */
|
|
14
|
-
export type PanelEnvelope<T> =
|
|
15
|
-
| { ok: true; value: T }
|
|
16
|
-
| { ok: false; error: PanelError }
|
|
17
|
-
|
|
18
|
-
/** Stable rejection codes (host-authored; the client prefers its own copy by code). */
|
|
19
|
-
export type PanelErrorCode =
|
|
20
|
-
| 'workspace-unknown'
|
|
21
|
-
| 'path-outside-root'
|
|
22
|
-
| 'not-found'
|
|
23
|
-
| 'is-directory'
|
|
24
|
-
| 'read-failed'
|
|
25
|
-
| 'write-conflict'
|
|
26
|
-
| 'write-failed'
|
|
27
|
-
| 'search-failed'
|
|
28
|
-
| 'git-unavailable'
|
|
29
|
-
| 'git-failed'
|
|
30
|
-
| 'internal'
|
|
31
|
-
|
|
32
|
-
/** One rejection with a human-readable host message. */
|
|
33
|
-
export interface PanelError {
|
|
34
|
-
code: PanelErrorCode
|
|
35
|
-
message: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** One filesystem entry in a directory listing (path is relative to the root). */
|
|
39
|
-
export interface FsEntry {
|
|
40
|
-
/** Display name (basename). */
|
|
41
|
-
name: string
|
|
42
|
-
/** Path relative to the project root, '/' separated; '' is the root itself. */
|
|
43
|
-
path: string
|
|
44
|
-
isDir: boolean
|
|
45
|
-
/** Size in bytes (0 for directories). */
|
|
46
|
-
size: number
|
|
47
|
-
/** Last-modified epoch millis (0 for directories). */
|
|
48
|
-
mtime: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The listing of one directory. */
|
|
52
|
-
export interface DirListing {
|
|
53
|
-
/** Canonical root path the listing is relative to. */
|
|
54
|
-
root: string
|
|
55
|
-
/** Sorted entries (directories first, then files, both case-insensitive alpha). */
|
|
56
|
-
entries: FsEntry[]
|
|
57
|
-
/** True when the entry count was capped at MAX_ENTRIES (a giant flat dir). */
|
|
58
|
-
truncated: boolean
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The result of reading one file for preview. */
|
|
62
|
-
export interface FileRead {
|
|
63
|
-
/** Text content (decoded utf-8), or a data URL for image kinds. */
|
|
64
|
-
content: string
|
|
65
|
-
/** True when the text was truncated at the preview ceiling. */
|
|
66
|
-
truncated: boolean
|
|
67
|
-
/** Total size in bytes. */
|
|
68
|
-
size: number
|
|
69
|
-
/** Last-modified epoch millis (the write-conflict base). */
|
|
70
|
-
mtime: number
|
|
71
|
-
/** Image dimensions when the file is an image (host decodes via probe). */
|
|
72
|
-
image?: { width: number; height: number }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** One filename-search hit. */
|
|
76
|
-
export interface SearchHit {
|
|
77
|
-
/** Path relative to the root. */
|
|
78
|
-
path: string
|
|
79
|
-
/** Display name (basename). */
|
|
80
|
-
name: string
|
|
81
|
-
isDir: boolean
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The filename-search result. */
|
|
85
|
-
export interface SearchView {
|
|
86
|
-
/** The query the hits were ranked for. */
|
|
87
|
-
query: string
|
|
88
|
-
hits: SearchHit[]
|
|
89
|
-
/** True when the hit cap cut the result stream. */
|
|
90
|
-
truncated: boolean
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Working-tree state of one change row. */
|
|
94
|
-
export type GitFileState = 'created' | 'modified' | 'deleted' | 'renamed' | 'conflicted' | 'untracked' | 'unknown'
|
|
95
|
-
|
|
96
|
-
/** One change row in a git status. */
|
|
97
|
-
export interface GitChangeRow {
|
|
98
|
-
/** Path relative to the repo root. */
|
|
99
|
-
path: string
|
|
100
|
-
/** Original path for renames (old -> new). */
|
|
101
|
-
oldPath?: string
|
|
102
|
-
state: GitFileState
|
|
103
|
-
/** True when the change sits in the index (staged). */
|
|
104
|
-
staged: boolean
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The git status view of one repo root. */
|
|
108
|
-
export interface GitStatusView {
|
|
109
|
-
/** Repo root (git rev-parse --show-toplevel). */
|
|
110
|
-
root: string
|
|
111
|
-
/** Current branch name; '' when detached. */
|
|
112
|
-
branch: string
|
|
113
|
-
/** Staged changes (index vs HEAD). */
|
|
114
|
-
staged: GitChangeRow[]
|
|
115
|
-
/** Unstaged changes (worktree vs index). */
|
|
116
|
-
unstaged: GitChangeRow[]
|
|
117
|
-
/** Untracked files (worktree, not in index). */
|
|
118
|
-
untracked: GitChangeRow[]
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** The result of a stage/unstage/discard batch. */
|
|
122
|
-
export interface GitBatchResult {
|
|
123
|
-
/** Rows the operation actually changed (post-op status snapshot for these paths). */
|
|
124
|
-
applied: string[]
|
|
125
|
-
/** Paths the host refused to touch. */
|
|
126
|
-
failed: string[]
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** One preview tab identity as persisted. */
|
|
130
|
-
export interface PreviewTabMeta {
|
|
131
|
-
/** Stable tab id (root + path + type). */
|
|
132
|
-
id: string
|
|
133
|
-
/** Display title (basename). */
|
|
134
|
-
title: string
|
|
135
|
-
/** Project root the file belongs to. */
|
|
136
|
-
root: string
|
|
137
|
-
/** File path relative to the root. */
|
|
138
|
-
path: string
|
|
139
|
-
contentType: PreviewContentType
|
|
140
|
-
/** Whether the tab carries unsaved edits. */
|
|
141
|
-
dirty?: boolean
|
|
142
|
-
/** Last write timestamp the tab's content was based on (conflict base). */
|
|
143
|
-
mtime?: number
|
|
144
|
-
/** When the tab was last touched (LRU ordering within a scope). */
|
|
145
|
-
savedAt: number
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Preview content kinds the panel can render. */
|
|
149
|
-
export type PreviewContentType =
|
|
150
|
-
| 'markdown'
|
|
151
|
-
| 'html'
|
|
152
|
-
| 'code'
|
|
153
|
-
| 'diff'
|
|
154
|
-
| 'csv'
|
|
155
|
-
| 'pdf'
|
|
156
|
-
| 'word'
|
|
157
|
-
| 'excel'
|
|
158
|
-
| 'ppt'
|
|
159
|
-
| 'image'
|
|
160
|
-
| 'text'
|
|
161
|
-
| 'url'
|
|
162
|
-
| 'unsupported'
|
|
1
|
+
/**
|
|
2
|
+
* Wire vocabulary shared by the host data services and the browser client:
|
|
3
|
+
* the request/response shapes of the /filemgr/* routes and the stable
|
|
4
|
+
* error codes the client maps onto copy. Pure types — no runtime code.
|
|
5
|
+
*
|
|
6
|
+
* The design follows FileManager's right-panel system (Apache-2.0, re-implemented
|
|
7
|
+
* from measured behavior, not copied code): the explorer column is a lazy
|
|
8
|
+
* directory tree, the preview column opens files as tabs, and the SCM tab
|
|
9
|
+
* reads the repository's real working-tree state.
|
|
10
|
+
* @module dsh-filemgr/core/types
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Envelope every /filemgr JSON response carries. */
|
|
14
|
+
export type PanelEnvelope<T> =
|
|
15
|
+
| { ok: true; value: T }
|
|
16
|
+
| { ok: false; error: PanelError }
|
|
17
|
+
|
|
18
|
+
/** Stable rejection codes (host-authored; the client prefers its own copy by code). */
|
|
19
|
+
export type PanelErrorCode =
|
|
20
|
+
| 'workspace-unknown'
|
|
21
|
+
| 'path-outside-root'
|
|
22
|
+
| 'not-found'
|
|
23
|
+
| 'is-directory'
|
|
24
|
+
| 'read-failed'
|
|
25
|
+
| 'write-conflict'
|
|
26
|
+
| 'write-failed'
|
|
27
|
+
| 'search-failed'
|
|
28
|
+
| 'git-unavailable'
|
|
29
|
+
| 'git-failed'
|
|
30
|
+
| 'internal'
|
|
31
|
+
|
|
32
|
+
/** One rejection with a human-readable host message. */
|
|
33
|
+
export interface PanelError {
|
|
34
|
+
code: PanelErrorCode
|
|
35
|
+
message: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One filesystem entry in a directory listing (path is relative to the root). */
|
|
39
|
+
export interface FsEntry {
|
|
40
|
+
/** Display name (basename). */
|
|
41
|
+
name: string
|
|
42
|
+
/** Path relative to the project root, '/' separated; '' is the root itself. */
|
|
43
|
+
path: string
|
|
44
|
+
isDir: boolean
|
|
45
|
+
/** Size in bytes (0 for directories). */
|
|
46
|
+
size: number
|
|
47
|
+
/** Last-modified epoch millis (0 for directories). */
|
|
48
|
+
mtime: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The listing of one directory. */
|
|
52
|
+
export interface DirListing {
|
|
53
|
+
/** Canonical root path the listing is relative to. */
|
|
54
|
+
root: string
|
|
55
|
+
/** Sorted entries (directories first, then files, both case-insensitive alpha). */
|
|
56
|
+
entries: FsEntry[]
|
|
57
|
+
/** True when the entry count was capped at MAX_ENTRIES (a giant flat dir). */
|
|
58
|
+
truncated: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The result of reading one file for preview. */
|
|
62
|
+
export interface FileRead {
|
|
63
|
+
/** Text content (decoded utf-8), or a data URL for image kinds. */
|
|
64
|
+
content: string
|
|
65
|
+
/** True when the text was truncated at the preview ceiling. */
|
|
66
|
+
truncated: boolean
|
|
67
|
+
/** Total size in bytes. */
|
|
68
|
+
size: number
|
|
69
|
+
/** Last-modified epoch millis (the write-conflict base). */
|
|
70
|
+
mtime: number
|
|
71
|
+
/** Image dimensions when the file is an image (host decodes via probe). */
|
|
72
|
+
image?: { width: number; height: number }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One filename-search hit. */
|
|
76
|
+
export interface SearchHit {
|
|
77
|
+
/** Path relative to the root. */
|
|
78
|
+
path: string
|
|
79
|
+
/** Display name (basename). */
|
|
80
|
+
name: string
|
|
81
|
+
isDir: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The filename-search result. */
|
|
85
|
+
export interface SearchView {
|
|
86
|
+
/** The query the hits were ranked for. */
|
|
87
|
+
query: string
|
|
88
|
+
hits: SearchHit[]
|
|
89
|
+
/** True when the hit cap cut the result stream. */
|
|
90
|
+
truncated: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Working-tree state of one change row. */
|
|
94
|
+
export type GitFileState = 'created' | 'modified' | 'deleted' | 'renamed' | 'conflicted' | 'untracked' | 'unknown'
|
|
95
|
+
|
|
96
|
+
/** One change row in a git status. */
|
|
97
|
+
export interface GitChangeRow {
|
|
98
|
+
/** Path relative to the repo root. */
|
|
99
|
+
path: string
|
|
100
|
+
/** Original path for renames (old -> new). */
|
|
101
|
+
oldPath?: string
|
|
102
|
+
state: GitFileState
|
|
103
|
+
/** True when the change sits in the index (staged). */
|
|
104
|
+
staged: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The git status view of one repo root. */
|
|
108
|
+
export interface GitStatusView {
|
|
109
|
+
/** Repo root (git rev-parse --show-toplevel). */
|
|
110
|
+
root: string
|
|
111
|
+
/** Current branch name; '' when detached. */
|
|
112
|
+
branch: string
|
|
113
|
+
/** Staged changes (index vs HEAD). */
|
|
114
|
+
staged: GitChangeRow[]
|
|
115
|
+
/** Unstaged changes (worktree vs index). */
|
|
116
|
+
unstaged: GitChangeRow[]
|
|
117
|
+
/** Untracked files (worktree, not in index). */
|
|
118
|
+
untracked: GitChangeRow[]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The result of a stage/unstage/discard batch. */
|
|
122
|
+
export interface GitBatchResult {
|
|
123
|
+
/** Rows the operation actually changed (post-op status snapshot for these paths). */
|
|
124
|
+
applied: string[]
|
|
125
|
+
/** Paths the host refused to touch. */
|
|
126
|
+
failed: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One preview tab identity as persisted. */
|
|
130
|
+
export interface PreviewTabMeta {
|
|
131
|
+
/** Stable tab id (root + path + type). */
|
|
132
|
+
id: string
|
|
133
|
+
/** Display title (basename). */
|
|
134
|
+
title: string
|
|
135
|
+
/** Project root the file belongs to. */
|
|
136
|
+
root: string
|
|
137
|
+
/** File path relative to the root. */
|
|
138
|
+
path: string
|
|
139
|
+
contentType: PreviewContentType
|
|
140
|
+
/** Whether the tab carries unsaved edits. */
|
|
141
|
+
dirty?: boolean
|
|
142
|
+
/** Last write timestamp the tab's content was based on (conflict base). */
|
|
143
|
+
mtime?: number
|
|
144
|
+
/** When the tab was last touched (LRU ordering within a scope). */
|
|
145
|
+
savedAt: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Preview content kinds the panel can render. */
|
|
149
|
+
export type PreviewContentType =
|
|
150
|
+
| 'markdown'
|
|
151
|
+
| 'html'
|
|
152
|
+
| 'code'
|
|
153
|
+
| 'diff'
|
|
154
|
+
| 'csv'
|
|
155
|
+
| 'pdf'
|
|
156
|
+
| 'word'
|
|
157
|
+
| 'excel'
|
|
158
|
+
| 'ppt'
|
|
159
|
+
| 'image'
|
|
160
|
+
| 'text'
|
|
161
|
+
| 'url'
|
|
162
|
+
| 'unsupported'
|
package/src/host/fs-service.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { open, readdir, readFile, realpath, rename as renameFile, stat, writeFile, rm, mkdir } from 'node:fs/promises'
|
|
20
20
|
import type { Dirent } from 'node:fs'
|
|
21
|
-
import { join, dirname, relative } from 'node:path'
|
|
21
|
+
import { join, dirname, relative, basename } from 'node:path'
|
|
22
22
|
import type { DirListing, FileRead, FsEntry, PanelError, SearchHit, SearchView } from '../core/types.ts'
|
|
23
23
|
import { isPathInside, type GateVerdict, type WorkspaceGate } from './gate.ts'
|
|
24
24
|
|
|
@@ -156,6 +156,12 @@ function mimeByExtension(rel: string): string | undefined {
|
|
|
156
156
|
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
157
157
|
webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon', avif: 'image/avif', bmp: 'image/bmp',
|
|
158
158
|
pdf: 'application/pdf',
|
|
159
|
+
doc: 'application/msword',
|
|
160
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
161
|
+
xls: 'application/vnd.ms-excel',
|
|
162
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
163
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
164
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
159
165
|
}
|
|
160
166
|
return byExt[ext]
|
|
161
167
|
}
|
package/src/host/gate.ts
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace gate for the /filemgr routes: canonicalize the requested
|
|
3
|
-
* project root and require it to be a registered workspace (or a directory
|
|
4
|
-
* inside one). This is the security boundary of the panel's fs/git routes -
|
|
5
|
-
* the browser may only read and mutate files under registered workspace
|
|
6
|
-
* roots, never arbitrary host directories.
|
|
7
|
-
* @module dsh-filemgr/host/gate
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { realpath } from 'node:fs/promises'
|
|
11
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
-
import type {} from '@deepseek-ai/dsh-workspace'
|
|
13
|
-
import type { PanelError } from '../core/types.ts'
|
|
14
|
-
|
|
15
|
-
/** The gate verdict for one project root. */
|
|
16
|
-
export type GateVerdict = { ok: true; canonical: string } | { ok: false; error: PanelError }
|
|
17
|
-
|
|
18
|
-
/** The workspace-membership check the services run on every request. */
|
|
19
|
-
export type WorkspaceGate = (root: string) => Promise<GateVerdict>
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Normalize a path for prefix comparison: collapse Windows separators to `/`
|
|
23
|
-
* and drop any trailing slash. On win32 the whole path is also lower-cased so
|
|
24
|
-
* a case-insensitive FS cannot trip the membership check (the drive letter and
|
|
25
|
-
* every segment are compared case-insensitively). On any other platform the
|
|
26
|
-
* path separator and case are left untouched.
|
|
27
|
-
*/
|
|
28
|
-
export function normalizeForPrefix(value: string): string {
|
|
29
|
-
const normalized = value.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
30
|
-
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The canonical prefix check: child must live inside (or equal) the root.
|
|
35
|
-
* Separator- and case-robust on Windows: `path.join` yields backslashes while
|
|
36
|
-
* git (`rev-parse --show-toplevel`) and the browser (`./x`) yield forward
|
|
37
|
-
* slashes, so both sides are normalized to forward slashes before comparing,
|
|
38
|
-
* and the comparison is case-insensitive on win32 (the FS is case-insensitive).
|
|
39
|
-
*/
|
|
40
|
-
export function isPathInside(root: string, child: string): boolean {
|
|
41
|
-
if (root === '' || child === '') return false
|
|
42
|
-
const normRoot = normalizeForPrefix(root)
|
|
43
|
-
const normChild = normalizeForPrefix(child)
|
|
44
|
-
if (normChild === normRoot) return true
|
|
45
|
-
return normChild.startsWith(`${normRoot}/`)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Production gate: canonicalize the requested root and require it to be a
|
|
50
|
-
* registered workspace path (or a subdirectory of one). The host's workspace
|
|
51
|
-
* registry owns canonicalization, so an unowned path is rejected outright.
|
|
52
|
-
* @param ctx - context carrying the workspace service.
|
|
53
|
-
* @returns the gate.
|
|
54
|
-
*/
|
|
55
|
-
export function createWorkspaceGate(ctx: Context): WorkspaceGate {
|
|
56
|
-
return async (root) => {
|
|
57
|
-
if (typeof root !== 'string' || root === '') {
|
|
58
|
-
return { ok: false, error: { code: 'workspace-unknown', message: 'empty project root' } }
|
|
59
|
-
}
|
|
60
|
-
let canonical: string
|
|
61
|
-
try {
|
|
62
|
-
canonical = await realpath(root)
|
|
63
|
-
} catch {
|
|
64
|
-
return { ok: false, error: { code: 'workspace-unknown', message: 'path does not resolve on disk' } }
|
|
65
|
-
}
|
|
66
|
-
const workspaces = ctx.workspaceRegistry.list()
|
|
67
|
-
for (const workspace of workspaces) {
|
|
68
|
-
if (isPathInside(workspace.path, canonical)) {
|
|
69
|
-
return { ok: true, canonical }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return { ok: false, error: { code: 'workspace-unknown', message: 'path is not inside a registered workspace' } }
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Workspace gate for the /filemgr routes: canonicalize the requested
|
|
3
|
+
* project root and require it to be a registered workspace (or a directory
|
|
4
|
+
* inside one). This is the security boundary of the panel's fs/git routes -
|
|
5
|
+
* the browser may only read and mutate files under registered workspace
|
|
6
|
+
* roots, never arbitrary host directories.
|
|
7
|
+
* @module dsh-filemgr/host/gate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { realpath } from 'node:fs/promises'
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import type {} from '@deepseek-ai/dsh-workspace'
|
|
13
|
+
import type { PanelError } from '../core/types.ts'
|
|
14
|
+
|
|
15
|
+
/** The gate verdict for one project root. */
|
|
16
|
+
export type GateVerdict = { ok: true; canonical: string } | { ok: false; error: PanelError }
|
|
17
|
+
|
|
18
|
+
/** The workspace-membership check the services run on every request. */
|
|
19
|
+
export type WorkspaceGate = (root: string) => Promise<GateVerdict>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a path for prefix comparison: collapse Windows separators to `/`
|
|
23
|
+
* and drop any trailing slash. On win32 the whole path is also lower-cased so
|
|
24
|
+
* a case-insensitive FS cannot trip the membership check (the drive letter and
|
|
25
|
+
* every segment are compared case-insensitively). On any other platform the
|
|
26
|
+
* path separator and case are left untouched.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeForPrefix(value: string): string {
|
|
29
|
+
const normalized = value.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
30
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The canonical prefix check: child must live inside (or equal) the root.
|
|
35
|
+
* Separator- and case-robust on Windows: `path.join` yields backslashes while
|
|
36
|
+
* git (`rev-parse --show-toplevel`) and the browser (`./x`) yield forward
|
|
37
|
+
* slashes, so both sides are normalized to forward slashes before comparing,
|
|
38
|
+
* and the comparison is case-insensitive on win32 (the FS is case-insensitive).
|
|
39
|
+
*/
|
|
40
|
+
export function isPathInside(root: string, child: string): boolean {
|
|
41
|
+
if (root === '' || child === '') return false
|
|
42
|
+
const normRoot = normalizeForPrefix(root)
|
|
43
|
+
const normChild = normalizeForPrefix(child)
|
|
44
|
+
if (normChild === normRoot) return true
|
|
45
|
+
return normChild.startsWith(`${normRoot}/`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Production gate: canonicalize the requested root and require it to be a
|
|
50
|
+
* registered workspace path (or a subdirectory of one). The host's workspace
|
|
51
|
+
* registry owns canonicalization, so an unowned path is rejected outright.
|
|
52
|
+
* @param ctx - context carrying the workspace service.
|
|
53
|
+
* @returns the gate.
|
|
54
|
+
*/
|
|
55
|
+
export function createWorkspaceGate(ctx: Context): WorkspaceGate {
|
|
56
|
+
return async (root) => {
|
|
57
|
+
if (typeof root !== 'string' || root === '') {
|
|
58
|
+
return { ok: false, error: { code: 'workspace-unknown', message: 'empty project root' } }
|
|
59
|
+
}
|
|
60
|
+
let canonical: string
|
|
61
|
+
try {
|
|
62
|
+
canonical = await realpath(root)
|
|
63
|
+
} catch {
|
|
64
|
+
return { ok: false, error: { code: 'workspace-unknown', message: 'path does not resolve on disk' } }
|
|
65
|
+
}
|
|
66
|
+
const workspaces = ctx.workspaceRegistry.list()
|
|
67
|
+
for (const workspace of workspaces) {
|
|
68
|
+
if (isPathInside(workspace.path, canonical)) {
|
|
69
|
+
return { ok: true, canonical }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ok: false, error: { code: 'workspace-unknown', message: 'path is not inside a registered workspace' } }
|
|
73
|
+
}
|
|
74
74
|
}
|