(root: string)
| 3593 | } |
| 3594 | |
| 3595 | export async function listAssets(root: string): Promise<AssetMeta[]> { |
| 3596 | const out: AssetMeta[] = [] |
| 3597 | const walk = async (dirAbs: string, dirReal: string, ancestors: Set<string>): Promise<void> => { |
| 3598 | let entries: Dirent[] |
| 3599 | try { |
| 3600 | entries = await fs.readdir(dirAbs, { withFileTypes: true }) |
| 3601 | } catch { |
| 3602 | return |
| 3603 | } |
| 3604 | for (const [index, entry] of entries.entries()) { |
| 3605 | if (entry.name.startsWith('.')) continue |
| 3606 | const full = path.join(dirAbs, entry.name) |
| 3607 | const childReal = await resolveDirDescent(full, entry, dirReal, ancestors) |
| 3608 | if (childReal !== null) { |
| 3609 | if (dirAbs === root && entry.name === INTERNAL_VAULT_DIR) continue |
| 3610 | // A `<Name>.base` database folder isn't an asset container — its |
| 3611 | // data.csv/schema.json/record notes never appear in the assets list. |
| 3612 | if (isFormDirName(entry.name)) continue |
| 3613 | ancestors.add(childReal) |
| 3614 | await walk(full, childReal, ancestors) |
| 3615 | ancestors.delete(childReal) |
| 3616 | continue |
| 3617 | } |
| 3618 | if (!entry.isFile()) continue |
| 3619 | if (entry.name.toLowerCase().endsWith('.md')) continue |
| 3620 | // Excalidraw drawings are a first-class file type (listed with notes), not |
| 3621 | // a generic attachment. |
| 3622 | if (isExcalidrawPath(entry.name)) continue |
| 3623 | // Legacy co-located sidecar/.bak (pre-migration) — not a user asset. |
| 3624 | if (isDatabaseInternalPath(entry.name)) continue |
| 3625 | let stat |
| 3626 | try { |
| 3627 | stat = await fs.stat(full) |
| 3628 | } catch { |
| 3629 | continue |
| 3630 | } |
| 3631 | const rel = toPosix(path.relative(root, full)) |
| 3632 | out.push({ |
| 3633 | path: rel, |
| 3634 | name: path.basename(full), |
| 3635 | kind: classifyImportedAsset(entry.name), |
| 3636 | siblingOrder: index, |
| 3637 | size: stat.size, |
| 3638 | updatedAt: stat.mtimeMs |
| 3639 | }) |
| 3640 | } |
| 3641 | } |
| 3642 | |
| 3643 | const rootReal = await realpathOrResolve(root) |
| 3644 | await walk(root, rootReal, new Set([rootReal])) |
| 3645 | out.sort((a, b) => b.updatedAt - a.updatedAt || a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) |
| 3646 | return out |
| 3647 | } |
| 3648 | |
| 3649 | /* ---------- Notes ---------------------------------------------------- */ |
| 3650 |
no test coverage detected