@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/everything.js
CHANGED
|
@@ -1,219 +1,277 @@
|
|
|
1
|
-
import { spawn, execFileSync } from 'node:child_process'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { isAbsolute } from 'node:path'
|
|
4
|
-
import { EngineError } from './errors.js'
|
|
5
|
-
import { filetimeToIso } from './format.js'
|
|
6
|
-
|
|
7
|
-
const ES_MAX_OUTPUT_BYTES = 8 * 1024 * 1024
|
|
8
|
-
const ES_MAX_STDERR_BYTES = 64 * 1024
|
|
9
|
-
const EXCLUDED = [/\\\$RECYCLE\.BIN\\/i, /\\System Volume Information\\/i]
|
|
10
|
-
|
|
11
|
-
let cachedEsPath
|
|
12
|
-
|
|
13
|
-
/** Decode es.exe stdout using the console code page (GBK on zh-CN). */
|
|
14
|
-
export function decodeEs(buffer, encoding = 'gbk') {
|
|
15
|
-
try {
|
|
16
|
-
return new TextDecoder(encoding).decode(buffer)
|
|
17
|
-
} catch {
|
|
18
|
-
return new TextDecoder('utf-8').decode(buffer)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Candidate es.exe locations, in priority order. */
|
|
23
|
-
export function esCandidates() {
|
|
24
|
-
const localAppData = process.env.LOCALAPPDATA || ''
|
|
25
|
-
return [
|
|
26
|
-
'C:\\Program Files\\Everything\\es.exe',
|
|
27
|
-
'C:\\Program Files (x86)\\Everything\\es.exe',
|
|
28
|
-
localAppData ? `${localAppData}\\Microsoft\\WindowsApps\\es.exe` : '',
|
|
29
|
-
].filter(Boolean)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Resolve es.exe from config, env, PATH, or common install directories. */
|
|
33
|
-
export function resolveEsPath(config = {}) {
|
|
34
|
-
if (config.esPath) {
|
|
35
|
-
if (!existsSync(config.esPath)) {
|
|
36
|
-
throw new EngineError(`es.exe not found at the configured esPath: ${config.esPath}`, 'EVERYTHING_ES_NOT_FOUND', {
|
|
37
|
-
hint: 'Point esPath at the es.exe download from https://www.voidtools.com/downloads/',
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
return config.esPath
|
|
41
|
-
}
|
|
42
|
-
if (process.env.ES_PATH && existsSync(process.env.ES_PATH)) return process.env.ES_PATH
|
|
43
|
-
if (cachedEsPath && existsSync(cachedEsPath)) return cachedEsPath
|
|
44
|
-
for (const candidate of esCandidates()) {
|
|
45
|
-
if (existsSync(candidate)) {
|
|
46
|
-
cachedEsPath = candidate
|
|
47
|
-
return candidate
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
const output = execFileSync('where.exe', ['es.exe'], {
|
|
52
|
-
encoding: 'utf8',
|
|
53
|
-
windowsHide: true,
|
|
54
|
-
timeout: 2000,
|
|
55
|
-
})
|
|
56
|
-
const first = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean)
|
|
57
|
-
if (first && existsSync(first)) {
|
|
58
|
-
cachedEsPath = first
|
|
59
|
-
return first
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// where.exe is unavailable or found nothing; fall through.
|
|
63
|
-
}
|
|
64
|
-
throw new EngineError('es.exe was not found', 'EVERYTHING_ES_NOT_FOUND', {
|
|
65
|
-
hint: 'Install es.exe (https://www.voidtools.com/downloads/), add it to PATH, or set esPath in the plugin config.',
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Build the fixed, read-only es.exe argv. Model text is always after `--`. */
|
|
70
|
-
export function buildEsArgs({ query, limit = 20, dir } = {}) {
|
|
71
|
-
if (typeof query !== 'string' || query.trim().length === 0) {
|
|
72
|
-
throw new EngineError('query must be a non-empty string', 'INVALID_ARGUMENT')
|
|
73
|
-
}
|
|
74
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
75
|
-
throw new EngineError('limit must be a positive integer', 'INVALID_ARGUMENT')
|
|
76
|
-
}
|
|
77
|
-
const args = ['-n', String(limit), '-json', '-size', '-date-modified']
|
|
78
|
-
if (dir) {
|
|
79
|
-
if (!isAbsolute(dir) || !existsSync(dir)) {
|
|
80
|
-
throw new EngineError(`dir is not an existing absolute directory: ${dir}`, 'ROOT_NOT_FOUND')
|
|
81
|
-
}
|
|
82
|
-
args.push('-path', dir)
|
|
83
|
-
}
|
|
84
|
-
args.push('--', query)
|
|
85
|
-
return args
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Run es.exe with a bounded buffer, timeout, and cancellation. */
|
|
89
|
-
export function runEsText(args, { esPath, timeoutMs = 15000, signal, encoding = 'gbk' } = {}) {
|
|
90
|
-
return new Promise((resolve, reject) => {
|
|
91
|
-
const executable = esPath || resolveEsPath({})
|
|
92
|
-
let settled = false
|
|
93
|
-
let timer
|
|
94
|
-
let child
|
|
95
|
-
|
|
96
|
-
const cleanup = () => {
|
|
97
|
-
if (timer) clearTimeout(timer)
|
|
98
|
-
if (signal) signal.removeEventListener('abort', onAbort)
|
|
99
|
-
}
|
|
100
|
-
const finish = (error, text) => {
|
|
101
|
-
if (settled) return
|
|
102
|
-
settled = true
|
|
103
|
-
cleanup()
|
|
104
|
-
if (error) reject(error)
|
|
105
|
-
else resolve(text)
|
|
106
|
-
}
|
|
107
|
-
const stop = (error) => {
|
|
108
|
-
try {
|
|
109
|
-
child?.kill()
|
|
110
|
-
} catch {
|
|
111
|
-
// Process already gone.
|
|
112
|
-
}
|
|
113
|
-
finish(error)
|
|
114
|
-
}
|
|
115
|
-
const onAbort = () =>
|
|
116
|
-
stop(new EngineError('Everything search was aborted', 'EVERYTHING_ABORTED'))
|
|
117
|
-
|
|
118
|
-
if (signal?.aborted) {
|
|
119
|
-
onAbort()
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
child = spawn(executable, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
125
|
-
} catch (error) {
|
|
126
|
-
finish(
|
|
127
|
-
new EngineError(`cannot start es.exe: ${error.message}`, 'EVERYTHING_DOWN', {
|
|
128
|
-
cause: error,
|
|
129
|
-
hint: 'Check that Everything/es.exe is installed and that esPath is correct.',
|
|
130
|
-
}),
|
|
131
|
-
)
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const stdout = []
|
|
136
|
-
const stderr = []
|
|
137
|
-
let stdoutBytes = 0
|
|
138
|
-
let stderrBytes = 0
|
|
139
|
-
|
|
140
|
-
timer = setTimeout(() => {
|
|
141
|
-
stop(new EngineError('Everything search timed out', 'EVERYTHING_TIMEOUT', { hint: 'Narrow the query or raise timeoutMs.' }))
|
|
142
|
-
}, timeoutMs)
|
|
143
|
-
signal?.addEventListener('abort', onAbort, { once: true })
|
|
144
|
-
|
|
145
|
-
child.stdout.on('data', (chunk) => {
|
|
146
|
-
stdoutBytes += chunk.length
|
|
147
|
-
if (stdoutBytes > ES_MAX_OUTPUT_BYTES) {
|
|
148
|
-
stop(new EngineError('Everything output exceeded 8 MB', 'EVERYTHING_TOO_LARGE', { hint: 'Narrow the query.' }))
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
stdout.push(chunk)
|
|
152
|
-
})
|
|
153
|
-
child.stderr.on('data', (chunk) => {
|
|
154
|
-
if (stderrBytes >= ES_MAX_STDERR_BYTES) return
|
|
155
|
-
stderrBytes += chunk.length
|
|
156
|
-
stderr.push(chunk)
|
|
157
|
-
})
|
|
158
|
-
child.on('error', (error) => {
|
|
159
|
-
const code = error.code === 'ENOENT' ? 'EVERYTHING_ES_NOT_FOUND' : 'EVERYTHING_DOWN'
|
|
160
|
-
stop(
|
|
161
|
-
new EngineError(`cannot start es.exe: ${error.message}`, code, {
|
|
162
|
-
cause: error,
|
|
163
|
-
hint: 'Start the Everything executable and verify esPath.',
|
|
164
|
-
}),
|
|
165
|
-
)
|
|
166
|
-
})
|
|
167
|
-
child.on('close', (code) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { isAbsolute } from 'node:path'
|
|
4
|
+
import { EngineError } from './errors.js'
|
|
5
|
+
import { filetimeToIso } from './format.js'
|
|
6
|
+
|
|
7
|
+
const ES_MAX_OUTPUT_BYTES = 8 * 1024 * 1024
|
|
8
|
+
const ES_MAX_STDERR_BYTES = 64 * 1024
|
|
9
|
+
const EXCLUDED = [/\\\$RECYCLE\.BIN\\/i, /\\System Volume Information\\/i]
|
|
10
|
+
|
|
11
|
+
let cachedEsPath
|
|
12
|
+
|
|
13
|
+
/** Decode es.exe stdout using the console code page (GBK on zh-CN). */
|
|
14
|
+
export function decodeEs(buffer, encoding = 'gbk') {
|
|
15
|
+
try {
|
|
16
|
+
return new TextDecoder(encoding).decode(buffer)
|
|
17
|
+
} catch {
|
|
18
|
+
return new TextDecoder('utf-8').decode(buffer)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Candidate es.exe locations, in priority order. */
|
|
23
|
+
export function esCandidates() {
|
|
24
|
+
const localAppData = process.env.LOCALAPPDATA || ''
|
|
25
|
+
return [
|
|
26
|
+
'C:\\Program Files\\Everything\\es.exe',
|
|
27
|
+
'C:\\Program Files (x86)\\Everything\\es.exe',
|
|
28
|
+
localAppData ? `${localAppData}\\Microsoft\\WindowsApps\\es.exe` : '',
|
|
29
|
+
].filter(Boolean)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve es.exe from config, env, PATH, or common install directories. */
|
|
33
|
+
export function resolveEsPath(config = {}) {
|
|
34
|
+
if (config.esPath) {
|
|
35
|
+
if (!existsSync(config.esPath)) {
|
|
36
|
+
throw new EngineError(`es.exe not found at the configured esPath: ${config.esPath}`, 'EVERYTHING_ES_NOT_FOUND', {
|
|
37
|
+
hint: 'Point esPath at the es.exe download from https://www.voidtools.com/downloads/',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
return config.esPath
|
|
41
|
+
}
|
|
42
|
+
if (process.env.ES_PATH && existsSync(process.env.ES_PATH)) return process.env.ES_PATH
|
|
43
|
+
if (cachedEsPath && existsSync(cachedEsPath)) return cachedEsPath
|
|
44
|
+
for (const candidate of esCandidates()) {
|
|
45
|
+
if (existsSync(candidate)) {
|
|
46
|
+
cachedEsPath = candidate
|
|
47
|
+
return candidate
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const output = execFileSync('where.exe', ['es.exe'], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
timeout: 2000,
|
|
55
|
+
})
|
|
56
|
+
const first = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean)
|
|
57
|
+
if (first && existsSync(first)) {
|
|
58
|
+
cachedEsPath = first
|
|
59
|
+
return first
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// where.exe is unavailable or found nothing; fall through.
|
|
63
|
+
}
|
|
64
|
+
throw new EngineError('es.exe was not found', 'EVERYTHING_ES_NOT_FOUND', {
|
|
65
|
+
hint: 'Install es.exe (https://www.voidtools.com/downloads/), add it to PATH, or set esPath in the plugin config.',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build the fixed, read-only es.exe argv. Model text is always after `--`. */
|
|
70
|
+
export function buildEsArgs({ query, limit = 20, dir } = {}) {
|
|
71
|
+
if (typeof query !== 'string' || query.trim().length === 0) {
|
|
72
|
+
throw new EngineError('query must be a non-empty string', 'INVALID_ARGUMENT')
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
75
|
+
throw new EngineError('limit must be a positive integer', 'INVALID_ARGUMENT')
|
|
76
|
+
}
|
|
77
|
+
const args = ['-n', String(limit), '-json', '-size', '-date-modified']
|
|
78
|
+
if (dir) {
|
|
79
|
+
if (!isAbsolute(dir) || !existsSync(dir)) {
|
|
80
|
+
throw new EngineError(`dir is not an existing absolute directory: ${dir}`, 'ROOT_NOT_FOUND')
|
|
81
|
+
}
|
|
82
|
+
args.push('-path', dir)
|
|
83
|
+
}
|
|
84
|
+
args.push('--', query)
|
|
85
|
+
return args
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Run es.exe with a bounded buffer, timeout, and cancellation. */
|
|
89
|
+
export function runEsText(args, { esPath, timeoutMs = 15000, signal, encoding = 'gbk' } = {}) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const executable = esPath || resolveEsPath({})
|
|
92
|
+
let settled = false
|
|
93
|
+
let timer
|
|
94
|
+
let child
|
|
95
|
+
|
|
96
|
+
const cleanup = () => {
|
|
97
|
+
if (timer) clearTimeout(timer)
|
|
98
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
99
|
+
}
|
|
100
|
+
const finish = (error, text) => {
|
|
101
|
+
if (settled) return
|
|
102
|
+
settled = true
|
|
103
|
+
cleanup()
|
|
104
|
+
if (error) reject(error)
|
|
105
|
+
else resolve(text)
|
|
106
|
+
}
|
|
107
|
+
const stop = (error) => {
|
|
108
|
+
try {
|
|
109
|
+
child?.kill()
|
|
110
|
+
} catch {
|
|
111
|
+
// Process already gone.
|
|
112
|
+
}
|
|
113
|
+
finish(error)
|
|
114
|
+
}
|
|
115
|
+
const onAbort = () =>
|
|
116
|
+
stop(new EngineError('Everything search was aborted', 'EVERYTHING_ABORTED'))
|
|
117
|
+
|
|
118
|
+
if (signal?.aborted) {
|
|
119
|
+
onAbort()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
child = spawn(executable, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
125
|
+
} catch (error) {
|
|
126
|
+
finish(
|
|
127
|
+
new EngineError(`cannot start es.exe: ${error.message}`, 'EVERYTHING_DOWN', {
|
|
128
|
+
cause: error,
|
|
129
|
+
hint: 'Check that Everything/es.exe is installed and that esPath is correct.',
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const stdout = []
|
|
136
|
+
const stderr = []
|
|
137
|
+
let stdoutBytes = 0
|
|
138
|
+
let stderrBytes = 0
|
|
139
|
+
|
|
140
|
+
timer = setTimeout(() => {
|
|
141
|
+
stop(new EngineError('Everything search timed out', 'EVERYTHING_TIMEOUT', { hint: 'Narrow the query or raise timeoutMs.' }))
|
|
142
|
+
}, timeoutMs)
|
|
143
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
144
|
+
|
|
145
|
+
child.stdout.on('data', (chunk) => {
|
|
146
|
+
stdoutBytes += chunk.length
|
|
147
|
+
if (stdoutBytes > ES_MAX_OUTPUT_BYTES) {
|
|
148
|
+
stop(new EngineError('Everything output exceeded 8 MB', 'EVERYTHING_TOO_LARGE', { hint: 'Narrow the query.' }))
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
stdout.push(chunk)
|
|
152
|
+
})
|
|
153
|
+
child.stderr.on('data', (chunk) => {
|
|
154
|
+
if (stderrBytes >= ES_MAX_STDERR_BYTES) return
|
|
155
|
+
stderrBytes += chunk.length
|
|
156
|
+
stderr.push(chunk)
|
|
157
|
+
})
|
|
158
|
+
child.on('error', (error) => {
|
|
159
|
+
const code = error.code === 'ENOENT' ? 'EVERYTHING_ES_NOT_FOUND' : 'EVERYTHING_DOWN'
|
|
160
|
+
stop(
|
|
161
|
+
new EngineError(`cannot start es.exe: ${error.message}`, code, {
|
|
162
|
+
cause: error,
|
|
163
|
+
hint: 'Start the Everything executable and verify esPath.',
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
child.on('close', (code) => {
|
|
168
|
+
const output = decodeEs(Buffer.concat(stdout), encoding).trim()
|
|
169
|
+
if (isFailedExit(code, output)) {
|
|
170
|
+
const detail = decodeEs(Buffer.concat(stderr), encoding).trim()
|
|
171
|
+
return finish(
|
|
172
|
+
new EngineError(`es.exe exited ${code}${detail ? `: ${detail}` : ''}`, 'EVERYTHING_DOWN', {
|
|
173
|
+
hint: 'Start the Everything executable, then retry.',
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
finish(undefined, output)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether an es.exe exit means "no answer" rather than "no match".
|
|
184
|
+
*
|
|
185
|
+
* es.exe uses exit 1 both for "nothing matched" and for a failed connection, so
|
|
186
|
+
* the code alone cannot decide; an empty stdout next to stderr text is the
|
|
187
|
+
* failure, anything else is a result.
|
|
188
|
+
* @param {number} code - the process exit code
|
|
189
|
+
* @param {string} output - trimmed stdout
|
|
190
|
+
* @returns true when the call produced no usable answer
|
|
191
|
+
*/
|
|
192
|
+
export function isFailedExit(code, output) {
|
|
193
|
+
return code !== 0 && (code !== 1 || output === '')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Parse es.exe `-json` output into rows, before any exclusion filter. */
|
|
197
|
+
export function parseEsJson(text) {
|
|
198
|
+
if (!text || text.trim().length === 0) return []
|
|
199
|
+
let rows
|
|
200
|
+
try {
|
|
201
|
+
rows = JSON.parse(text)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new EngineError('es.exe returned unparsable JSON', 'EVERYTHING_BAD_OUTPUT', { cause: error })
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(rows)) {
|
|
206
|
+
throw new EngineError('es.exe JSON output was not an array', 'EVERYTHING_BAD_OUTPUT')
|
|
207
|
+
}
|
|
208
|
+
return rows
|
|
209
|
+
.filter((row) => row && typeof row.filename === 'string')
|
|
210
|
+
.map((row) => {
|
|
211
|
+
const item = { path: row.filename }
|
|
212
|
+
if (typeof row.size === 'number') item.size = row.size
|
|
213
|
+
const modified = filetimeToIso(row.date_modified)
|
|
214
|
+
if (modified) item.modified = modified
|
|
215
|
+
return item
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Drop rows that live in an index we never surface.
|
|
221
|
+
* @param {Array<{path: string}>} rows - parsed rows
|
|
222
|
+
* @returns {Array<{path: string}>} the rows a tool may return
|
|
223
|
+
*/
|
|
224
|
+
export function withoutExcludedPaths(rows) {
|
|
225
|
+
return rows.filter((row) => !EXCLUDED.some((re) => re.test(row.path)))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Candidate Everything.exe locations, in priority order. */
|
|
229
|
+
export function everythingCandidates() {
|
|
230
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files'
|
|
231
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'
|
|
232
|
+
return [`${programFiles}\\Everything\\Everything.exe`, `${programFilesX86}\\Everything\\Everything.exe`]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Whether Everything itself is installed, independent of es.exe.
|
|
237
|
+
*
|
|
238
|
+
* `sc query` exits non-zero when the service is absent, which is the one signal
|
|
239
|
+
* that survives a localized Windows console. An install outside Program Files
|
|
240
|
+
* (or a portable one) is only found through that service, so a check that could
|
|
241
|
+
* not run at all reports `known: false` instead of claiming "not installed".
|
|
242
|
+
* @returns {{installed: boolean, via?: 'exe'|'service', known: boolean}} install evidence
|
|
243
|
+
*/
|
|
244
|
+
export function everythingInstalled() {
|
|
245
|
+
for (const candidate of everythingCandidates()) {
|
|
246
|
+
if (existsSync(candidate)) return { installed: true, via: 'exe', known: true }
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
execFileSync('sc.exe', ['query', 'Everything'], { encoding: 'utf8', windowsHide: true, timeout: 2000 })
|
|
250
|
+
return { installed: true, via: 'service', known: true }
|
|
251
|
+
} catch (error) {
|
|
252
|
+
// A numeric `status` means sc.exe ran and the service is absent; a spawn
|
|
253
|
+
// failure (EPERM, ENOENT) proves nothing about the machine.
|
|
254
|
+
const ran = typeof error?.status === 'number'
|
|
255
|
+
return { installed: false, known: ran }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Search file names/paths through Everything. */
|
|
260
|
+
export async function searchFiles({ query, dir, limit = 20, esPath, timeoutMs, signal, encoding } = {}) {
|
|
261
|
+
const executable = esPath || resolveEsPath({})
|
|
262
|
+
const args = buildEsArgs({ query, dir, limit })
|
|
263
|
+
const text = await runEsText(args, { esPath: executable, timeoutMs, signal, encoding })
|
|
264
|
+
const rows = parseEsJson(text)
|
|
265
|
+
// Truncation is decided on what es.exe returned, not on what survived the
|
|
266
|
+
// $RECYCLE.BIN filter: a capped page of excluded hits must not read as a
|
|
267
|
+
// plain "no files found".
|
|
268
|
+
return { items: withoutExcludedPaths(rows), truncated: rows.length >= limit }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Read the running Everything version. */
|
|
272
|
+
export async function getEsVersion({ esPath, timeoutMs, signal, encoding } = {}) {
|
|
273
|
+
const executable = esPath || resolveEsPath({})
|
|
274
|
+
const text = await runEsText(['-get-everything-version'], { esPath: executable, timeoutMs, signal, encoding })
|
|
275
|
+
if (!text) throw new EngineError('Everything version probe returned nothing', 'EVERYTHING_DOWN')
|
|
276
|
+
return text
|
|
277
|
+
}
|