import * as fs from 'fs/promises' import * as path from 'path' import { findInstallRoot } from '../install.js' import { packageJsonAssetDependencies } from '../package-json.js' import { assetDependencyAlias, assetDependencyRange, type AssetDependencyAlias, } from '../schemas.js' import { listResult } from '../output.js' export interface ListCommandOptions { cwd?: string } export interface InstalledAssetListing { name: string range: string /** Canonical install path → the project's current path (a rename) or `false` (a tombstone: * the file is intentionally not installed). */ alias: AssetDependencyAlias } export interface ListInstalledAssetsResult { projectRoot: string packageJsonPath: string assets: InstalledAssetListing[] } export async function listCommand(opts: ListCommandOptions): Promise { const result = await listInstalledAssets(opts.cwd) console.log(listResult(result)) } /** The project's declared asset dependencies — package.json is the single source of truth. */ export async function listInstalledAssets( cwd: string | undefined, ): Promise { const projectRoot = await findInstallRoot(cwd ?? process.cwd()) const packageJsonPath = path.join(projectRoot, 'package.json') const dependencies = await readAssetDependencies(packageJsonPath) const assets = Object.entries(dependencies) .map(([name, value]) => ({ name, range: assetDependencyRange(value), alias: assetDependencyAlias(value), })) .sort((a, b) => a.name.localeCompare(b.name)) return { projectRoot, packageJsonPath, assets, } } async function readAssetDependencies(packageJsonPath: string) { try { return packageJsonAssetDependencies( await fs.readFile(packageJsonPath, 'utf-8'), packageJsonPath, ) } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return {} throw error } }