mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fmt(js): npm run fix on merge (#68681)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
d9dae17e97
commit
d604141d09
8 changed files with 54 additions and 4 deletions
|
|
@ -11,11 +11,13 @@ const tempDirs: string[] = []
|
|||
function tempDir(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-repo-scan-'))
|
||||
tempDirs.push(dir)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
function makeRepo(root: string, valid = true): void {
|
||||
fs.mkdirSync(path.join(root, '.git'), { recursive: true })
|
||||
|
||||
if (valid) {
|
||||
fs.writeFileSync(path.join(root, '.git', 'HEAD'), 'ref: refs/heads/main\n')
|
||||
}
|
||||
|
|
@ -23,6 +25,7 @@ function makeRepo(root: string, valid = true): void {
|
|||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { force: true, recursive: true })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ export function normalizeRepoScanPath(rawPath: string, options: RepoScanPathOpti
|
|||
const homeDir = options.homeDir ?? os.homedir()
|
||||
const pathApi = pathApiFor(platform)
|
||||
const raw = String(rawPath ?? '').trim()
|
||||
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
|
||||
let expanded = raw
|
||||
|
||||
if (raw === '~') {
|
||||
expanded = homeDir
|
||||
} else if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
||||
|
|
@ -50,6 +52,7 @@ export function normalizeRepoScanPath(rawPath: string, options: RepoScanPathOpti
|
|||
const absolute = pathApi.isAbsolute(expanded) ? expanded : pathApi.resolve(homeDir, expanded)
|
||||
const value = pathApi.normalize(absolute)
|
||||
const key = platform === 'win32' ? value.toLocaleLowerCase('en-US') : value
|
||||
|
||||
return { key, value }
|
||||
}
|
||||
|
||||
|
|
@ -58,10 +61,13 @@ export function repoScanPathIsWithin(candidate: string, parent: string, options:
|
|||
const pathApi = pathApiFor(platform)
|
||||
const candidatePath = normalizeRepoScanPath(candidate, options)
|
||||
const parentPath = normalizeRepoScanPath(parent, options)
|
||||
|
||||
if (!candidatePath || !parentPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
const relative = pathApi.relative(parentPath.key, candidatePath.key)
|
||||
|
||||
return (
|
||||
relative === '' || (relative !== '..' && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative))
|
||||
)
|
||||
|
|
@ -95,6 +101,7 @@ export async function scanGitRepos(roots: string[], options: RepoScanOptions = {
|
|||
const maxDepth = Number.isFinite(maxDepthValue) && maxDepthValue >= 0 ? maxDepthValue : DEFAULT_MAX_DEPTH
|
||||
const pathOptions: RepoScanPathOptions = {}
|
||||
const requestedRoots = Array.isArray(roots) && roots.length > 0 ? roots : [os.homedir()]
|
||||
|
||||
const searchRoots = [
|
||||
...new Map(
|
||||
requestedRoots
|
||||
|
|
@ -103,9 +110,11 @@ export async function scanGitRepos(roots: string[], options: RepoScanOptions = {
|
|||
.map(entry => [entry.key, entry.value])
|
||||
).values()
|
||||
]
|
||||
|
||||
const exclusions = (options.excludePaths ?? [])
|
||||
.map(excluded => normalizeRepoScanPath(excluded, pathOptions))
|
||||
.filter((entry): entry is NormalizedScanPath => entry !== null)
|
||||
|
||||
const found = new Map<string, { root: string; label: string }>()
|
||||
|
||||
function isExcluded(candidate: string): boolean {
|
||||
|
|
@ -118,6 +127,7 @@ export async function scanGitRepos(roots: string[], options: RepoScanOptions = {
|
|||
}
|
||||
|
||||
let entries: fs.Dirent[]
|
||||
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
|
|
@ -125,28 +135,34 @@ export async function scanGitRepos(roots: string[], options: RepoScanOptions = {
|
|||
}
|
||||
|
||||
const gitDir = entries.find(entry => entry.name === '.git' && entry.isDirectory())
|
||||
|
||||
if (gitDir) {
|
||||
try {
|
||||
await fsp.access(path.join(dir, '.git', 'HEAD'), fs.constants.R_OK)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const normalized = normalizeRepoScanPath(dir, pathOptions)
|
||||
|
||||
if (normalized) {
|
||||
found.set(normalized.key, {
|
||||
root: normalized.value,
|
||||
label: path.basename(normalized.value) || normalized.value
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const subdirs = entries
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') && !JUNK_DIRS.has(entry.name))
|
||||
.map(entry => path.join(dir, entry.name))
|
||||
|
||||
await mapLimit(subdirs, MAX_CONCURRENCY, subdir => walk(subdir, depth + 1))
|
||||
}
|
||||
|
||||
await mapLimit(searchRoots, MAX_CONCURRENCY, root => walk(root, 0))
|
||||
|
||||
return [...found.values()]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const { renameSession, request, activeGateway } = vi.hoisted(() => ({
|
|||
request: vi.fn(async () => ({ title: 'rpc-title' }) as never),
|
||||
activeGateway: vi.fn<() => { request: unknown } | null>(() => ({ request: undefined }))
|
||||
}))
|
||||
|
||||
// Wire activeGateway's default return to the shared request mock now that it exists.
|
||||
activeGateway.mockReturnValue({ request })
|
||||
|
||||
|
|
|
|||
|
|
@ -137,19 +137,23 @@ export function ConfigSettings({
|
|||
void (async () => {
|
||||
try {
|
||||
const result = await saveHermesConfig(config)
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(c.autosaveFailed)
|
||||
}
|
||||
|
||||
// Mirror the saved record into the shared cache so MCP/model surfaces
|
||||
// reflect the edit without their own refetch.
|
||||
setHermesConfigCache(config)
|
||||
|
||||
if (saveVersionRef.current === v) {
|
||||
const discoverySignature = repoDiscoveryPolicySignature(repoDiscoveryPolicyFromConfig(config))
|
||||
|
||||
if (savedDiscoverySignatureRef.current !== discoverySignature) {
|
||||
savedDiscoverySignatureRef.current = discoverySignature
|
||||
await scanAndRecordRepos(true)
|
||||
}
|
||||
|
||||
onConfigSaved?.()
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ export function setApiRequestProfile(profile: null | string): void {
|
|||
|
||||
function profileScoped(profile?: null | string): { profile?: string } {
|
||||
const selected = profile === undefined ? _apiProfile : profile
|
||||
|
||||
return selected ? { profile: selected } : {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,10 +90,7 @@ if (
|
|||
// Inverse: persisted/default active id is still the live-preview tab, but that
|
||||
// target isn't open and file tabs are. Point at the first file tab so ⌘W and
|
||||
// the strip agree before React's fallback sync runs.
|
||||
if (
|
||||
$rightRailActiveTabId.get() === RIGHT_RAIL_PREVIEW_TAB_ID &&
|
||||
$filePreviewTabs.get().length > 0
|
||||
) {
|
||||
if ($rightRailActiveTabId.get() === RIGHT_RAIL_PREVIEW_TAB_ID && $filePreviewTabs.get().length > 0) {
|
||||
selectRightRailTab($filePreviewTabs.get()[0]!.id)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ describe('repository discovery policy', () => {
|
|||
const gateway = { connectionState: 'open', request }
|
||||
activeGateway.mockReturnValue(gateway as never)
|
||||
gatewayAtom.set(gateway as never)
|
||||
|
||||
return gateway
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +302,7 @@ describe('repository discovery policy', () => {
|
|||
? { active_id: null, projects: [], scoped_session_ids: [] }
|
||||
: { accepted: false, repos: [] }
|
||||
)
|
||||
|
||||
gatewayWith(request)
|
||||
const scanRepos = vi.fn()
|
||||
desktopGit.mockReturnValue({ scanRepos } as never)
|
||||
|
|
@ -327,6 +329,7 @@ describe('repository discovery policy', () => {
|
|||
? { active_id: null, projects: [], scoped_session_ids: [] }
|
||||
: { accepted: true, repos: [] }
|
||||
)
|
||||
|
||||
gatewayWith(request)
|
||||
const scanRepos = vi.fn().mockResolvedValue([{ label: 'repo', root: '/work/repo' }])
|
||||
desktopGit.mockReturnValue({ scanRepos } as never)
|
||||
|
|
@ -370,10 +373,13 @@ describe('repository discovery policy', () => {
|
|||
describe('project tree profile isolation', () => {
|
||||
it('does not publish a late response from the previous profile', async () => {
|
||||
let resolveA: ((value: unknown) => void) | undefined
|
||||
|
||||
const responseA = new Promise(resolve => {
|
||||
resolveA = resolve
|
||||
})
|
||||
|
||||
const gatewayA = { connectionState: 'open', request: vi.fn(() => responseA) }
|
||||
|
||||
const gatewayB = {
|
||||
connectionState: 'open',
|
||||
request: vi.fn().mockResolvedValue({
|
||||
|
|
@ -382,6 +388,7 @@ describe('project tree profile isolation', () => {
|
|||
scoped_session_ids: []
|
||||
})
|
||||
}
|
||||
|
||||
let current = gatewayA
|
||||
activeGateway.mockImplementation(() => current as never)
|
||||
gatewayAtom.set(gatewayA as never)
|
||||
|
|
|
|||
|
|
@ -282,12 +282,15 @@ interface ActiveProjectsContext {
|
|||
async function activeProjectsContext(): Promise<ActiveProjectsContext> {
|
||||
const profile = $activeGatewayProfile.get() || 'default'
|
||||
let gateway = activeGateway()
|
||||
|
||||
if (!gateway || gateway.connectionState !== 'open') {
|
||||
gateway = await ensureActiveGatewayOpen()
|
||||
}
|
||||
|
||||
if (!gateway || gateway !== activeGateway() || profile !== ($activeGatewayProfile.get() || 'default')) {
|
||||
throw new Error('Active Hermes profile changed while connecting')
|
||||
}
|
||||
|
||||
return { gateway, profile }
|
||||
}
|
||||
|
||||
|
|
@ -318,13 +321,16 @@ let projectTreeRefreshGeneration = 0
|
|||
|
||||
async function refreshProjectTreeOn(gateway: HermesGateway): Promise<void> {
|
||||
const generation = ++projectTreeRefreshGeneration
|
||||
|
||||
if (activeGateway() === gateway) {
|
||||
$projectTreeLoading.set(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await gatewayRequestOn<ProjectTreePayload>(gateway, 'projects.tree', {
|
||||
preview_limit: 3
|
||||
})
|
||||
|
||||
if (generation !== projectTreeRefreshGeneration || activeGateway() !== gateway) {
|
||||
return
|
||||
}
|
||||
|
|
@ -333,12 +339,15 @@ async function refreshProjectTreeOn(gateway: HermesGateway): Promise<void> {
|
|||
$projectTree.set(res.projects ?? [])
|
||||
$activeProjectId.set(res.active_id ?? null)
|
||||
const tombstones = $removedSessionIds.get()
|
||||
|
||||
if (tombstones.size) {
|
||||
const pending = new Set([...tombstones].filter(id => scoped.has(id)))
|
||||
|
||||
if (pending.size !== tombstones.size) {
|
||||
$removedSessionIds.set(pending)
|
||||
}
|
||||
}
|
||||
|
||||
markProjectsRpcSuccess()
|
||||
} catch (err) {
|
||||
if (activeGateway() === gateway) {
|
||||
|
|
@ -386,6 +395,7 @@ export interface RepoDiscoveryPolicy {
|
|||
|
||||
export function repoDiscoveryPolicyFromConfig(config: unknown): RepoDiscoveryPolicy {
|
||||
const desktopValue = config && typeof config === 'object' ? (config as { desktop?: unknown }).desktop : undefined
|
||||
|
||||
const desktop =
|
||||
desktopValue && typeof desktopValue === 'object'
|
||||
? (desktopValue as {
|
||||
|
|
@ -394,6 +404,7 @@ export function repoDiscoveryPolicyFromConfig(config: unknown): RepoDiscoveryPol
|
|||
repo_scan_roots?: unknown
|
||||
})
|
||||
: {}
|
||||
|
||||
return {
|
||||
enabled: desktop.repo_scan_enabled !== false,
|
||||
roots: Array.isArray(desktop.repo_scan_roots)
|
||||
|
|
@ -431,6 +442,7 @@ export async function scanAndRecordRepos(force = false): Promise<void> {
|
|||
}
|
||||
|
||||
let context: ActiveProjectsContext
|
||||
|
||||
try {
|
||||
context = await activeProjectsContext()
|
||||
} catch {
|
||||
|
|
@ -438,6 +450,7 @@ export async function scanAndRecordRepos(force = false): Promise<void> {
|
|||
}
|
||||
|
||||
const scan = desktopGit()?.scanRepos
|
||||
|
||||
if (!scan) {
|
||||
return
|
||||
}
|
||||
|
|
@ -449,12 +462,14 @@ export async function scanAndRecordRepos(force = false): Promise<void> {
|
|||
try {
|
||||
const policy = repoDiscoveryPolicyFromConfig(await getHermesConfig(context.profile))
|
||||
const signature = repoDiscoveryPolicySignature(policy)
|
||||
|
||||
if (!force && (state.completedSignature === signature || state.runningSignature === signature)) {
|
||||
return
|
||||
}
|
||||
|
||||
generation = ++state.generation
|
||||
state.runningSignature = signature
|
||||
|
||||
if (!policy.enabled) {
|
||||
await gatewayRequestOn(context.gateway, 'projects.record_repos', {
|
||||
discovery_policy: policy,
|
||||
|
|
@ -463,13 +478,16 @@ export async function scanAndRecordRepos(force = false): Promise<void> {
|
|||
} else {
|
||||
scanningGatewayGenerations.set(context.gateway, generation)
|
||||
syncReposScanning()
|
||||
|
||||
const repos = await scan(policy.roots, {
|
||||
enabled: true,
|
||||
excludePaths: policy.exclude_paths
|
||||
})
|
||||
|
||||
if (state.generation !== generation) {
|
||||
return
|
||||
}
|
||||
|
||||
await gatewayRequestOn(context.gateway, 'projects.record_repos', {
|
||||
discovery_policy: policy,
|
||||
repos
|
||||
|
|
@ -479,15 +497,18 @@ export async function scanAndRecordRepos(force = false): Promise<void> {
|
|||
if (state.generation !== generation) {
|
||||
return
|
||||
}
|
||||
|
||||
state.completedSignature = signature
|
||||
await refreshProjectTreeOn(context.gateway)
|
||||
} catch {
|
||||
state.completedSignature = undefined
|
||||
} finally {
|
||||
state.runningSignature = undefined
|
||||
|
||||
if (scanningGatewayGenerations.get(context.gateway) === generation) {
|
||||
scanningGatewayGenerations.delete(context.gateway)
|
||||
}
|
||||
|
||||
syncReposScanning()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue