dsh-sidebar-suite 0.1.3 → 0.2.5

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/lib/index.js CHANGED
@@ -1,158 +1,215 @@
1
- // dsh-sidebar-suite — Host half.
2
- //
3
- // Ships only the option schema. Every layout rule lives in the Client half, so
4
- // this module has no runtime dependencies of its own: `@deepseek-ai/schemastery`
5
- // is resolved the way the host resolves it (bare specifier first, then the
6
- // profile's own node_modules), and when neither works the plugin still loads
7
- // without a schema instead of failing the whole plugin tree.
8
- import { existsSync, readdirSync } from 'node:fs'
9
- import { createRequire } from 'node:module'
10
- import { dirname, join } from 'node:path'
11
- import { fileURLToPath, pathToFileURL } from 'node:url'
12
-
13
- /** Values the Client half falls back to when a field is absent. */
14
- const DEFAULTS = {
15
- mode: 'b',
16
- showQuickSwitch: true,
17
- tab: 'ext',
18
- foldExt: false,
19
- foldWs: false,
20
- }
21
-
22
- /**
23
- * Resolve the schema library without making the module import depend on it.
24
- *
25
- * A plugin installed from a local directory is linked into the profile, so the
26
- * host usually resolves a bare specifier through the profile's node_modules. If
27
- * the loader realpaths that link, a bare specifier fails; the profile directory
28
- * itself is then the second attempt, and the directories around this file and
29
- * the running process are the last resort. When nothing resolves, the plugin
30
- * still loads, just without a settings schema.
31
- */
32
- function looksLikeSchema(value) {
33
- return typeof value === 'function' || typeof value?.object === 'function'
34
- }
35
-
36
- async function tryBare() {
37
- try {
38
- const mod = await import('@deepseek-ai/schemastery')
39
- const schema = mod && (mod.default || mod)
40
- return looksLikeSchema(schema) ? schema : null
41
- } catch (error) {
42
- void error
43
- return null
44
- }
45
- }
46
-
47
- /** Directories whose `node_modules` may hold the profile's schema copy. */
48
- function candidateBases() {
49
- const bases = []
50
- const push = (dir) => {
51
- if (typeof dir === 'string' && dir.length > 0 && !bases.includes(dir)) bases.push(dir)
52
- }
53
-
54
- const climb = (start, levels) => {
55
- let dir = start
56
- for (let i = 0; i < levels && dir; i += 1) {
57
- push(dir)
58
- const parent = dirname(dir)
59
- if (parent === dir) break
60
- dir = parent
61
- }
62
- }
63
-
64
- push(process.env.DSH_PROFILE_DIR)
65
- climb(dirname(fileURLToPath(import.meta.url)), 8)
66
- climb(process.cwd(), 8)
67
-
68
- const homes = [process.env.DSH_HOME, process.env.USERPROFILE, process.env.HOME].filter(Boolean)
69
- for (const home of homes) {
70
- const profiles = join(home, '.dsh', 'profiles')
71
- try {
72
- for (const name of readdirSync(profiles)) push(join(profiles, name))
73
- } catch (error) {
74
- void error
75
- }
76
- const direct = join(home, 'profiles')
77
- try {
78
- for (const name of readdirSync(direct)) push(join(direct, name))
79
- } catch (error) {
80
- void error
81
- }
82
- }
83
- return bases
84
- }
85
-
86
- async function loadSchema() {
87
- const bare = await tryBare()
88
- if (bare) return bare
89
-
90
- for (const base of candidateBases()) {
91
- const packageDir = join(base, 'node_modules', '@deepseek-ai', 'schemastery')
92
- const entry = join(packageDir, 'lib', 'index.mjs')
93
- if (!existsSync(entry)) continue
94
-
95
- // require() follows the package's own `exports` map, so this keeps working
96
- // when the library layout changes between versions.
97
- try {
98
- const require = createRequire(join(base, 'package.json'))
99
- const schema = require('@deepseek-ai/schemastery')
100
- if (looksLikeSchema(schema)) return schema
101
- } catch (error) {
102
- void error
103
- }
104
- try {
105
- const mod = await import(pathToFileURL(entry).href)
106
- const schema = mod && (mod.default || mod)
107
- if (looksLikeSchema(schema)) return schema
108
- } catch (error) {
109
- void error
110
- }
111
- }
112
- return null
113
- }
114
-
115
- const schema = await loadSchema()
116
-
117
- /**
118
- * Volatile fields are the ones the settings page can change without restarting
119
- * this plugin: the host re-applies them in place and the Client half reads the
120
- * new values through its config form.
121
- */
122
- export const Config = schema === null
123
- ? undefined
124
- : schema.object({
125
- mode: schema
126
- .union(['a', 'b', 'c'])
127
- .default(DEFAULTS.mode)
128
- .description('Sidebar layout: a folds each group, b switches the two groups with tabs, c keeps the workspaces and opens the panels as an overlay.')
129
- .volatile(),
130
- showQuickSwitch: schema
131
- .boolean()
132
- .default(DEFAULTS.showQuickSwitch)
133
- .description('Show the button that cycles through the three layouts in the sidebar control bar.')
134
- .volatile(),
135
- tab: schema
136
- .union(['ext', 'ws'])
137
- .default(DEFAULTS.tab)
138
- .description('Which group the tabbed layout shows first.')
139
- .volatile(),
140
- foldExt: schema
141
- .boolean()
142
- .default(DEFAULTS.foldExt)
143
- .description('Folded layout: whether the panels group starts folded.')
144
- .volatile(),
145
- foldWs: schema
146
- .boolean()
147
- .default(DEFAULTS.foldWs)
148
- .description('Folded layout: whether the workspaces group starts folded.')
149
- .volatile(),
150
- })
151
-
152
- export function apply(ctx) {
153
- try {
154
- ctx.logger?.debug?.('[dsh-sidebar-suite] host half ready')
155
- } catch (error) {
156
- void error
157
- }
158
- }
1
+ // dsh-sidebar-suite — Host half.
2
+ //
3
+ // Ships only the option schema. Every layout rule lives in the Client half, so
4
+ // this module has no runtime dependencies of its own: `@deepseek-ai/schemastery`
5
+ // is resolved the way the host resolves it (bare specifier first, then the
6
+ // profile's own node_modules), and when neither works the plugin still loads
7
+ // without a schema instead of failing the whole plugin tree.
8
+ import { existsSync, readdirSync } from 'node:fs'
9
+ import { createRequire } from 'node:module'
10
+ import { dirname, join } from 'node:path'
11
+ import { fileURLToPath, pathToFileURL } from 'node:url'
12
+
13
+ // Load the temporary-session host lazily. If that one file ever fails to parse
14
+ // (a broken edit, a partial install), the three sidebar layouts must still
15
+ // load: a static import would take down the entire Host half with it.
16
+ let createTempSessionHost = null
17
+ let tempImportError = null
18
+ try {
19
+ const tempModule = await import('./temp-session-host.mjs')
20
+ createTempSessionHost = tempModule.createTempSessionHost || null
21
+ } catch (error) {
22
+ tempImportError = String(error && (error.message || error))
23
+ }
24
+
25
+ /** Values the Client half falls back to when a field is absent. */
26
+ const DEFAULTS = {
27
+ mode: 'b',
28
+ showQuickSwitch: true,
29
+ tab: 'ext',
30
+ foldExt: false,
31
+ foldWs: false,
32
+ }
33
+
34
+ /**
35
+ * Resolve the schema library without making the module import depend on it.
36
+ *
37
+ * A plugin installed from a local directory is linked into the profile, so the
38
+ * host usually resolves a bare specifier through the profile's node_modules. If
39
+ * the loader realpaths that link, a bare specifier fails; the profile directory
40
+ * itself is then the second attempt, and the directories around this file and
41
+ * the running process are the last resort. When nothing resolves, the plugin
42
+ * still loads, just without a settings schema.
43
+ */
44
+ function looksLikeSchema(value) {
45
+ return typeof value === 'function' || typeof value?.object === 'function'
46
+ }
47
+
48
+ async function tryBare() {
49
+ try {
50
+ const mod = await import('@deepseek-ai/schemastery')
51
+ const schema = mod && (mod.default || mod)
52
+ return looksLikeSchema(schema) ? schema : null
53
+ } catch (error) {
54
+ void error
55
+ return null
56
+ }
57
+ }
58
+
59
+ /** Directories whose `node_modules` may hold the profile's schema copy. */
60
+ function candidateBases() {
61
+ const bases = []
62
+ const push = (dir) => {
63
+ if (typeof dir === 'string' && dir.length > 0 && !bases.includes(dir)) bases.push(dir)
64
+ }
65
+
66
+ const climb = (start, levels) => {
67
+ let dir = start
68
+ for (let i = 0; i < levels && dir; i += 1) {
69
+ push(dir)
70
+ const parent = dirname(dir)
71
+ if (parent === dir) break
72
+ dir = parent
73
+ }
74
+ }
75
+
76
+ push(process.env.DSH_PROFILE_DIR)
77
+ climb(dirname(fileURLToPath(import.meta.url)), 8)
78
+ climb(process.cwd(), 8)
79
+
80
+ const homes = [process.env.DSH_HOME, process.env.USERPROFILE, process.env.HOME].filter(Boolean)
81
+ for (const home of homes) {
82
+ const profiles = join(home, '.dsh', 'profiles')
83
+ try {
84
+ for (const name of readdirSync(profiles)) push(join(profiles, name))
85
+ } catch (error) {
86
+ void error
87
+ }
88
+ const direct = join(home, 'profiles')
89
+ try {
90
+ for (const name of readdirSync(direct)) push(join(direct, name))
91
+ } catch (error) {
92
+ void error
93
+ }
94
+ }
95
+ return bases
96
+ }
97
+
98
+ async function loadSchema() {
99
+ const bare = await tryBare()
100
+ if (bare) return bare
101
+
102
+ for (const base of candidateBases()) {
103
+ const packageDir = join(base, 'node_modules', '@deepseek-ai', 'schemastery')
104
+ const entry = join(packageDir, 'lib', 'index.mjs')
105
+ if (!existsSync(entry)) continue
106
+
107
+ // require() follows the package's own `exports` map, so this keeps working
108
+ // when the library layout changes between versions.
109
+ try {
110
+ const require = createRequire(join(base, 'package.json'))
111
+ const schema = require('@deepseek-ai/schemastery')
112
+ if (looksLikeSchema(schema)) return schema
113
+ } catch (error) {
114
+ void error
115
+ }
116
+ try {
117
+ const mod = await import(pathToFileURL(entry).href)
118
+ const schema = mod && (mod.default || mod)
119
+ if (looksLikeSchema(schema)) return schema
120
+ } catch (error) {
121
+ void error
122
+ }
123
+ }
124
+ return null
125
+ }
126
+
127
+ const schema = await loadSchema()
128
+
129
+ /**
130
+ * Volatile fields are the ones the settings page can change without restarting
131
+ * this plugin: the host re-applies them in place and the Client half reads the
132
+ * new values through its config form.
133
+ */
134
+ export const Config = schema === null
135
+ ? undefined
136
+ : schema.object({
137
+ mode: schema
138
+ .union(['a', 'b', 'c'])
139
+ .default(DEFAULTS.mode)
140
+ .description('Sidebar layout: a folds each group, b switches the two groups with tabs, c keeps the workspaces and opens the panels as an overlay.')
141
+ .volatile(),
142
+ showQuickSwitch: schema
143
+ .boolean()
144
+ .default(DEFAULTS.showQuickSwitch)
145
+ .description('Show the button that cycles through the three layouts in the sidebar control bar.')
146
+ .volatile(),
147
+ tab: schema
148
+ .union(['ext', 'ws'])
149
+ .default(DEFAULTS.tab)
150
+ .description('Which group the tabbed layout shows first.')
151
+ .volatile(),
152
+ foldExt: schema
153
+ .boolean()
154
+ .default(DEFAULTS.foldExt)
155
+ .description('Folded layout: whether the panels group starts folded.')
156
+ .volatile(),
157
+ foldWs: schema
158
+ .boolean()
159
+ .default(DEFAULTS.foldWs)
160
+ .description('Folded layout: whether the workspaces group starts folded.')
161
+ .volatile(),
162
+ })
163
+
164
+ let tempHost = null
165
+
166
+ /**
167
+ * `connection` is the one service this bundle cannot work without: it owns the
168
+ * `/api` Fetch registry that serves the temporary-session routes.
169
+ *
170
+ * This MUST be a static declaration. Verified against the host's own
171
+ * `@deepseek-ai/cordis` 4.1.0: a plugin that registers no `inject` export still
172
+ * receives a ctx where reading `ctx.connection` throws
173
+ * "cannot get property \\"connection\\" without inject", and calling
174
+ * `ctx.inject(['connection'], cb)` from inside `apply` never activates — so the
175
+ * routes were never registered and the browser got
176
+ * `404 {"error":"not found"}` from Connection.createSharedFetchHandler
177
+ * (dsh-client-connection/lib/index.js). Every official bundle that publishes
178
+ * /api routes declares it statically (dsh-session-log-export:
179
+ * `inject = ["commands", "connection"]`).
180
+ *
181
+ * Since 0.2.0 the model is no longer read here at all: a temporary session is
182
+ * a real DSH session, so it inherits whatever model the user picked. That
183
+ * removes the whole class of "no model" failures 0.1.5/0.1.6 had to patch.
184
+ */
185
+ export const inject = ['connection']
186
+
187
+ /**
188
+ * Register the /api routes that track temporary sessions.
189
+ *
190
+ * `register()` is idempotent per route and treats "already registered" as
191
+ * success, so repeated calls (volatile config re-applies `apply`) are safe.
192
+ */
193
+ function startTempHost(ctx) {
194
+ if (!createTempSessionHost) {
195
+ try { ctx.logger?.warn?.(`[dsh-sidebar-suite] temp-session host file failed to load: ${tempImportError}`) } catch (error) { void error }
196
+ return
197
+ }
198
+ try {
199
+ if (!tempHost) tempHost = createTempSessionHost(ctx)
200
+ else tempHost.setContext(ctx)
201
+ tempHost.register(ctx)
202
+ try { ctx.logger?.debug?.(`[dsh-sidebar-suite] temp routes=${tempHost.state.routes.registered}`) } catch (error) { void error }
203
+ } catch (error) {
204
+ try { ctx.logger?.debug?.(`[dsh-sidebar-suite] temp session unavailable: ${error && error.message}`) } catch (inner) { void inner }
205
+ }
206
+ }
207
+
208
+ export function apply(ctx) {
209
+ try {
210
+ ctx.logger?.debug?.('[dsh-sidebar-suite] host half ready')
211
+ } catch (error) {
212
+ void error
213
+ }
214
+ startTempHost(ctx)
215
+ }