|
| 1 | +import * as fs from 'fs/promises' |
| 2 | +import * as path from 'path' |
| 3 | +import type { CommandModule } from 'yargs' |
| 4 | +import { |
| 5 | + PatchManifestSchema, |
| 6 | + DEFAULT_PATCH_MANIFEST_PATH, |
| 7 | +} from '../schema/manifest-schema.js' |
| 8 | + |
| 9 | +interface ListArgs { |
| 10 | + cwd: string |
| 11 | + 'manifest-path': string |
| 12 | + json: boolean |
| 13 | +} |
| 14 | + |
| 15 | +async function listPatches( |
| 16 | + manifestPath: string, |
| 17 | + outputJson: boolean, |
| 18 | +): Promise<void> { |
| 19 | + // Read and parse manifest |
| 20 | + const manifestContent = await fs.readFile(manifestPath, 'utf-8') |
| 21 | + const manifestData = JSON.parse(manifestContent) |
| 22 | + const manifest = PatchManifestSchema.parse(manifestData) |
| 23 | + |
| 24 | + const patchEntries = Object.entries(manifest.patches) |
| 25 | + |
| 26 | + if (patchEntries.length === 0) { |
| 27 | + if (outputJson) { |
| 28 | + console.log(JSON.stringify({ patches: [] }, null, 2)) |
| 29 | + } else { |
| 30 | + console.log('No patches found in manifest.') |
| 31 | + } |
| 32 | + return |
| 33 | + } |
| 34 | + |
| 35 | + if (outputJson) { |
| 36 | + // Output as JSON for machine consumption |
| 37 | + const jsonOutput = { |
| 38 | + patches: patchEntries.map(([purl, patch]) => ({ |
| 39 | + purl, |
| 40 | + uuid: patch.uuid, |
| 41 | + exportedAt: patch.exportedAt, |
| 42 | + tier: patch.tier, |
| 43 | + license: patch.license, |
| 44 | + description: patch.description, |
| 45 | + files: Object.keys(patch.files), |
| 46 | + vulnerabilities: Object.entries(patch.vulnerabilities).map( |
| 47 | + ([id, vuln]) => ({ |
| 48 | + id, |
| 49 | + cves: vuln.cves, |
| 50 | + summary: vuln.summary, |
| 51 | + severity: vuln.severity, |
| 52 | + description: vuln.description, |
| 53 | + }), |
| 54 | + ), |
| 55 | + })), |
| 56 | + } |
| 57 | + console.log(JSON.stringify(jsonOutput, null, 2)) |
| 58 | + } else { |
| 59 | + // Human-readable output |
| 60 | + console.log(`Found ${patchEntries.length} patch(es):\n`) |
| 61 | + |
| 62 | + for (const [purl, patch] of patchEntries) { |
| 63 | + console.log(`Package: ${purl}`) |
| 64 | + console.log(` UUID: ${patch.uuid}`) |
| 65 | + console.log(` Tier: ${patch.tier}`) |
| 66 | + console.log(` License: ${patch.license}`) |
| 67 | + console.log(` Exported: ${patch.exportedAt}`) |
| 68 | + |
| 69 | + if (patch.description) { |
| 70 | + console.log(` Description: ${patch.description}`) |
| 71 | + } |
| 72 | + |
| 73 | + // List vulnerabilities |
| 74 | + const vulnEntries = Object.entries(patch.vulnerabilities) |
| 75 | + if (vulnEntries.length > 0) { |
| 76 | + console.log(` Vulnerabilities (${vulnEntries.length}):`) |
| 77 | + for (const [id, vuln] of vulnEntries) { |
| 78 | + const cveList = vuln.cves.length > 0 ? ` (${vuln.cves.join(', ')})` : '' |
| 79 | + console.log(` - ${id}${cveList}`) |
| 80 | + console.log(` Severity: ${vuln.severity}`) |
| 81 | + console.log(` Summary: ${vuln.summary}`) |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + // List files being patched |
| 86 | + const fileList = Object.keys(patch.files) |
| 87 | + if (fileList.length > 0) { |
| 88 | + console.log(` Files patched (${fileList.length}):`) |
| 89 | + for (const filePath of fileList) { |
| 90 | + console.log(` - ${filePath}`) |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + console.log('') // Empty line between patches |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +export const listCommand: CommandModule<{}, ListArgs> = { |
| 100 | + command: 'list', |
| 101 | + describe: 'List all patches in the local manifest', |
| 102 | + builder: yargs => { |
| 103 | + return yargs |
| 104 | + .option('cwd', { |
| 105 | + describe: 'Working directory', |
| 106 | + type: 'string', |
| 107 | + default: process.cwd(), |
| 108 | + }) |
| 109 | + .option('manifest-path', { |
| 110 | + alias: 'm', |
| 111 | + describe: 'Path to patch manifest file', |
| 112 | + type: 'string', |
| 113 | + default: DEFAULT_PATCH_MANIFEST_PATH, |
| 114 | + }) |
| 115 | + .option('json', { |
| 116 | + describe: 'Output as JSON', |
| 117 | + type: 'boolean', |
| 118 | + default: false, |
| 119 | + }) |
| 120 | + }, |
| 121 | + handler: async argv => { |
| 122 | + try { |
| 123 | + const manifestPath = path.isAbsolute(argv['manifest-path']) |
| 124 | + ? argv['manifest-path'] |
| 125 | + : path.join(argv.cwd, argv['manifest-path']) |
| 126 | + |
| 127 | + // Check if manifest exists |
| 128 | + try { |
| 129 | + await fs.access(manifestPath) |
| 130 | + } catch { |
| 131 | + if (argv.json) { |
| 132 | + console.log(JSON.stringify({ error: 'Manifest not found', path: manifestPath }, null, 2)) |
| 133 | + } else { |
| 134 | + console.error(`Manifest not found at ${manifestPath}`) |
| 135 | + } |
| 136 | + process.exit(1) |
| 137 | + } |
| 138 | + |
| 139 | + await listPatches(manifestPath, argv.json) |
| 140 | + process.exit(0) |
| 141 | + } catch (err) { |
| 142 | + const errorMessage = err instanceof Error ? err.message : String(err) |
| 143 | + if (argv.json) { |
| 144 | + console.log(JSON.stringify({ error: errorMessage }, null, 2)) |
| 145 | + } else { |
| 146 | + console.error(`Error: ${errorMessage}`) |
| 147 | + } |
| 148 | + process.exit(1) |
| 149 | + } |
| 150 | + }, |
| 151 | +} |
0 commit comments