pinokiod 7.3.14 → 7.4.0
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/kernel/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/public/logs.js +15 -1
- package/server/public/style.css +99 -31
- package/server/views/app.ejs +263 -159
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/logs.ejs +14 -12
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- package/spec/INSTRUCTION_SYNC.md +0 -432
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const fsp = fs.promises
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const { execFileSync, spawn } = require('node:child_process')
|
|
8
|
+
const { performance } = require('node:perf_hooks')
|
|
9
|
+
|
|
10
|
+
const ignore = require('ignore')
|
|
11
|
+
|
|
12
|
+
const CURRENT_GITIGNORE_SCAN_SKIP_DIRS = new Set(['.git', 'node_modules', 'venv', '.venv'])
|
|
13
|
+
const CURRENT_REPO_SCAN_SKIP_DIRS = new Set(['node_modules', 'venv'])
|
|
14
|
+
|
|
15
|
+
const GIT_STATUS_IGNORE_PATTERNS = [
|
|
16
|
+
/(^|\/)node_modules\//,
|
|
17
|
+
/(^|\/)__pycache__\//,
|
|
18
|
+
/(^|\/)\.cache\//,
|
|
19
|
+
/(^|\/)\.ruff_cache\//,
|
|
20
|
+
/(^|\/)\.tox\//,
|
|
21
|
+
/(^|\/)\.terraform\//,
|
|
22
|
+
/(^|\/)\.parcel-cache\//,
|
|
23
|
+
/(^|\/)\.webpack\//,
|
|
24
|
+
/(^|\/)\.mypy_cache\//,
|
|
25
|
+
/(^|\/)\.pytest_cache\//,
|
|
26
|
+
/(^|\/)\.git\//,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
return [
|
|
31
|
+
'Usage: node test/workspace-gitignore-benchmark.js [options]',
|
|
32
|
+
'',
|
|
33
|
+
'Options:',
|
|
34
|
+
' --api-root <path> API root. Defaults to $PINOKIO_API_ROOT or ~/pinokio/api.',
|
|
35
|
+
' --apps <list> Comma-separated top-level app folders. Defaults to all folders.',
|
|
36
|
+
' --max-apps <n> Limit number of app folders after sorting.',
|
|
37
|
+
' --out <path> Output JSON path. Defaults to ~/.codex/benchmarks/pinokiod-gitignore/gitignore-<timestamp>.json.',
|
|
38
|
+
' --markdown-out <path> Output Markdown summary path. Defaults to JSON path with .md extension.',
|
|
39
|
+
' --no-markdown Do not write a Markdown summary.',
|
|
40
|
+
' --help Show this help.',
|
|
41
|
+
'',
|
|
42
|
+
'This is a focused component benchmark for replacing the workspace .gitignore pre-scan.',
|
|
43
|
+
'It does not prove the runtime route no longer calls the pre-scan; route instrumentation must verify that after implementation.',
|
|
44
|
+
].join('\n')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const args = {
|
|
49
|
+
apiRoot: process.env.PINOKIO_API_ROOT || path.join(os.homedir(), 'pinokio', 'api'),
|
|
50
|
+
apps: null,
|
|
51
|
+
maxApps: null,
|
|
52
|
+
out: null,
|
|
53
|
+
markdownOut: null,
|
|
54
|
+
noMarkdown: false,
|
|
55
|
+
help: false,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const arg = argv[i]
|
|
60
|
+
const next = () => {
|
|
61
|
+
i += 1
|
|
62
|
+
if (i >= argv.length) throw new Error(`${arg} requires a value`)
|
|
63
|
+
return argv[i]
|
|
64
|
+
}
|
|
65
|
+
if (arg === '--help' || arg === '-h') {
|
|
66
|
+
args.help = true
|
|
67
|
+
} else if (arg === '--api-root') {
|
|
68
|
+
args.apiRoot = path.resolve(next())
|
|
69
|
+
} else if (arg === '--apps') {
|
|
70
|
+
args.apps = next().split(',').map((item) => item.trim()).filter(Boolean)
|
|
71
|
+
} else if (arg === '--max-apps') {
|
|
72
|
+
args.maxApps = Number.parseInt(next(), 10)
|
|
73
|
+
} else if (arg === '--out') {
|
|
74
|
+
args.out = path.resolve(next())
|
|
75
|
+
} else if (arg === '--markdown-out') {
|
|
76
|
+
args.markdownOut = path.resolve(next())
|
|
77
|
+
} else if (arg === '--no-markdown') {
|
|
78
|
+
args.noMarkdown = true
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (args.maxApps != null && (!Number.isInteger(args.maxApps) || args.maxApps < 1)) {
|
|
85
|
+
throw new Error('--max-apps must be a positive integer')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!args.out) {
|
|
89
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
90
|
+
args.out = path.join(os.homedir(), '.codex', 'benchmarks', 'pinokiod-gitignore', `gitignore-${stamp}.json`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!args.markdownOut && !args.noMarkdown) {
|
|
94
|
+
args.markdownOut = args.out.replace(/\.json$/i, '.md')
|
|
95
|
+
if (args.markdownOut === args.out) {
|
|
96
|
+
args.markdownOut = `${args.out}.md`
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return args
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizePath(value) {
|
|
104
|
+
return String(value || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function posixRelative(from, to) {
|
|
108
|
+
return normalizePath(path.relative(from, to))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function execFileText(command, args, options = {}) {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const started = performance.now()
|
|
114
|
+
const child = spawn(command, args, {
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
env: options.env || process.env,
|
|
117
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
|
+
})
|
|
119
|
+
let stdout = ''
|
|
120
|
+
let stderr = ''
|
|
121
|
+
let done = false
|
|
122
|
+
const timeoutMs = options.timeout || 30000
|
|
123
|
+
const timeout = setTimeout(() => {
|
|
124
|
+
if (!done) {
|
|
125
|
+
child.kill('SIGKILL')
|
|
126
|
+
}
|
|
127
|
+
}, timeoutMs)
|
|
128
|
+
|
|
129
|
+
child.stdout.on('data', (chunk) => {
|
|
130
|
+
stdout += chunk.toString()
|
|
131
|
+
if (stdout.length > (options.maxBuffer || 50 * 1024 * 1024)) {
|
|
132
|
+
child.kill('SIGKILL')
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
child.stderr.on('data', (chunk) => {
|
|
136
|
+
stderr += chunk.toString()
|
|
137
|
+
})
|
|
138
|
+
child.stdin.on('error', () => {
|
|
139
|
+
// Some git commands can close stdin before Node finishes writing a large batch.
|
|
140
|
+
// The process exit code and captured output are the source of truth.
|
|
141
|
+
})
|
|
142
|
+
child.on('error', (error) => {
|
|
143
|
+
if (done) return
|
|
144
|
+
done = true
|
|
145
|
+
clearTimeout(timeout)
|
|
146
|
+
resolve({
|
|
147
|
+
ok: false,
|
|
148
|
+
code: 1,
|
|
149
|
+
error: error ? (error.message || String(error)) : null,
|
|
150
|
+
stdout,
|
|
151
|
+
stderr,
|
|
152
|
+
ms: performance.now() - started,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
child.on('close', (code, signal) => {
|
|
156
|
+
if (done) return
|
|
157
|
+
done = true
|
|
158
|
+
clearTimeout(timeout)
|
|
159
|
+
resolve({
|
|
160
|
+
ok: code === 0,
|
|
161
|
+
code: typeof code === 'number' ? code : 1,
|
|
162
|
+
signal,
|
|
163
|
+
error: code === 0 ? null : `Command exited with code ${code}${signal ? ` signal ${signal}` : ''}`,
|
|
164
|
+
stdout,
|
|
165
|
+
stderr,
|
|
166
|
+
ms: performance.now() - started,
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
if (options.input != null) {
|
|
170
|
+
child.stdin.end(options.input)
|
|
171
|
+
} else {
|
|
172
|
+
child.stdin.end()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createEmptyWalkMetrics() {
|
|
178
|
+
return {
|
|
179
|
+
ms: 0,
|
|
180
|
+
dirs: 0,
|
|
181
|
+
readdir: 0,
|
|
182
|
+
readdirEntries: 0,
|
|
183
|
+
readFile: 0,
|
|
184
|
+
readFileBytes: 0,
|
|
185
|
+
gitignoreFiles: 0,
|
|
186
|
+
errors: 0,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function buildCurrentWorkspaceIgnoreEngine(workspaceRoot) {
|
|
191
|
+
const metrics = createEmptyWalkMetrics()
|
|
192
|
+
const engine = ignore()
|
|
193
|
+
const gitignoreFiles = []
|
|
194
|
+
const started = performance.now()
|
|
195
|
+
|
|
196
|
+
async function walk(dir) {
|
|
197
|
+
metrics.dirs += 1
|
|
198
|
+
let entries
|
|
199
|
+
try {
|
|
200
|
+
entries = await fsp.readdir(dir, { withFileTypes: true })
|
|
201
|
+
metrics.readdir += 1
|
|
202
|
+
metrics.readdirEntries += entries.length
|
|
203
|
+
} catch (_) {
|
|
204
|
+
metrics.errors += 1
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const child = path.join(dir, entry.name)
|
|
210
|
+
if (entry.isDirectory()) {
|
|
211
|
+
if (CURRENT_GITIGNORE_SCAN_SKIP_DIRS.has(entry.name)) {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
await walk(child)
|
|
215
|
+
} else if (entry.isFile() && entry.name === '.gitignore') {
|
|
216
|
+
gitignoreFiles.push(child)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await walk(workspaceRoot)
|
|
222
|
+
|
|
223
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
224
|
+
let content
|
|
225
|
+
try {
|
|
226
|
+
content = await fsp.readFile(gitignorePath, 'utf8')
|
|
227
|
+
metrics.readFile += 1
|
|
228
|
+
metrics.readFileBytes += Buffer.byteLength(content)
|
|
229
|
+
metrics.gitignoreFiles += 1
|
|
230
|
+
} catch (_) {
|
|
231
|
+
metrics.errors += 1
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
addGitignoreContent(engine, workspaceRoot, gitignorePath, content)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
metrics.ms = performance.now() - started
|
|
239
|
+
return { engine, metrics, gitignoreFiles }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addGitignoreContent(engine, root, gitignorePath, content) {
|
|
243
|
+
const relDir = path.relative(root, path.dirname(gitignorePath))
|
|
244
|
+
const prefix = relDir && relDir !== '.' ? normalizePath(relDir) + '/' : ''
|
|
245
|
+
for (let line of content.split(/\r?\n/)) {
|
|
246
|
+
if (!line) continue
|
|
247
|
+
line = line.trim()
|
|
248
|
+
if (!line || line.startsWith('#')) continue
|
|
249
|
+
let negated = false
|
|
250
|
+
if (line.startsWith('!')) {
|
|
251
|
+
negated = true
|
|
252
|
+
line = line.slice(1)
|
|
253
|
+
}
|
|
254
|
+
line = line.replace(/^\/+/, '')
|
|
255
|
+
if (!line) continue
|
|
256
|
+
const pattern = prefix + line
|
|
257
|
+
engine.add((negated ? '!' : '') + pattern)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function candidateIgnoredByPathScopedGitignores(workspaceRoot, records) {
|
|
262
|
+
const ignored = new Set()
|
|
263
|
+
const contentCache = new Map()
|
|
264
|
+
const metrics = {
|
|
265
|
+
ms: 0,
|
|
266
|
+
checked: records.length,
|
|
267
|
+
readFile: 0,
|
|
268
|
+
readFileBytes: 0,
|
|
269
|
+
}
|
|
270
|
+
const started = performance.now()
|
|
271
|
+
|
|
272
|
+
const readGitignore = async (gitignorePath) => {
|
|
273
|
+
if (contentCache.has(gitignorePath)) {
|
|
274
|
+
return contentCache.get(gitignorePath)
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const content = await fsp.readFile(gitignorePath, 'utf8')
|
|
278
|
+
contentCache.set(gitignorePath, content)
|
|
279
|
+
metrics.readFile += 1
|
|
280
|
+
metrics.readFileBytes += Buffer.byteLength(content)
|
|
281
|
+
return content
|
|
282
|
+
} catch (_) {
|
|
283
|
+
contentCache.set(gitignorePath, null)
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const record of records) {
|
|
289
|
+
const engine = ignore()
|
|
290
|
+
const workspaceRelative = normalizePath(record.workspaceRelative)
|
|
291
|
+
const parts = workspaceRelative.split('/').filter(Boolean)
|
|
292
|
+
let stopped = false
|
|
293
|
+
|
|
294
|
+
for (let depth = 0; depth < parts.length; depth++) {
|
|
295
|
+
const relDir = depth === 0 ? '' : parts.slice(0, depth).join('/')
|
|
296
|
+
const gitignorePath = path.join(workspaceRoot, relDir, '.gitignore')
|
|
297
|
+
const content = await readGitignore(gitignorePath)
|
|
298
|
+
if (content) {
|
|
299
|
+
addGitignoreContent(engine, workspaceRoot, gitignorePath, content)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const nextPart = parts[depth]
|
|
303
|
+
if (CURRENT_GITIGNORE_SCAN_SKIP_DIRS.has(nextPart)) {
|
|
304
|
+
stopped = true
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!stopped || parts.length > 0) {
|
|
310
|
+
if (workspaceRelative && engine.ignores(workspaceRelative)) {
|
|
311
|
+
ignored.add(record.key)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
metrics.ms = performance.now() - started
|
|
317
|
+
return { ignored, metrics }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function discoverReposCurrentStyle(workspaceRoot) {
|
|
321
|
+
const metrics = {
|
|
322
|
+
ms: 0,
|
|
323
|
+
dirs: 0,
|
|
324
|
+
readdir: 0,
|
|
325
|
+
readdirEntries: 0,
|
|
326
|
+
gitDirs: 0,
|
|
327
|
+
errors: 0,
|
|
328
|
+
}
|
|
329
|
+
const repos = []
|
|
330
|
+
const started = performance.now()
|
|
331
|
+
|
|
332
|
+
async function walk(dir) {
|
|
333
|
+
metrics.dirs += 1
|
|
334
|
+
let entries
|
|
335
|
+
try {
|
|
336
|
+
entries = await fsp.readdir(dir, { withFileTypes: true })
|
|
337
|
+
metrics.readdir += 1
|
|
338
|
+
metrics.readdirEntries += entries.length
|
|
339
|
+
} catch (_) {
|
|
340
|
+
metrics.errors += 1
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
if (!entry.isDirectory()) continue
|
|
346
|
+
const child = path.join(dir, entry.name)
|
|
347
|
+
if (entry.name === '.git') {
|
|
348
|
+
repos.push(path.dirname(child))
|
|
349
|
+
metrics.gitDirs += 1
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
if (CURRENT_REPO_SCAN_SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
await walk(child)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await walk(workspaceRoot)
|
|
360
|
+
metrics.ms = performance.now() - started
|
|
361
|
+
return { repos, metrics }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function extractGitStatusPath(statusLine) {
|
|
365
|
+
if (!statusLine || statusLine.length < 4) return ''
|
|
366
|
+
const rest = statusLine.slice(3)
|
|
367
|
+
const renameIdx = rest.indexOf(' -> ')
|
|
368
|
+
const candidate = renameIdx === -1 ? rest : rest.slice(renameIdx + 4)
|
|
369
|
+
return candidate.replace(/^"|"$/g, '')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function gitStatusRecords(workspaceRoot, repoRoot) {
|
|
373
|
+
const result = await execFileText('git', [
|
|
374
|
+
'-c',
|
|
375
|
+
'core.quotePath=false',
|
|
376
|
+
'status',
|
|
377
|
+
'--porcelain=v1',
|
|
378
|
+
'--untracked-files=all',
|
|
379
|
+
], {
|
|
380
|
+
cwd: repoRoot,
|
|
381
|
+
timeout: 30000,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const records = []
|
|
385
|
+
if (!result.ok) {
|
|
386
|
+
return { records, metrics: { ms: result.ms, ok: false, error: result.error } }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
390
|
+
if (!line || line.length < 4) continue
|
|
391
|
+
const repoRelative = normalizePath(extractGitStatusPath(line))
|
|
392
|
+
if (!repoRelative) continue
|
|
393
|
+
const absolutePath = path.resolve(repoRoot, repoRelative)
|
|
394
|
+
records.push({
|
|
395
|
+
key: `${repoRoot}\0${repoRelative}`,
|
|
396
|
+
statusCode: line.slice(0, 2),
|
|
397
|
+
repoRoot,
|
|
398
|
+
repoRelative,
|
|
399
|
+
absolutePath,
|
|
400
|
+
workspaceRelative: posixRelative(workspaceRoot, absolutePath),
|
|
401
|
+
rawLine: line,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return { records, metrics: { ms: result.ms, ok: true, error: null } }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isHardcodedStatusIgnored(relativePath) {
|
|
409
|
+
if (!relativePath) return false
|
|
410
|
+
const normalized = normalizePath(relativePath)
|
|
411
|
+
if (GIT_STATUS_IGNORE_PATTERNS.some((regex) => regex.test(normalized) || regex.test(`${normalized}/`))) {
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
if (normalized.includes('/site-packages/')) return true
|
|
415
|
+
if (normalized.includes('/Scripts/')) return true
|
|
416
|
+
if (normalized.includes('/bin/activate')) return true
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
function currentEngineIgnored(engine, records) {
|
|
422
|
+
const ignored = new Set()
|
|
423
|
+
const started = performance.now()
|
|
424
|
+
for (const record of records) {
|
|
425
|
+
const workspaceRelative = normalizePath(record.workspaceRelative)
|
|
426
|
+
if (workspaceRelative && engine.ignores(workspaceRelative)) {
|
|
427
|
+
ignored.add(record.key)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { ignored, metrics: { ms: performance.now() - started } }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function compareSets(left, right) {
|
|
434
|
+
const onlyLeft = []
|
|
435
|
+
const onlyRight = []
|
|
436
|
+
for (const value of left) {
|
|
437
|
+
if (!right.has(value)) onlyLeft.push(value)
|
|
438
|
+
}
|
|
439
|
+
for (const value of right) {
|
|
440
|
+
if (!left.has(value)) onlyRight.push(value)
|
|
441
|
+
}
|
|
442
|
+
return { onlyLeft, onlyRight, same: onlyLeft.length === 0 && onlyRight.length === 0 }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function listAppFolders(apiRoot, selectedApps, maxApps) {
|
|
446
|
+
const entries = await fsp.readdir(apiRoot, { withFileTypes: true })
|
|
447
|
+
let apps = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort()
|
|
448
|
+
if (selectedApps && selectedApps.length > 0) {
|
|
449
|
+
const selected = new Set(selectedApps)
|
|
450
|
+
apps = apps.filter((app) => selected.has(app))
|
|
451
|
+
}
|
|
452
|
+
if (maxApps) {
|
|
453
|
+
apps = apps.slice(0, maxApps)
|
|
454
|
+
}
|
|
455
|
+
return apps
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function measureWorkspace(apiRoot, appName) {
|
|
459
|
+
const workspaceRoot = path.join(apiRoot, appName)
|
|
460
|
+
const workspaceStarted = performance.now()
|
|
461
|
+
|
|
462
|
+
const currentPreScan = await buildCurrentWorkspaceIgnoreEngine(workspaceRoot)
|
|
463
|
+
const repoDiscovery = await discoverReposCurrentStyle(workspaceRoot)
|
|
464
|
+
|
|
465
|
+
const statusStarted = performance.now()
|
|
466
|
+
const records = []
|
|
467
|
+
const statusErrors = []
|
|
468
|
+
let statusMs = 0
|
|
469
|
+
for (const repoRoot of repoDiscovery.repos) {
|
|
470
|
+
const status = await gitStatusRecords(workspaceRoot, repoRoot)
|
|
471
|
+
statusMs += status.metrics.ms
|
|
472
|
+
records.push(...status.records)
|
|
473
|
+
if (!status.metrics.ok) {
|
|
474
|
+
statusErrors.push({ repoRoot, error: status.metrics.error })
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const statusWallMs = performance.now() - statusStarted
|
|
478
|
+
|
|
479
|
+
const hardcodedIgnored = new Set()
|
|
480
|
+
for (const record of records) {
|
|
481
|
+
if (isHardcodedStatusIgnored(record.repoRelative)) {
|
|
482
|
+
hardcodedIgnored.add(record.key)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const currentFilter = currentEngineIgnored(currentPreScan.engine, records)
|
|
487
|
+
const candidateInputRecords = records.filter((record) => !hardcodedIgnored.has(record.key))
|
|
488
|
+
const candidateFilter = await candidateIgnoredByPathScopedGitignores(workspaceRoot, candidateInputRecords)
|
|
489
|
+
|
|
490
|
+
const currentFinalIgnored = new Set([...currentFilter.ignored, ...hardcodedIgnored])
|
|
491
|
+
const candidateFinalIgnored = new Set([...candidateFilter.ignored, ...hardcodedIgnored])
|
|
492
|
+
const comparison = compareSets(currentFinalIgnored, candidateFinalIgnored)
|
|
493
|
+
const byKey = new Map(records.map((record) => [record.key, record]))
|
|
494
|
+
const sampleFor = (keys) => keys.slice(0, 20).map((key) => {
|
|
495
|
+
const record = byKey.get(key)
|
|
496
|
+
return record ? {
|
|
497
|
+
repo: posixRelative(apiRoot, record.repoRoot),
|
|
498
|
+
file: record.repoRelative,
|
|
499
|
+
workspaceRelative: record.workspaceRelative,
|
|
500
|
+
statusCode: record.statusCode,
|
|
501
|
+
} : { key }
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
app: appName,
|
|
506
|
+
workspaceRoot,
|
|
507
|
+
ok: true,
|
|
508
|
+
wallMs: performance.now() - workspaceStarted,
|
|
509
|
+
currentPreScan: currentPreScan.metrics,
|
|
510
|
+
repoDiscovery: {
|
|
511
|
+
...repoDiscovery.metrics,
|
|
512
|
+
repos: repoDiscovery.repos.length,
|
|
513
|
+
},
|
|
514
|
+
gitStatus: {
|
|
515
|
+
ms: statusMs,
|
|
516
|
+
wallMs: statusWallMs,
|
|
517
|
+
records: records.length,
|
|
518
|
+
errors: statusErrors,
|
|
519
|
+
},
|
|
520
|
+
currentFilter: {
|
|
521
|
+
...currentFilter.metrics,
|
|
522
|
+
ignored: currentFilter.ignored.size,
|
|
523
|
+
kept: records.length - currentFilter.ignored.size,
|
|
524
|
+
},
|
|
525
|
+
candidateFilter: {
|
|
526
|
+
...candidateFilter.metrics,
|
|
527
|
+
checked: candidateInputRecords.length,
|
|
528
|
+
ignored: candidateFilter.ignored.size,
|
|
529
|
+
finalIgnored: candidateFinalIgnored.size,
|
|
530
|
+
finalKept: records.length - candidateFinalIgnored.size,
|
|
531
|
+
},
|
|
532
|
+
hardcodedFilter: {
|
|
533
|
+
ignored: hardcodedIgnored.size,
|
|
534
|
+
keptAfterCurrentAndHardcoded: records.filter((record) => (
|
|
535
|
+
!currentFilter.ignored.has(record.key) && !hardcodedIgnored.has(record.key)
|
|
536
|
+
)).length,
|
|
537
|
+
keptAfterCandidateAndHardcoded: records.filter((record) => (
|
|
538
|
+
!candidateFilter.ignored.has(record.key) && !hardcodedIgnored.has(record.key)
|
|
539
|
+
)).length,
|
|
540
|
+
},
|
|
541
|
+
parity: {
|
|
542
|
+
same: comparison.same,
|
|
543
|
+
onlyCurrentIgnored: comparison.onlyLeft.length,
|
|
544
|
+
onlyCandidateIgnored: comparison.onlyRight.length,
|
|
545
|
+
onlyCurrentIgnoredSample: sampleFor(comparison.onlyLeft),
|
|
546
|
+
onlyCandidateIgnoredSample: sampleFor(comparison.onlyRight),
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function summarize(rows) {
|
|
552
|
+
const totals = {
|
|
553
|
+
apps: rows.length,
|
|
554
|
+
okApps: rows.filter((row) => row.ok).length,
|
|
555
|
+
currentPreScanMs: 0,
|
|
556
|
+
currentPreScanDirs: 0,
|
|
557
|
+
currentPreScanEntries: 0,
|
|
558
|
+
currentPreScanGitignoreFiles: 0,
|
|
559
|
+
repoDiscoveryMs: 0,
|
|
560
|
+
repoDiscoveryDirs: 0,
|
|
561
|
+
repoDiscoveryEntries: 0,
|
|
562
|
+
repos: 0,
|
|
563
|
+
gitStatusRecords: 0,
|
|
564
|
+
currentIgnored: 0,
|
|
565
|
+
candidateIgnored: 0,
|
|
566
|
+
hardcodedIgnored: 0,
|
|
567
|
+
candidateChecked: 0,
|
|
568
|
+
candidateFilterMs: 0,
|
|
569
|
+
finalCurrentKept: 0,
|
|
570
|
+
finalCandidateKept: 0,
|
|
571
|
+
parityMismatchedApps: 0,
|
|
572
|
+
parityOnlyCurrentIgnored: 0,
|
|
573
|
+
parityOnlyCandidateIgnored: 0,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
for (const row of rows) {
|
|
577
|
+
if (!row.ok) continue
|
|
578
|
+
totals.currentPreScanMs += row.currentPreScan.ms
|
|
579
|
+
totals.currentPreScanDirs += row.currentPreScan.dirs
|
|
580
|
+
totals.currentPreScanEntries += row.currentPreScan.readdirEntries
|
|
581
|
+
totals.currentPreScanGitignoreFiles += row.currentPreScan.gitignoreFiles
|
|
582
|
+
totals.repoDiscoveryMs += row.repoDiscovery.ms
|
|
583
|
+
totals.repoDiscoveryDirs += row.repoDiscovery.dirs
|
|
584
|
+
totals.repoDiscoveryEntries += row.repoDiscovery.readdirEntries
|
|
585
|
+
totals.repos += row.repoDiscovery.repos
|
|
586
|
+
totals.gitStatusRecords += row.gitStatus.records
|
|
587
|
+
totals.currentIgnored += row.currentFilter.ignored
|
|
588
|
+
totals.candidateIgnored += row.candidateFilter.ignored
|
|
589
|
+
totals.hardcodedIgnored += row.hardcodedFilter.ignored
|
|
590
|
+
totals.candidateChecked += row.candidateFilter.checked
|
|
591
|
+
totals.candidateFilterMs += row.candidateFilter.ms
|
|
592
|
+
totals.finalCurrentKept += row.hardcodedFilter.keptAfterCurrentAndHardcoded
|
|
593
|
+
totals.finalCandidateKept += row.candidateFilter.finalKept
|
|
594
|
+
if (!row.parity.same) {
|
|
595
|
+
totals.parityMismatchedApps += 1
|
|
596
|
+
totals.parityOnlyCurrentIgnored += row.parity.onlyCurrentIgnored
|
|
597
|
+
totals.parityOnlyCandidateIgnored += row.parity.onlyCandidateIgnored
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
totals,
|
|
603
|
+
topByCurrentPreScanMs: rows.filter((row) => row.ok).slice().sort((a, b) => b.currentPreScan.ms - a.currentPreScan.ms).slice(0, 20).map(summaryRow),
|
|
604
|
+
topByRepoDiscoveryMs: rows.filter((row) => row.ok).slice().sort((a, b) => b.repoDiscovery.ms - a.repoDiscovery.ms).slice(0, 20).map(summaryRow),
|
|
605
|
+
parityMismatches: rows.filter((row) => row.ok && !row.parity.same).map((row) => ({
|
|
606
|
+
app: row.app,
|
|
607
|
+
onlyCurrentIgnored: row.parity.onlyCurrentIgnored,
|
|
608
|
+
onlyCandidateIgnored: row.parity.onlyCandidateIgnored,
|
|
609
|
+
onlyCurrentIgnoredSample: row.parity.onlyCurrentIgnoredSample,
|
|
610
|
+
onlyCandidateIgnoredSample: row.parity.onlyCandidateIgnoredSample,
|
|
611
|
+
})),
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function summaryRow(row) {
|
|
616
|
+
return {
|
|
617
|
+
app: row.app,
|
|
618
|
+
currentPreScanMs: Number(row.currentPreScan.ms.toFixed(1)),
|
|
619
|
+
currentPreScanDirs: row.currentPreScan.dirs,
|
|
620
|
+
currentPreScanEntries: row.currentPreScan.readdirEntries,
|
|
621
|
+
gitignoreFiles: row.currentPreScan.gitignoreFiles,
|
|
622
|
+
repoDiscoveryMs: Number(row.repoDiscovery.ms.toFixed(1)),
|
|
623
|
+
repos: row.repoDiscovery.repos,
|
|
624
|
+
statusRecords: row.gitStatus.records,
|
|
625
|
+
currentIgnored: row.currentFilter.ignored,
|
|
626
|
+
hardcodedIgnored: row.hardcodedFilter.ignored,
|
|
627
|
+
candidateChecked: row.candidateFilter.checked,
|
|
628
|
+
candidateIgnored: row.candidateFilter.ignored,
|
|
629
|
+
candidateFinalKept: row.candidateFilter.finalKept,
|
|
630
|
+
candidateFilterMs: Number(row.candidateFilter.ms.toFixed(1)),
|
|
631
|
+
paritySame: row.parity.same,
|
|
632
|
+
mismatchCount: row.parity.onlyCurrentIgnored + row.parity.onlyCandidateIgnored,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function markdownTable(rows) {
|
|
637
|
+
const header = [
|
|
638
|
+
'| App | Current pre-scan ms | Dirs | Entries | .gitignore files | Repo scan ms | Repos | Status paths | Hardcoded ignored | Candidate checked | Current gitignored | Candidate gitignored | Final kept | Candidate filter ms | Mismatches |',
|
|
639
|
+
'| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
|
|
640
|
+
]
|
|
641
|
+
const body = rows.map((row) => [
|
|
642
|
+
`| \`${row.app}\``,
|
|
643
|
+
Number(row.currentPreScan.ms).toFixed(1),
|
|
644
|
+
row.currentPreScan.dirs,
|
|
645
|
+
row.currentPreScan.readdirEntries,
|
|
646
|
+
row.currentPreScan.gitignoreFiles,
|
|
647
|
+
Number(row.repoDiscovery.ms).toFixed(1),
|
|
648
|
+
row.repoDiscovery.repos,
|
|
649
|
+
row.gitStatus.records,
|
|
650
|
+
row.hardcodedFilter.ignored,
|
|
651
|
+
row.candidateFilter.checked,
|
|
652
|
+
row.currentFilter.ignored,
|
|
653
|
+
row.candidateFilter.ignored,
|
|
654
|
+
row.candidateFilter.finalKept,
|
|
655
|
+
Number(row.candidateFilter.ms).toFixed(1),
|
|
656
|
+
row.parity.onlyCurrentIgnored + row.parity.onlyCandidateIgnored,
|
|
657
|
+
].join(' | ') + ' |')
|
|
658
|
+
return header.concat(body).join('\n')
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderMarkdown(report) {
|
|
662
|
+
const rows = report.rows.filter((row) => row.ok).slice().sort((a, b) => b.currentPreScan.ms - a.currentPreScan.ms)
|
|
663
|
+
const totals = report.summary.totals
|
|
664
|
+
const lines = []
|
|
665
|
+
lines.push(`# Workspace Gitignore Benchmark`)
|
|
666
|
+
lines.push('')
|
|
667
|
+
lines.push(`Created: ${report.createdAt}`)
|
|
668
|
+
lines.push(`API root: \`${report.apiRoot}\``)
|
|
669
|
+
lines.push(`Apps measured: ${totals.okApps}/${totals.apps}`)
|
|
670
|
+
lines.push('')
|
|
671
|
+
lines.push(`## Totals`)
|
|
672
|
+
lines.push('')
|
|
673
|
+
lines.push('| Metric | Value |')
|
|
674
|
+
lines.push('| --- | ---: |')
|
|
675
|
+
lines.push(`| Current recursive pre-scan wall time | ${totals.currentPreScanMs.toFixed(1)} ms |`)
|
|
676
|
+
lines.push(`| Current recursive pre-scan dirs | ${totals.currentPreScanDirs} |`)
|
|
677
|
+
lines.push(`| Current recursive pre-scan entries | ${totals.currentPreScanEntries} |`)
|
|
678
|
+
lines.push(`| Current .gitignore files read | ${totals.currentPreScanGitignoreFiles} |`)
|
|
679
|
+
lines.push(`| Current repo discovery wall time | ${totals.repoDiscoveryMs.toFixed(1)} ms |`)
|
|
680
|
+
lines.push(`| Current repo discovery dirs | ${totals.repoDiscoveryDirs} |`)
|
|
681
|
+
lines.push(`| Current repo discovery entries | ${totals.repoDiscoveryEntries} |`)
|
|
682
|
+
lines.push(`| Repos discovered | ${totals.repos} |`)
|
|
683
|
+
lines.push(`| Raw git status paths | ${totals.gitStatusRecords} |`)
|
|
684
|
+
lines.push(`| Current JS ignore-filter ignored paths | ${totals.currentIgnored} |`)
|
|
685
|
+
lines.push(`| Hard-coded ignored paths | ${totals.hardcodedIgnored} |`)
|
|
686
|
+
lines.push(`| Candidate paths checked after hard-coded prefilter | ${totals.candidateChecked} |`)
|
|
687
|
+
lines.push(`| Candidate path-scoped ignored paths | ${totals.candidateIgnored} |`)
|
|
688
|
+
lines.push(`| Final current kept paths | ${totals.finalCurrentKept} |`)
|
|
689
|
+
lines.push(`| Final candidate kept paths | ${totals.finalCandidateKept} |`)
|
|
690
|
+
lines.push(`| Candidate ignore-filter wall time | ${totals.candidateFilterMs.toFixed(1)} ms |`)
|
|
691
|
+
lines.push(`| Apps with filter mismatches | ${totals.parityMismatchedApps} |`)
|
|
692
|
+
lines.push(`| Current-only ignored paths | ${totals.parityOnlyCurrentIgnored} |`)
|
|
693
|
+
lines.push(`| Candidate-only ignored paths | ${totals.parityOnlyCandidateIgnored} |`)
|
|
694
|
+
lines.push('')
|
|
695
|
+
lines.push(`## Per-App Rows`)
|
|
696
|
+
lines.push('')
|
|
697
|
+
lines.push(markdownTable(rows))
|
|
698
|
+
if (report.summary.parityMismatches.length > 0) {
|
|
699
|
+
lines.push('')
|
|
700
|
+
lines.push('## Parity Mismatches')
|
|
701
|
+
lines.push('')
|
|
702
|
+
for (const mismatch of report.summary.parityMismatches) {
|
|
703
|
+
lines.push(`### ${mismatch.app}`)
|
|
704
|
+
lines.push('')
|
|
705
|
+
lines.push(`Current-only ignored: ${mismatch.onlyCurrentIgnored}`)
|
|
706
|
+
lines.push(`Candidate-only ignored: ${mismatch.onlyCandidateIgnored}`)
|
|
707
|
+
lines.push('')
|
|
708
|
+
lines.push('Current-only sample:')
|
|
709
|
+
lines.push('')
|
|
710
|
+
lines.push('```json')
|
|
711
|
+
lines.push(JSON.stringify(mismatch.onlyCurrentIgnoredSample, null, 2))
|
|
712
|
+
lines.push('```')
|
|
713
|
+
lines.push('')
|
|
714
|
+
lines.push('Candidate-only sample:')
|
|
715
|
+
lines.push('')
|
|
716
|
+
lines.push('```json')
|
|
717
|
+
lines.push(JSON.stringify(mismatch.onlyCandidateIgnoredSample, null, 2))
|
|
718
|
+
lines.push('```')
|
|
719
|
+
lines.push('')
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
lines.push('')
|
|
723
|
+
return lines.join('\n')
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function gitCommit(repoRoot) {
|
|
727
|
+
try {
|
|
728
|
+
return fs.existsSync(path.join(repoRoot, '.git'))
|
|
729
|
+
? execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
730
|
+
cwd: repoRoot,
|
|
731
|
+
encoding: 'utf8',
|
|
732
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
733
|
+
}).trim()
|
|
734
|
+
: null
|
|
735
|
+
} catch (_) {
|
|
736
|
+
return null
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function main() {
|
|
741
|
+
const args = parseArgs(process.argv.slice(2))
|
|
742
|
+
if (args.help) {
|
|
743
|
+
console.log(usage())
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const repoRoot = path.resolve(__dirname, '..')
|
|
748
|
+
const appNames = await listAppFolders(args.apiRoot, args.apps, args.maxApps)
|
|
749
|
+
const rows = []
|
|
750
|
+
|
|
751
|
+
for (const appName of appNames) {
|
|
752
|
+
try {
|
|
753
|
+
const row = await measureWorkspace(args.apiRoot, appName)
|
|
754
|
+
rows.push(row)
|
|
755
|
+
console.error(`${appName}: preScan=${row.currentPreScan.ms.toFixed(1)}ms repoScan=${row.repoDiscovery.ms.toFixed(1)}ms statusPaths=${row.gitStatus.records} mismatches=${row.parity.onlyCurrentIgnored + row.parity.onlyCandidateIgnored}`)
|
|
756
|
+
} catch (error) {
|
|
757
|
+
rows.push({
|
|
758
|
+
app: appName,
|
|
759
|
+
ok: false,
|
|
760
|
+
error: error && error.stack ? error.stack : String(error),
|
|
761
|
+
})
|
|
762
|
+
console.error(`${appName}: failed: ${error && error.message ? error.message : error}`)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const report = {
|
|
767
|
+
kind: 'pinokiod-workspace-gitignore-component-benchmark',
|
|
768
|
+
createdAt: new Date().toISOString(),
|
|
769
|
+
repoRoot,
|
|
770
|
+
commit: gitCommit(repoRoot),
|
|
771
|
+
node: process.version,
|
|
772
|
+
platform: process.platform,
|
|
773
|
+
apiRoot: args.apiRoot,
|
|
774
|
+
selectedApps: args.apps,
|
|
775
|
+
maxApps: args.maxApps,
|
|
776
|
+
notes: [
|
|
777
|
+
'Current pre-scan mimics WorkspaceStatusManager.ensureGitIgnoreEngine().',
|
|
778
|
+
'Current repo discovery approximates kernel.git.findGitDirs() for measurement only.',
|
|
779
|
+
'Candidate filtering uses path-scoped .gitignore evaluation with the same parser rules as the current pre-scan.',
|
|
780
|
+
'This benchmark measures component algorithms, not live route integration.',
|
|
781
|
+
],
|
|
782
|
+
rows,
|
|
783
|
+
summary: null,
|
|
784
|
+
}
|
|
785
|
+
report.summary = summarize(rows)
|
|
786
|
+
|
|
787
|
+
await fsp.mkdir(path.dirname(args.out), { recursive: true })
|
|
788
|
+
await fsp.writeFile(args.out, `${JSON.stringify(report, null, 2)}\n`)
|
|
789
|
+
console.log(args.out)
|
|
790
|
+
|
|
791
|
+
if (!args.noMarkdown && args.markdownOut) {
|
|
792
|
+
await fsp.mkdir(path.dirname(args.markdownOut), { recursive: true })
|
|
793
|
+
await fsp.writeFile(args.markdownOut, renderMarkdown(report))
|
|
794
|
+
console.log(args.markdownOut)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (require.main === module) {
|
|
799
|
+
main().catch((error) => {
|
|
800
|
+
console.error(error && error.stack ? error.stack : error)
|
|
801
|
+
process.exitCode = 1
|
|
802
|
+
})
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
module.exports = {
|
|
806
|
+
buildCurrentWorkspaceIgnoreEngine,
|
|
807
|
+
candidateIgnoredByPathScopedGitignores,
|
|
808
|
+
compareSets,
|
|
809
|
+
currentEngineIgnored,
|
|
810
|
+
discoverReposCurrentStyle,
|
|
811
|
+
gitStatusRecords,
|
|
812
|
+
isHardcodedStatusIgnored,
|
|
813
|
+
measureWorkspace,
|
|
814
|
+
normalizePath,
|
|
815
|
+
}
|