@fishdivinity/dsh-local-search 0.1.0 → 0.2.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.
- package/LICENSE +21 -21
- package/README-zh.md +167 -166
- package/README.md +177 -176
- package/cordis.patch.yml +9 -9
- package/icon.svg +35 -7
- package/lib/anytxt.js +272 -252
- package/lib/client.js +623 -0
- package/lib/config.js +101 -0
- package/lib/engines.js +85 -85
- package/lib/errors.js +18 -18
- package/lib/everything.js +277 -219
- package/lib/format.js +101 -92
- package/lib/index.js +77 -71
- package/lib/limits.js +14 -0
- package/lib/probe.js +88 -0
- package/lib/ref.js +38 -38
- package/lib/roots.js +114 -86
- package/lib/route.js +170 -0
- package/lib/tools.js +498 -506
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +86 -65
package/lib/config.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config access shared by activation and every tool call.
|
|
3
|
+
*
|
|
4
|
+
* The fields the Plugins page may edit are declared `.volatile()` in the
|
|
5
|
+
* schema, so the Host hands `apply` a live reference instead of the value
|
|
6
|
+
* frozen at activation. A save on the page updates that reference in place;
|
|
7
|
+
* only `get()` sees it. Nothing downstream may keep such a reference, so every
|
|
8
|
+
* operation starts from {@link readConfig}'s plain snapshot.
|
|
9
|
+
*
|
|
10
|
+
* `timeoutMs` is deliberately not in {@link LIVE_KEYS}: `defineTool` fixes each
|
|
11
|
+
* tool's deadline when the tool is registered, so a new value could not take
|
|
12
|
+
* effect without reloading the row. It stays a profile-YAML setting.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Config keys the Plugins page edits. */
|
|
16
|
+
export const LIVE_KEYS = [
|
|
17
|
+
'esPath',
|
|
18
|
+
'esEncoding',
|
|
19
|
+
'anytxtUrl',
|
|
20
|
+
'workspaceRoot',
|
|
21
|
+
'workRoot',
|
|
22
|
+
'personalRoot',
|
|
23
|
+
'allRoots',
|
|
24
|
+
'maxResults',
|
|
25
|
+
'readMaxChars',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read a value that may still be a live volatile reference.
|
|
30
|
+
* @param {unknown} value - plain value, or a reference carrying `get()`
|
|
31
|
+
* @returns {unknown} the current value
|
|
32
|
+
*/
|
|
33
|
+
export function liveValue(value) {
|
|
34
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot the whole config as plain values.
|
|
39
|
+
* @param {object} config - schema output, volatile references included
|
|
40
|
+
* @returns {object} plain values for this operation
|
|
41
|
+
*/
|
|
42
|
+
export function readConfig(config) {
|
|
43
|
+
const snapshot = {}
|
|
44
|
+
for (const key of [...LIVE_KEYS, 'timeoutMs']) snapshot[key] = liveValue(config[key])
|
|
45
|
+
return snapshot
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertPositiveInteger(field, value) {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
50
|
+
throw new Error(`dsh-local-search: ${field} must be a positive integer`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Hosts that name this machine. */
|
|
55
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether a value is an `http(s)` URL that names this machine.
|
|
59
|
+
*
|
|
60
|
+
* Both indexes describe the local machine: `es.exe` is spawned here, and every
|
|
61
|
+
* Anytxt result carries a path that only resolves here. A remote endpoint would
|
|
62
|
+
* answer with another machine's paths, so it is refused rather than supported —
|
|
63
|
+
* tunnelling one in lands it on loopback anyway.
|
|
64
|
+
* @param {unknown} value - candidate endpoint
|
|
65
|
+
* @returns {boolean} true for a loopback `http(s)` URL
|
|
66
|
+
*/
|
|
67
|
+
export function isLoopbackUrl(value) {
|
|
68
|
+
let parsed
|
|
69
|
+
try {
|
|
70
|
+
parsed = new URL(String(value))
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
|
|
75
|
+
return LOOPBACK_HOSTS.has(parsed.hostname.replace(/^\[|\]$/g, ''))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate a candidate config and return its plain snapshot. Run once at
|
|
80
|
+
* activation; the page's writes are validated by the Host against the Config
|
|
81
|
+
* schema before they are persisted, so the range checks that matter live there
|
|
82
|
+
* (`lib/index.js`) and this function covers what the schema cannot express.
|
|
83
|
+
*
|
|
84
|
+
* The endpoint's loopback rule is deliberately *not* enforced here. A profile
|
|
85
|
+
* that 0.1 accepted must still activate: a wrong address should fail the content
|
|
86
|
+
* engine with a readable error, not take file-name search down with it. The
|
|
87
|
+
* rule lives where the endpoint is used (`lib/anytxt.js`).
|
|
88
|
+
* @param {object} config - schema output, volatile references included
|
|
89
|
+
* @param {number} [maxTimerDelayMs] - largest deadline the timer API accepts
|
|
90
|
+
* @returns {object} the validated plain snapshot
|
|
91
|
+
*/
|
|
92
|
+
export function validateConfig(config, maxTimerDelayMs) {
|
|
93
|
+
const values = readConfig(config)
|
|
94
|
+
assertPositiveInteger('maxResults', values.maxResults)
|
|
95
|
+
assertPositiveInteger('timeoutMs', values.timeoutMs)
|
|
96
|
+
assertPositiveInteger('readMaxChars', values.readMaxChars)
|
|
97
|
+
if (maxTimerDelayMs !== undefined && values.timeoutMs > maxTimerDelayMs) {
|
|
98
|
+
throw new Error(`dsh-local-search: timeoutMs must be no greater than ${maxTimerDelayMs}`)
|
|
99
|
+
}
|
|
100
|
+
return values
|
|
101
|
+
}
|
package/lib/engines.js
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { EngineError } from './errors.js'
|
|
2
|
-
import { getEsVersion, resolveEsPath, searchFiles } from './everything.js'
|
|
3
|
-
import { anytxtStatus, getFragments, getRawText, searchContent } from './anytxt.js'
|
|
4
|
-
import { assertRefEngine, formatRef, parseRef } from './ref.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Engine registry. A new filename or content backend is one entry here plus
|
|
8
|
-
* its module; the tool surface and `engine` values stay unchanged.
|
|
9
|
-
*
|
|
10
|
-
* files: { name, search(args), status(args) }
|
|
11
|
-
* content: { name, search(args), fragments(args), read(args), status(args) }
|
|
12
|
-
*/
|
|
13
|
-
const filesEngines = new Map([
|
|
14
|
-
[
|
|
15
|
-
'everything',
|
|
16
|
-
{
|
|
17
|
-
name: 'everything',
|
|
18
|
-
search: (args) => searchFiles(args),
|
|
19
|
-
status: async (args) => ({
|
|
20
|
-
ok: true,
|
|
21
|
-
version: await getEsVersion(args),
|
|
22
|
-
path: resolveEsPath(args.config ?? {}),
|
|
23
|
-
}),
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
const contentEngines = new Map([
|
|
29
|
-
[
|
|
30
|
-
'anytxt',
|
|
31
|
-
{
|
|
32
|
-
name: 'anytxt',
|
|
33
|
-
async search(args) {
|
|
34
|
-
const result = await searchContent(args)
|
|
35
|
-
return {
|
|
36
|
-
...result,
|
|
37
|
-
items: result.items.map((item) => ({
|
|
38
|
-
...item,
|
|
39
|
-
engine: 'anytxt',
|
|
40
|
-
ref: formatRef('anytxt', item.ref),
|
|
41
|
-
})),
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
fragments(args) {
|
|
45
|
-
const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
|
|
46
|
-
return getFragments({ ...args, ref: parsed.id })
|
|
47
|
-
},
|
|
48
|
-
read(args) {
|
|
49
|
-
const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
|
|
50
|
-
return getRawText({ ...args, ref: parsed.id })
|
|
51
|
-
},
|
|
52
|
-
status: (args) => anytxtStatus(args),
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
])
|
|
56
|
-
|
|
57
|
-
function resolve(registry, kind, requested) {
|
|
58
|
-
const name = requested || 'auto'
|
|
59
|
-
if (name === 'auto') return registry.values().next().value
|
|
60
|
-
const engine = registry.get(name)
|
|
61
|
-
if (!engine) {
|
|
62
|
-
throw new EngineError(`unknown ${kind} engine: ${name}`, 'ENGINE_UNSUPPORTED', {
|
|
63
|
-
hint: `Use auto, ${[...registry.keys()].join(', ')}.`,
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
return engine
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Resolve a filename engine; `auto` selects the first registered engine. */
|
|
70
|
-
export function resolveFilesEngine(requested = 'auto') {
|
|
71
|
-
return resolve(filesEngines, 'files', requested)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Resolve a content engine; `auto` selects the first registered engine. */
|
|
75
|
-
export function resolveContentEngine(requested = 'auto') {
|
|
76
|
-
return resolve(contentEngines, 'content', requested)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Engine names for status output and future config pages. */
|
|
80
|
-
export function engineNames() {
|
|
81
|
-
return {
|
|
82
|
-
files: [...filesEngines.keys()],
|
|
83
|
-
content: [...contentEngines.keys()],
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
import { EngineError } from './errors.js'
|
|
2
|
+
import { getEsVersion, resolveEsPath, searchFiles } from './everything.js'
|
|
3
|
+
import { anytxtStatus, getFragments, getRawText, searchContent } from './anytxt.js'
|
|
4
|
+
import { assertRefEngine, formatRef, parseRef } from './ref.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Engine registry. A new filename or content backend is one entry here plus
|
|
8
|
+
* its module; the tool surface and `engine` values stay unchanged.
|
|
9
|
+
*
|
|
10
|
+
* files: { name, search(args), status(args) }
|
|
11
|
+
* content: { name, search(args), fragments(args), read(args), status(args) }
|
|
12
|
+
*/
|
|
13
|
+
const filesEngines = new Map([
|
|
14
|
+
[
|
|
15
|
+
'everything',
|
|
16
|
+
{
|
|
17
|
+
name: 'everything',
|
|
18
|
+
search: (args) => searchFiles(args),
|
|
19
|
+
status: async (args) => ({
|
|
20
|
+
ok: true,
|
|
21
|
+
version: await getEsVersion(args),
|
|
22
|
+
path: resolveEsPath(args.config ?? {}),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const contentEngines = new Map([
|
|
29
|
+
[
|
|
30
|
+
'anytxt',
|
|
31
|
+
{
|
|
32
|
+
name: 'anytxt',
|
|
33
|
+
async search(args) {
|
|
34
|
+
const result = await searchContent(args)
|
|
35
|
+
return {
|
|
36
|
+
...result,
|
|
37
|
+
items: result.items.map((item) => ({
|
|
38
|
+
...item,
|
|
39
|
+
engine: 'anytxt',
|
|
40
|
+
ref: formatRef('anytxt', item.ref),
|
|
41
|
+
})),
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
fragments(args) {
|
|
45
|
+
const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
|
|
46
|
+
return getFragments({ ...args, ref: parsed.id })
|
|
47
|
+
},
|
|
48
|
+
read(args) {
|
|
49
|
+
const parsed = assertRefEngine(parseRef(args.ref), 'anytxt')
|
|
50
|
+
return getRawText({ ...args, ref: parsed.id })
|
|
51
|
+
},
|
|
52
|
+
status: (args) => anytxtStatus(args),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
function resolve(registry, kind, requested) {
|
|
58
|
+
const name = requested || 'auto'
|
|
59
|
+
if (name === 'auto') return registry.values().next().value
|
|
60
|
+
const engine = registry.get(name)
|
|
61
|
+
if (!engine) {
|
|
62
|
+
throw new EngineError(`unknown ${kind} engine: ${name}`, 'ENGINE_UNSUPPORTED', {
|
|
63
|
+
hint: `Use auto, ${[...registry.keys()].join(', ')}.`,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
return engine
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve a filename engine; `auto` selects the first registered engine. */
|
|
70
|
+
export function resolveFilesEngine(requested = 'auto') {
|
|
71
|
+
return resolve(filesEngines, 'files', requested)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Resolve a content engine; `auto` selects the first registered engine. */
|
|
75
|
+
export function resolveContentEngine(requested = 'auto') {
|
|
76
|
+
return resolve(contentEngines, 'content', requested)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Engine names for status output and future config pages. */
|
|
80
|
+
export function engineNames() {
|
|
81
|
+
return {
|
|
82
|
+
files: [...filesEngines.keys()],
|
|
83
|
+
content: [...contentEngines.keys()],
|
|
84
|
+
}
|
|
85
|
+
}
|
package/lib/errors.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Engine-level error with a stable code. Kept independent of the DSH runtime
|
|
3
|
-
* so engine modules and their unit tests can run under plain Node; the tool
|
|
4
|
-
* layer converts these to `HarnessError` before returning to the model.
|
|
5
|
-
*/
|
|
6
|
-
export class EngineError extends Error {
|
|
7
|
-
/**
|
|
8
|
-
* @param {string} message
|
|
9
|
-
* @param {string} code
|
|
10
|
-
* @param {{ cause?: unknown, hint?: string }} [options]
|
|
11
|
-
*/
|
|
12
|
-
constructor(message, code, options = {}) {
|
|
13
|
-
super(message, { cause: options.cause })
|
|
14
|
-
this.name = 'EngineError'
|
|
15
|
-
this.code = code
|
|
16
|
-
this.hint = options.hint
|
|
17
|
-
}
|
|
18
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Engine-level error with a stable code. Kept independent of the DSH runtime
|
|
3
|
+
* so engine modules and their unit tests can run under plain Node; the tool
|
|
4
|
+
* layer converts these to `HarnessError` before returning to the model.
|
|
5
|
+
*/
|
|
6
|
+
export class EngineError extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} message
|
|
9
|
+
* @param {string} code
|
|
10
|
+
* @param {{ cause?: unknown, hint?: string }} [options]
|
|
11
|
+
*/
|
|
12
|
+
constructor(message, code, options = {}) {
|
|
13
|
+
super(message, { cause: options.cause })
|
|
14
|
+
this.name = 'EngineError'
|
|
15
|
+
this.code = code
|
|
16
|
+
this.hint = options.hint
|
|
17
|
+
}
|
|
18
|
+
}
|