fix: include gitignored files in quick open (#1762)

This commit is contained in:
Jinjing 2026-05-13 14:29:30 -07:00 committed by GitHub
parent a076656ff8
commit c8d97f3b98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 273 additions and 66 deletions

View File

@ -0,0 +1,76 @@
# Quick Open Gitignored Files
## Problem
Cmd/Ctrl+P Quick Open does not list arbitrary gitignored files in the active workspace.
- `src/renderer/src/components/QuickOpen.tsx:257` calls `window.api.fs.listFiles(...)`; filtering in the renderer only fuzzy-matches the returned list, so missing files are already absent from the backend result.
- `src/main/ipc/filesystem-list-files.ts:46` uses `buildRgArgsForQuickOpen(...)` for local worktrees, then merges the primary rg pass with `envPass`.
- `src/relay/fs-handler-list-files.ts:37` uses the same shared rg args for SSH worktrees.
- `src/shared/quick-open-filter.ts:252` builds the primary rg pass without `--no-ignore-vcs`, so gitignored files are hidden.
- `src/shared/quick-open-filter.ts:265` builds a second `--no-ignore-vcs` pass, but it is restricted to `.env*` and `**/.env*`.
- `src/shared/quick-open-filter.ts:357` builds the git fallback primary pass with `--exclude-standard`; `src/shared/quick-open-filter.ts:365` only adds `.env*` pathspecs.
## Root Cause
Quick Open intentionally mirrors `rg --files --hidden` with gitignore respect, then adds a special-case pass for gitignored `.env*` files. That policy lives in shared code used by local main-process listing, SSH relay listing, and git fallback listing, so non-env ignored files are excluded consistently in every workspace type.
## Non-goals
- Do not change right-sidebar text search behavior.
- Do not add a new UI preference or mode switch.
- Do not follow symlinks or broaden traversal outside the authorized workspace root.
- Do not surface heavy generated directories already excluded by Quick Open policy, including `node_modules`, `.git`, `.cache`, `.next`, and other `HIDDEN_DIR_BLOCKLIST` entries.
- Do not change fuzzy ranking, selection, file-opening behavior, or shortcut handling.
## Design
1. Replace the `.env*` rg second pass with a general ignored-files pass.
- Keep `--files --hidden --no-ignore-vcs`, hidden-dir blocklist globs, nested-worktree exclude globs, `searchRoot='.'`, and path-separator handling.
- Remove positive `.env*` globs so the second pass is no longer a whitelist.
- Keep the primary pass unchanged.
2. Rename pass naming from `envPass` to `ignoredPass` in shared/main/relay code.
- This is a behavior change, not cosmetic: the second pass now returns all gitignored candidates.
3. Replace git fallback `.env*` pass with ignored-files pass.
- Keep primary as `['--cached', '--others', '--exclude-standard', ...]`.
- Use ignored pass as `['--others', '--ignored', '--exclude-standard', ...]`.
- Keep nested-worktree pathspec exclusion semantics (`--`, `.`, excludes when exclude prefixes exist).
- Do not assume both runtimes have identical error semantics today:
- local main fallback currently resolves on most git failures;
- relay fallback rejects on spawn/signal failures.
Preserve each runtimes current behavior unless explicitly changing it in a separate doc.
4. Keep post-filters unchanged.
- `shouldIncludeQuickOpenPath` remains final blocklist enforcement.
- `shouldExcludeQuickOpenRelPath` remains nested-worktree correctness backstop.
- Set-based merge still dedupes cross-pass overlap.
5. Do not change renderer request-lifecycle logic in this doc.
- Current `QuickOpen.tsx` effect cleanup cancels the prior request before the next effect body runs.
- This change is backend listing policy only.
6. Update tests.
- `quick-open-filter.test.ts`: assert rg ignored pass has `--no-ignore-vcs` and no `.env*` globs; assert git ignored pass has `--others --ignored --exclude-standard` and no `.env*` pathspec whitelist.
- `filesystem-list-files.test.ts`: update pass-detection helpers that currently key off `'**/.env*'`; cover ignored non-env files; keep local fallbacks resolve-on-failure behavior unchanged.
- Add relay coverage in `src/relay/fs-handler.test.ts` (or new focused relay tests) for ignored-pass args and current reject-on-signal behavior.
## Edge Cases
- Gitignored files inside `node_modules`, `.git`, `.cache`, `.next`, `.npm`, `.npm-global`, `.gvfs`, and other blocklisted dirs must remain hidden.
- Nested linked worktree paths passed as `excludePaths` must remain excluded from both rg and git passes.
- Local/SSH candidate sets should match when both run the same backend path (rg or git) and complete successfully; timeout/error behavior remains intentionally different today.
- Windows and WSL path normalization must remain unchanged; output still passes through `normalizeQuickOpenRgLine`.
- If rg is unavailable, git fallback should include ignored files only when Git can enumerate them; non-git roots keep existing fallback limits.
- Timeout/signal behavior must not regress into partial false-empty results.
- Keep existing timeout asymmetry unless intentionally changed: local rg/git fallback uses 10s timeouts, relay rg uses 25s.
- `--no-ignore-vcs` also includes files ignored by parent/global excludes; blocklists are the guardrail against accidental heavy trees.
## Rollout
1. Update `src/shared/quick-open-filter.ts` types, rg args, git args, and comments.
2. Update local main-process and SSH relay callers/tests for `ignoredPass`.
3. Run focused tests for quick-open filters and list-files (main + relay).
4. Run `pnpm typecheck` and `pnpm lint`.
5. Validate in Electron on local + SSH worktrees with gitignored non-env files and nested linked worktrees.

View File

@ -43,6 +43,10 @@ function createMockProcess(): ChildProcess {
return p
}
function isIgnoredRgPass(args: string[]): boolean {
return args.includes('--no-ignore-vcs')
}
describe('filesystem-list-files', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -50,12 +54,12 @@ describe('filesystem-list-files', () => {
checkRgAvailableMock.mockResolvedValue(true)
})
it('merges normal files and env files and filters correctly', async () => {
it('merges normal files and ignored files and filters correctly', async () => {
const p1 = createMockProcess()
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1
@ -74,15 +78,23 @@ describe('filesystem-list-files', () => {
;(p1.stdout as unknown as EventEmitter).emit('data', 'file2.js\n')
p1.emit('close', 0, null)
// Simulate stdout output for env files
// Simulate stdout output for ignored files
;(p2.stdout as unknown as EventEmitter).emit('data', '.env.local\n')
;(p2.stdout as unknown as EventEmitter).emit('data', 'dist/generated.js\n')
;(p2.stdout as unknown as EventEmitter).emit('data', 'file1.ts\n') // Duplicate
;(p2.stdout as unknown as EventEmitter).emit('data', 'node_modules/ignored.js\n')
p2.emit('close', 0, null)
}, 10)
const result = await promise
expect(result).toEqual(['file1.ts', '.github/workflows/ci.yml', 'dir1/file2.js', '.env.local'])
expect(result).toEqual([
'file1.ts',
'.github/workflows/ci.yml',
'dir1/file2.js',
'.env.local',
'dist/generated.js'
])
})
it('rejects rg failures instead of resolving a false-empty list', async () => {
@ -90,7 +102,7 @@ describe('filesystem-list-files', () => {
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1
@ -112,7 +124,7 @@ describe('filesystem-list-files', () => {
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1
@ -135,7 +147,7 @@ describe('filesystem-list-files', () => {
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1
@ -158,7 +170,7 @@ describe('filesystem-list-files', () => {
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1
@ -176,7 +188,7 @@ describe('filesystem-list-files', () => {
;(p1.stdout as unknown as EventEmitter).emit('data', 'valid.ts\n')
p1.emit('close', 0, null)
// Empty env result
// Empty ignored result
p2.emit('close', 0, null)
}, 10)
@ -211,6 +223,7 @@ describe('filesystem-list-files', () => {
gitP1.emit('close', 0, null)
;(gitP2.stdout as unknown as EventEmitter).emit('data', '.env.local\n')
;(gitP2.stdout as unknown as EventEmitter).emit('data', 'dist/generated.js\n')
gitP2.emit('close', 0, null)
}, 10)
@ -229,6 +242,7 @@ describe('filesystem-list-files', () => {
expect(result).toContain('src/index.ts')
expect(result).toContain('package.json')
expect(result).toContain('.env.local')
expect(result).toContain('dist/generated.js')
expect(result).not.toContain('node_modules/dep/index.js')
})
@ -272,7 +286,7 @@ describe('filesystem-list-files', () => {
const p2 = createMockProcess()
spawnMock.mockImplementation((_cmd, args: string[]) => {
if (args.includes('**/.env*')) {
if (isIgnoredRgPass(args)) {
return p2
}
return p1

View File

@ -43,7 +43,7 @@ export async function listQuickOpenFiles(
// UNC paths up-front before the shared line normalizer runs.
const wslInfo = parseWslPath(authorizedRootPath)
const { primary, envPass } = buildRgArgsForQuickOpen({
const { primary, ignoredPass } = buildRgArgsForQuickOpen({
// Why: rg evaluates root-relative exclude globs against cwd only when the
// search target is cwd-relative. With an absolute target, `!packages/app`
// filters output after traversal but does not prune the nested worktree.
@ -159,7 +159,7 @@ export async function listQuickOpenFiles(
}
try {
await Promise.all([runRg(primary), runRg(envPass)])
await Promise.all([runRg(primary), runRg(ignoredPass)])
} catch (err) {
killSurvivors()
throw err
@ -172,15 +172,14 @@ export async function listQuickOpenFiles(
*
* Why two git ls-files calls: the first lists tracked + untracked-but-not-ignored
* files (mirrors rg --files --hidden with gitignore respect). The second
* surfaces .env* files that are typically gitignored but users frequently
* need in quick-open (mirrors the second rg call with --no-ignore-vcs).
* surfaces ignored files (mirrors the second rg call with --no-ignore-vcs).
*/
function listFilesWithGit(
rootPath: string,
excludePathPrefixes: readonly string[]
): Promise<string[]> {
const files = new Set<string>()
const { primary, envPass } = buildGitLsFilesArgsForQuickOpen(excludePathPrefixes)
const { primary, ignoredPass } = buildGitLsFilesArgsForQuickOpen(excludePathPrefixes)
const runGitLsFiles = (args: string[]): Promise<void> => {
return new Promise((resolve) => {
@ -251,5 +250,7 @@ function listFilesWithGit(
})
}
return Promise.all([runGitLsFiles(primary), runGitLsFiles(envPass)]).then(() => Array.from(files))
return Promise.all([runGitLsFiles(primary), runGitLsFiles(ignoredPass)]).then(() =>
Array.from(files)
)
}

View File

@ -26,7 +26,7 @@ import {
* List files using `git ls-files`. Fallback when rg is not installed.
*
* Why both passes: primary surfaces tracked + untracked-non-ignored;
* envPass surfaces gitignored .env* files that users frequently Quick Open.
* ignoredPass surfaces gitignored files that users frequently Quick Open.
* Exclude pathspecs are prepended by the shared builder so nested linked
* worktrees are pruned by git directly; post-filtering remains as a
* correctness backstop.
@ -36,7 +36,7 @@ export function listFilesWithGit(
excludePathPrefixes: readonly string[] = []
): Promise<string[]> {
const files = new Set<string>()
const { primary, envPass } = buildGitLsFilesArgsForQuickOpen(excludePathPrefixes)
const { primary, ignoredPass } = buildGitLsFilesArgsForQuickOpen(excludePathPrefixes)
const runGitLsFiles = (args: string[]): Promise<void> => {
return new Promise((resolve, reject) => {
@ -112,7 +112,9 @@ export function listFilesWithGit(
})
}
return Promise.all([runGitLsFiles(primary), runGitLsFiles(envPass)]).then(() => Array.from(files))
return Promise.all([runGitLsFiles(primary), runGitLsFiles(ignoredPass)]).then(() =>
Array.from(files)
)
}
/**

View File

@ -0,0 +1,131 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'
const { spawnMock } = vi.hoisted(() => ({
spawnMock: vi.fn()
}))
vi.mock('child_process', () => ({
spawn: spawnMock
}))
import { EventEmitter } from 'events'
import type { ChildProcess } from 'child_process'
import { listFilesWithGit } from './fs-handler-git-fallback'
import { listFilesWithRg } from './fs-handler-list-files'
function createMockProcess(): ChildProcess {
const p = new EventEmitter() as unknown as ChildProcess
;(p as unknown as Record<string, unknown>).stdout = new EventEmitter()
;(
(p as unknown as Record<string, unknown>).stdout as EventEmitter & {
setEncoding: () => void
}
).setEncoding = vi.fn()
;(p as unknown as Record<string, unknown>).stderr = new EventEmitter()
;(p as unknown as Record<string, unknown>).kill = vi.fn()
;(p as unknown as Record<string, unknown>).exitCode = null
;(p as unknown as Record<string, unknown>).signalCode = null
return p
}
describe('relay quick open ignored file listing', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('rg ignored pass includes ignored non-env files and keeps blocklists/excludes', async () => {
const primaryProc = createMockProcess()
const ignoredProc = createMockProcess()
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
if (args.includes('--no-ignore-vcs')) {
return ignoredProc
}
return primaryProc
})
const promise = listFilesWithRg('/remote/root', ['packages/other'])
setTimeout(() => {
;(primaryProc.stdout as unknown as EventEmitter).emit('data', 'src/index.ts\n')
primaryProc.emit('close', 0, null)
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'dist/generated.js\n')
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'node_modules/pkg/index.js\n')
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'packages/other/src/x.ts\n')
ignoredProc.emit('close', 0, null)
}, 10)
await expect(promise).resolves.toEqual(['src/index.ts', 'dist/generated.js'])
const ignoredArgs = spawnMock.mock.calls.find((call) =>
(call[1] as string[]).includes('--no-ignore-vcs')
)?.[1] as string[]
expect(ignoredArgs).toBeDefined()
expect(ignoredArgs).toContain('--no-ignore-vcs')
expect(ignoredArgs).not.toContain('.env*')
expect(ignoredArgs).not.toContain('**/.env*')
expect(ignoredArgs).toContain('!**/node_modules')
expect(ignoredArgs).toContain('!packages/other')
expect(ignoredArgs).toContain('!packages/other/**')
})
it('git fallback ignored pass includes ignored non-env files', async () => {
const primaryProc = createMockProcess()
const ignoredProc = createMockProcess()
let callIndex = 0
spawnMock.mockImplementation(() => {
callIndex++
return callIndex === 1 ? primaryProc : ignoredProc
})
const promise = listFilesWithGit('/remote/root', ['packages/other'])
setTimeout(() => {
;(primaryProc.stdout as unknown as EventEmitter).emit('data', 'src/index.ts\n')
primaryProc.emit('close', 0, null)
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'dist/generated.js\n')
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'packages/other/src/x.ts\n')
ignoredProc.emit('close', 0, null)
}, 10)
await expect(promise).resolves.toEqual(['src/index.ts', 'dist/generated.js'])
const ignoredArgs = spawnMock.mock.calls[1][1] as string[]
expect(ignoredArgs).toEqual([
'ls-files',
'--others',
'--ignored',
'--exclude-standard',
'--',
'.',
':(exclude,glob)packages/other',
':(exclude,glob)packages/other/**'
])
})
it('git fallback rejects signal exits instead of returning partial results', async () => {
const primaryProc = createMockProcess()
const ignoredProc = createMockProcess()
let callIndex = 0
spawnMock.mockImplementation(() => {
callIndex++
return callIndex === 1 ? primaryProc : ignoredProc
})
const promise = listFilesWithGit('/remote/root')
setTimeout(() => {
;(primaryProc.stdout as unknown as EventEmitter).emit('data', 'src/index.ts\n')
primaryProc.emit('close', 0, null)
;(ignoredProc.stdout as unknown as EventEmitter).emit('data', 'dist/generated.js\n')
ignoredProc.emit('close', null, 'SIGTERM')
}, 10)
await expect(promise).rejects.toThrow('git ls-files killed by SIGTERM')
})
})

View File

@ -8,7 +8,7 @@
* matching files" even though the file existed on disk. This implementation:
* - streams via spawn (no maxBuffer failure mode)
* - prunes traversal at rg level using the shared blocklist globs
* - runs a second --no-ignore-vcs pass for .env* files
* - runs a second --no-ignore-vcs pass for ignored files
* - honors excludePathPrefixes for nested linked worktrees
* - rejects (not resolves) on timeout / spawn error / signal exit so
* the UI shows a load error instead of a false-empty list
@ -34,7 +34,7 @@ export function listFilesWithRg(
let done = false
const children: ChildProcess[] = []
const { primary, envPass } = buildRgArgsForQuickOpen({
const { primary, ignoredPass } = buildRgArgsForQuickOpen({
// Why: rg only applies root-relative exclude globs as traversal pruning
// when the search target is relative to cwd. Absolute targets still
// emit root-relative-looking paths for filters, but they do not prune.
@ -162,7 +162,7 @@ export function listFilesWithRg(
}
}
Promise.all([runPass(primary), runPass(envPass)])
Promise.all([runPass(primary), runPass(ignoredPass)])
.then(() => {
if (done) {
return

View File

@ -114,16 +114,16 @@ describe('buildRgArgsForQuickOpen', () => {
expect(primary).not.toContain('--follow')
})
it('.env* pass includes --no-ignore-vcs and both root + nested globs, no --follow', () => {
const { envPass } = buildRgArgsForQuickOpen({
it('ignored pass includes --no-ignore-vcs without .env* whitelist globs, no --follow', () => {
const { ignoredPass } = buildRgArgsForQuickOpen({
searchRoot: '/root',
excludePathPrefixes: [],
forceSlashSeparator: false
})
expect(envPass).toContain('--no-ignore-vcs')
expect(envPass).toContain('.env*')
expect(envPass).toContain('**/.env*')
expect(envPass).not.toContain('--follow')
expect(ignoredPass).toContain('--no-ignore-vcs')
expect(ignoredPass).not.toContain('.env*')
expect(ignoredPass).not.toContain('**/.env*')
expect(ignoredPass).not.toContain('--follow')
})
it('forceSlashSeparator emits --path-separator /', () => {
@ -203,21 +203,22 @@ describe('buildGitLsFilesArgsForQuickOpen', () => {
expect(primary).toEqual(['--cached', '--others', '--exclude-standard'])
})
it('env pass surfaces gitignored .env at root and nested, without --exclude-standard', () => {
const { envPass } = buildGitLsFilesArgsForQuickOpen()
expect(envPass).toContain('--others')
expect(envPass).toContain('.env*')
expect(envPass).toContain(':(glob)**/.env*')
expect(envPass).not.toContain('--exclude-standard')
it('ignored pass surfaces ignored files without .env* pathspec whitelist', () => {
const { ignoredPass } = buildGitLsFilesArgsForQuickOpen()
expect(ignoredPass).toEqual(['--others', '--ignored', '--exclude-standard'])
expect(ignoredPass).not.toContain('.env*')
expect(ignoredPass).not.toContain(':(glob)**/.env*')
})
it('exclude prefixes prepend positive "." pathspec', () => {
const { primary } = buildGitLsFilesArgsForQuickOpen(['packages/app'])
const { primary, ignoredPass } = buildGitLsFilesArgsForQuickOpen(['packages/app'])
const dashDashIdx = primary.indexOf('--')
expect(dashDashIdx).toBeGreaterThanOrEqual(0)
// Positive pathspec must appear before any exclude pathspec.
expect(primary[dashDashIdx + 1]).toBe('.')
expect(primary).toContain(':(exclude,glob)packages/app')
expect(primary).toContain(':(exclude,glob)packages/app/**')
expect(ignoredPass).toContain(':(exclude,glob)packages/app')
expect(ignoredPass).toContain(':(exclude,glob)packages/app/**')
})
})

View File

@ -5,8 +5,8 @@
*
* Why this module exists (design doc: docs/design/share-quick-open-file-listing.md):
* Before extraction, the local and relay listFiles implementations had diverged
* on blocklist, .env* handling, nested-worktree exclusions, timeout strategy,
* and buffering. A home-dir worktree over SSH would descend into $HOME dotfile
* on blocklist, ignored-file handling, nested-worktree exclusions, timeout
* strategy, and buffering. A home-dir worktree over SSH would descend into $HOME dotfile
* caches, hit a 10s timeout, and silently resolve with a partial result
* Quick Open showed "No matching files" even though the scan was incomplete.
* Centralizing the policy prevents future drift.
@ -223,8 +223,8 @@ export type RgArgsOptions = {
export type RgArgs = {
/** Main pass: all non-ignored files, hidden dotfiles included. */
primary: string[]
/** Second pass: gitignored .env* files. */
envPass: string[]
/** Second pass: ignored files, hidden dotfiles included. */
ignoredPass: string[]
}
/**
@ -258,25 +258,19 @@ export function buildRgArgsForQuickOpen(opts: RgArgsOptions): RgArgs {
opts.searchRoot
]
// .env* pass: must include --no-ignore-vcs so rg surfaces gitignored .env
// files, which is the whole reason the second pass exists. Two positive
// globs (root-level and nested) because rg treats any positive --glob as a
// whitelist; no preceding negative pattern is needed.
const envPass = [
// Ignored pass: --no-ignore-vcs broadens traversal to gitignored and
// parent/global ignored files; blocklist globs remain the guardrail.
const ignoredPass = [
'--files',
'--hidden',
'--no-ignore-vcs',
...sepArgs,
'--glob',
'.env*',
'--glob',
'**/.env*',
...hiddenDirGlobs,
...excludeGlobs,
opts.searchRoot
]
return { primary, envPass }
return { primary, ignoredPass }
}
// ─── rg stdout line normalization ────────────────────────────────────
@ -331,7 +325,7 @@ export function normalizeQuickOpenRgLine(rawLine: string, outputMode: RgOutputMo
export type GitLsFilesArgs = {
primary: string[]
envPass: string[]
ignoredPass: string[]
}
/**
@ -340,9 +334,8 @@ export type GitLsFilesArgs = {
* prepended so exclude-only pathspecs do not depend on git's edge-case
* defaults.
*
* The `.env*` pass uses BOTH `.env*` (root-level) and `**\/.env*` (nested)
* because the `**\/` prefix alone does not match a root-level `.env` file
* this was the silent bug in the prior local implementation.
* The ignored pass asks git for ignored untracked files. Non-git roots keep
* their existing non-git fallback limits in the callers.
*/
export function buildGitLsFilesArgsForQuickOpen(
excludePathPrefixes: readonly string[] = []
@ -355,17 +348,6 @@ export function buildGitLsFilesArgsForQuickOpen(
const trailingPathspecs = excludeSpecs.length > 0 ? ['--', '.', ...excludeSpecs] : []
const primary = ['--cached', '--others', '--exclude-standard', ...trailingPathspecs]
// Second pass: untracked AND ignored .env* files. Do not pass
// --exclude-standard — that would re-hide gitignored .env files, which is
// the whole reason this pass exists.
// Why :(glob): default git pathspec uses fnmatch, where `*` crossing `/` is
// implementation-dependent. `:(glob)` pins the semantics explicitly so
// `**/.env*` reliably surfaces nested `.env` files across git versions.
const nestedEnvSpec = ':(glob)**/.env*'
const envPassPathspecs =
excludeSpecs.length > 0
? ['--', '.env*', nestedEnvSpec, ...excludeSpecs]
: ['--', '.env*', nestedEnvSpec]
const envPass = ['--others', ...envPassPathspecs]
return { primary, envPass }
const ignoredPass = ['--others', '--ignored', '--exclude-standard', ...trailingPathspecs]
return { primary, ignoredPass }
}