@objectivex666/dsh-settings-search 1.13.1 → 1.14.1

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,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Harness compatibility audit.
4
+ *
5
+ * This plugin talks to DSH through surfaces that are not part of any public npm
6
+ * API — client service names, Typert Remote descriptors, slot keys, theme
7
+ * tokens, the host Fetch route registry, and the DeepSeek adapter constants.
8
+ * `npm test` cannot see any of those, so run this against the Harness install
9
+ * you are actually targeting:
10
+ *
11
+ * node scripts/compat.mjs # auto-detect the install
12
+ * node scripts/compat.mjs --asar <app.asar> # explicit archive
13
+ * DSH_ASAR=<app.asar> node scripts/compat.mjs # or through the environment
14
+ *
15
+ * The archive is read read-only and nothing is written. The script exits
16
+ * non-zero as soon as an assumption breaks — that moment is exactly when a
17
+ * Harness upgrade needs this plugin adapted.
18
+ */
19
+ import { closeSync, existsSync, openSync, readFileSync, readSync } from 'node:fs'
20
+ import { dirname, join } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
24
+
25
+ /** Candidate install locations, most specific first. */
26
+ const DEFAULT_ASAR_CANDIDATES = [
27
+ 'E:\\Program Files\\dsh\\resources\\app.asar',
28
+ 'C:\\Program Files\\dsh\\resources\\app.asar',
29
+ '/Applications/DeepSeek Harness.app/Contents/Resources/app.asar',
30
+ ]
31
+
32
+ const argOf = (name) => {
33
+ const at = process.argv.indexOf(name)
34
+ return at >= 0 ? process.argv[at + 1] : undefined
35
+ }
36
+
37
+ /**
38
+ * A minimal read-only Electron asar reader: a pickle-framed JSON header with
39
+ * `{ size, offset }` per entry, the payload starting right after the header.
40
+ * @param archive - absolute path to `app.asar`.
41
+ */
42
+ const openAsar = (archive) => {
43
+ const fd = openSync(archive, 'r')
44
+ const sizeBuf = Buffer.alloc(8)
45
+ readSync(fd, sizeBuf, 0, 8, 0)
46
+ const headerSize = sizeBuf.readUInt32LE(4)
47
+ const headerBuf = Buffer.alloc(headerSize)
48
+ readSync(fd, headerBuf, 0, headerSize, 8)
49
+ const jsonLength = headerBuf.readUInt32LE(4)
50
+ const header = JSON.parse(headerBuf.toString('utf8', 8, 8 + jsonLength))
51
+ const dataStart = 8 + headerSize
52
+ const files = new Map()
53
+ const walk = (node, prefix) => {
54
+ for (const [name, entry] of Object.entries(node.files ?? {})) {
55
+ const path = prefix === '' ? name : `${prefix}/${name}`
56
+ if (entry.files !== undefined) walk(entry, path)
57
+ else files.set(path, { size: entry.size, offset: Number(entry.offset) })
58
+ }
59
+ }
60
+ walk(header, '')
61
+ const reader = {
62
+ archive,
63
+ files,
64
+ has: (path) => files.has(path),
65
+ read(path) {
66
+ const entry = files.get(path)
67
+ if (entry === undefined) throw new Error(`no entry ${JSON.stringify(path)}`)
68
+ const buffer = Buffer.alloc(entry.size)
69
+ readSync(fd, buffer, 0, entry.size, dataStart + entry.offset)
70
+ return buffer.toString('utf8')
71
+ },
72
+ close: () => closeSync(fd),
73
+ }
74
+ return reader
75
+ }
76
+
77
+ /** Locate the archive to audit. */
78
+ const resolveAsar = () => {
79
+ const explicit = argOf('--asar') ?? process.env.DSH_ASAR
80
+ if (explicit !== undefined && explicit !== '') {
81
+ if (!existsSync(explicit)) throw new Error(`archive not found at ${explicit}`)
82
+ return explicit
83
+ }
84
+ const found = DEFAULT_ASAR_CANDIDATES.find((candidate) => existsSync(candidate))
85
+ if (found === undefined) {
86
+ throw new Error('could not locate a Harness install — pass --asar <app.asar> or set DSH_ASAR')
87
+ }
88
+ return found
89
+ }
90
+
91
+ const checks = []
92
+ /**
93
+ * Run one compatibility check. A check returns a short detail string, `true`,
94
+ * or `undefined` when it passes, and throws (or returns `false`) when the
95
+ * Harness no longer matches an assumption this plugin makes.
96
+ */
97
+ const check = (label, run) => {
98
+ try {
99
+ const detail = run()
100
+ checks.push({ label, ok: detail !== false, detail: detail === true || detail === undefined ? '' : String(detail) })
101
+ } catch (error) {
102
+ checks.push({ label, ok: false, detail: String(error?.message ?? error) })
103
+ }
104
+ }
105
+
106
+ /** The client-half source of one package, whatever the platform file is named. */
107
+ const clientSourceOf = (archive, name, cache) => {
108
+ if (cache.has(name)) return cache.get(name)
109
+ const candidates = [
110
+ `dsh/node_modules/@deepseek-ai/${name}/lib/client.js`,
111
+ `dsh/node_modules/@deepseek-ai/${name}/lib/client.browser.js`,
112
+ ]
113
+ const found = candidates.find((path) => archive.has(path))
114
+ if (found === undefined) throw new Error(`${name} ships no client bundle`)
115
+ const text = archive.read(found)
116
+ cache.set(name, text)
117
+ return text
118
+ }
119
+
120
+ /** Assert one substring, reporting the missing needle. */
121
+ const mustInclude = (text, needle, message) => {
122
+ if (!text.includes(needle)) throw new Error(message ?? `missing ${JSON.stringify(needle)}`)
123
+ }
124
+
125
+ /**
126
+ * Assert one pattern. Bundled client code is minified and re-emitted by every
127
+ * release, so it may quote a string with `"`, `'`, or a template literal
128
+ * without any behavioural change — match the code, not the quoting.
129
+ */
130
+ const mustMatch = (text, pattern, message) => {
131
+ if (!pattern.test(text)) throw new Error(message ?? `missing ${pattern}`)
132
+ }
133
+
134
+ const main = () => {
135
+ const archivePath = resolveAsar()
136
+ const archive = openAsar(archivePath)
137
+ const cache = new Map()
138
+ try {
139
+ const connectionManifest = JSON.parse(archive.read('dsh/node_modules/@deepseek-ai/dsh-client-connection/package.json'))
140
+ const source = readFileSync(join(ROOT, 'lib/client.js'), 'utf8')
141
+ console.log(`Harness: ${archivePath}`)
142
+ console.log(`Version: ${connectionManifest.version}\n`)
143
+
144
+ check('client connection service is provided', () => {
145
+ mustMatch(
146
+ clientSourceOf(archive, 'dsh-client-connection', cache),
147
+ /provide\(\s*(['"`])connection\1/,
148
+ 'the connection handle is gone',
149
+ )
150
+ return 'ctx.connection'
151
+ })
152
+
153
+ check('Remote namespaces live at remote.<namespace>', () => {
154
+ const gateway = clientSourceOf(archive, 'dsh-api-gateway', cache)
155
+ mustMatch(gateway, /super\(\s*ctx\s*,\s*(['"`])remote\1\s*\)/, 'the `remote` service is gone')
156
+ mustInclude(gateway, 'remote.${namespace}', 'namespace service keys changed')
157
+ return 'remote.<ns>'
158
+ })
159
+
160
+ check('settings/describe answers { namespaces: [{ ns, value }] }', () => {
161
+ const settings = archive.read('dsh/node_modules/@deepseek-ai/dsh-api-settings-controller/lib/typert.remote-client.js')
162
+ mustInclude(settings, "'namespaces'", 'describe value lost `namespaces`')
163
+ mustInclude(settings, "'ns': z.string()", 'namespace view lost `ns`')
164
+ mustInclude(settings, "'value': z.json()", 'namespace view lost `value`')
165
+ return 'ok'
166
+ })
167
+
168
+ check('settings/describe takes no arguments', () => {
169
+ const settings = archive.read('dsh/node_modules/@deepseek-ai/dsh-api-settings-controller/lib/typert.remote-client.js')
170
+ const at = settings.indexOf("'settings/describe':")
171
+ if (at === -1) throw new Error('the settings/describe descriptor is gone')
172
+ if (!/parameters:\s*\[\s*\]/.test(settings.slice(at, at + 900))) throw new Error('settings/describe now takes parameters')
173
+ return 'ok'
174
+ })
175
+
176
+ check('llm/listConfigurableProviders keeps settingsNs/settingsPath', () => {
177
+ const llm = archive.read('dsh/node_modules/@deepseek-ai/dsh-llm/lib/typert.remote-client.js')
178
+ mustInclude(llm, "'settingsNs'", 'rows lost settingsNs')
179
+ mustInclude(llm, "'settingsPath'", 'rows lost settingsPath')
180
+ return 'ok'
181
+ })
182
+
183
+ check('pluginManager inventories keep the fields the fingerprint reads', () => {
184
+ const manager = archive.read('dsh/node_modules/@deepseek-ai/dsh-plugin-manager/lib/typert.remote-client.js')
185
+ for (const field of ["'name'", "'version'", "'enabled'"]) {
186
+ mustInclude(manager, field, `a bundle row lost ${field}`)
187
+ }
188
+ for (const field of ["'entryId'", "'moduleName'"]) {
189
+ mustInclude(manager, field, `a plugin row lost ${field}`)
190
+ }
191
+ return 'name/version/enabled + entryId/moduleName'
192
+ })
193
+
194
+ check('host connection still owns the exact-Fetch route registry', () => {
195
+ const host = archive.read('dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js')
196
+ mustInclude(host, 'registerFetchRoute', 'the host fetch registry is gone')
197
+ mustInclude(host, 'bodyMode === "streaming"', 'streaming exact routes are no longer supported')
198
+ const segment = /const ENDPOINT_SEGMENT_PATTERN = \/\^\[([^\]]+)\]/.exec(host)
199
+ if (segment === null) throw new Error('endpoint validation pattern not found')
200
+ const sample = 'settings-search-log'
201
+ if (!new RegExp(`^[${segment[1]}]+$`).test(sample)) throw new Error(`route segment ${sample} is no longer valid`)
202
+ return `/api/${sample}`
203
+ })
204
+
205
+ check('every theme token this plugin uses exists in the Harness theme', () => {
206
+ const theme = clientSourceOf(archive, 'dsh-client-ui-theme', cache)
207
+ const defined = new Set([...theme.matchAll(/--dsw-[a-z0-9-]+/g)].map((match) => match[0]))
208
+ const used = new Set([...source.matchAll(/--dsw-[a-z0-9-]+/g)].map((match) => match[0]))
209
+ const missing = [...used].filter((token) => !defined.has(token))
210
+ if (missing.length > 0) throw new Error(`missing: ${missing.join(', ')}`)
211
+ return `${used.size} tokens`
212
+ })
213
+
214
+ check('the settings seats this plugin indexes still exist', () => {
215
+ const seats = ['settings.header', 'settings.section', 'settings.plugins.tab', 'settings.general.item']
216
+ const bundles = [...archive.files.keys()]
217
+ .filter((path) => /^dsh\/node_modules\/@deepseek-ai\/dsh-client-ui-[^/]+\/lib\/client(\.browser)?\.js$/.test(path))
218
+ const text = bundles.map((path) => archive.read(path)).join('\n')
219
+ const missing = seats.filter((seat) => !text.includes(`"${seat}"`))
220
+ if (missing.length > 0) throw new Error(`undeclared: ${missing.join(', ')}`)
221
+ return `${seats.length} seats`
222
+ })
223
+
224
+ check('the Plugins inventory still renders the 会话/全局 groups this plugin mines', () => {
225
+ const inventory = clientSourceOf(archive, 'dsh-client-ui-settings-plugin-inventory', cache)
226
+ mustInclude(inventory, '"groupTitle"', 'the group title class hook is gone')
227
+ mustInclude(inventory, '"data-plugin-scope"', 'the group scope attribute is gone')
228
+ return 'groupTitle + data-plugin-scope'
229
+ })
230
+
231
+ check('the DeepSeek adapter constants the import fallback mirrors', () => {
232
+ const deepseek = archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js')
233
+ mustInclude(deepseek, '"https://api.deepseek.com/anthropic"', 'PUBLIC_BASE_URL changed')
234
+ mustInclude(deepseek, '"DEEPSEEK_BASE_URL"', 'BASE_URL_ENV changed')
235
+ mustInclude(
236
+ archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek-api-key/lib/index.js'),
237
+ '"deepseek-official"',
238
+ 'the deepseek-official provider id changed',
239
+ )
240
+ mustInclude(
241
+ archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek-account/lib/index.js'),
242
+ '"deepseek-account"',
243
+ 'the deepseek-account provider id changed',
244
+ )
245
+ return 'endpoint + two provider ids'
246
+ })
247
+
248
+ check('the client module loader still accepts this plugin\u2019s bundle format', () => {
249
+ mustInclude(clientSourceOf(archive, 'dsh-client-modules', cache), '__ModuleLoader__', 'the __ModuleLoader__ facade is gone')
250
+ mustInclude(source, 'window.__ModuleLoader__.load(', 'this plugin no longer uses the facade')
251
+ return 'window.__ModuleLoader__.load({ id, factory })'
252
+ })
253
+
254
+ const failed = checks.filter((entry) => !entry.ok)
255
+ const width = Math.max(...checks.map((entry) => entry.label.length))
256
+ for (const entry of checks) {
257
+ console.log(`${entry.ok ? 'PASS' : 'FAIL'} ${entry.label.padEnd(width)} ${entry.detail}`)
258
+ }
259
+ console.log(`\n${checks.length - failed.length}/${checks.length} compatibility checks passed`)
260
+ if (failed.length > 0) {
261
+ console.error('\nThis Harness needs the plugin adapted before it can be trusted here.')
262
+ process.exitCode = 1
263
+ }
264
+ } finally {
265
+ archive.close()
266
+ }
267
+ }
268
+
269
+ main()
@@ -8,7 +8,10 @@
8
8
  * that never deletes the active file, and a tail that never starts mid-line.
9
9
  */
10
10
  import assert from 'node:assert/strict'
11
- import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
11
+ import {
12
+ closeSync, existsSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync,
13
+ rmSync, statSync, writeFileSync,
14
+ } from 'node:fs'
12
15
  import { basename, join } from 'node:path'
13
16
  import { tmpdir } from 'node:os'
14
17
  import test from 'node:test'
@@ -228,6 +231,31 @@ test('a fresh store reads the count of a file written by an earlier run', () =>
228
231
  }
229
232
  })
230
233
 
234
+ test('a persisted log is counted in bounded chunks instead of one large allocation', () => {
235
+ const { dir, cleanup } = withStore({ now: () => day('2026-10-06') })
236
+ try {
237
+ const file = join(dir, 'settings-search-2026-10-06.log')
238
+ writeFileSync(file, 'entry\n'.repeat(20000), 'utf8')
239
+ let largestRequest = 0
240
+ let reads = 0
241
+ const io = {
242
+ openSync,
243
+ closeSync,
244
+ readSync(fd, buffer, offset, length, position) {
245
+ largestRequest = Math.max(largestRequest, length)
246
+ reads += 1
247
+ return readSync(fd, buffer, offset, length, position)
248
+ },
249
+ }
250
+ const reopened = createLogStore({ dir, now: () => day('2026-10-06'), io })
251
+ assert.equal(reopened.count(), 20000)
252
+ assert.ok(largestRequest <= 64 * 1024, `requested ${largestRequest} bytes`)
253
+ assert.ok(reads > 1, 'the file must be streamed across multiple reads')
254
+ } finally {
255
+ cleanup()
256
+ }
257
+ })
258
+
231
259
  const send = async (route, method, body, query = '') => {
232
260
  const request = new Request(`http://dsh.internal${route.path}${query}`, {
233
261
  method,
@@ -268,7 +296,7 @@ test('the log route serves the tail and accepts append/clear only', async () =>
268
296
  try {
269
297
  assert.equal(route.path, '/api/settings-search-log')
270
298
  assert.deepEqual(route.methods, ['GET', 'POST'])
271
- assert.equal(route.requestBody, 'buffered')
299
+ assert.equal(route.requestBody, 'streaming')
272
300
 
273
301
  const appended = await send(route, 'POST', { action: 'append', lines: ['hello'] })
274
302
  assert.equal(appended.status, 200)
@@ -290,6 +318,28 @@ test('the log route serves the tail and accepts append/clear only', async () =>
290
318
  }
291
319
  })
292
320
 
321
+ test('an over-budget request body is rejected before any action runs', async () => {
322
+ let actions = 0
323
+ const route = createLogRoute({
324
+ dir: 'unused',
325
+ file: 'unused',
326
+ ensure() {},
327
+ append() { actions += 1; return 0 },
328
+ count() { actions += 1; return 0 },
329
+ clear() { actions += 1; return true },
330
+ open() { actions += 1; return { failure: undefined, command: 'test' } },
331
+ read() { actions += 1; return { text: '', size: 0, truncated: false } },
332
+ })
333
+ const request = new Request('http://dsh.internal/api/settings-search-log', {
334
+ method: 'POST',
335
+ headers: { 'content-type': 'application/json' },
336
+ body: JSON.stringify({ action: 'append', lines: ['x'.repeat(257 * 1024)] }),
337
+ })
338
+ const response = await route.fetch(request)
339
+ assert.equal(response.status, 400)
340
+ assert.equal(actions, 0)
341
+ })
342
+
293
343
  test('the folder launcher uses the OS shell open verb, never a hidden file manager', () => {
294
344
  const win = folderLaunchCommand('win32', 'C:\\logs\\dsh-settings-search')
295
345
  assert.equal(win.command, 'cmd.exe')