Skip to content

Commit 8ffa6ef

Browse files
Merge pull request #2 from SocketDev/mik/socket-patch-list-remove-gc
Add new commands for patch remove, list and GC
2 parents 53ac4d1 + 5c41dcf commit 8ffa6ef

File tree

4 files changed

+384
-0
lines changed

4 files changed

+384
-0
lines changed

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import yargs from 'yargs'
44
import { hideBin } from 'yargs/helpers'
55
import { applyCommand } from './commands/apply.js'
66
import { downloadCommand } from './commands/download.js'
7+
import { listCommand } from './commands/list.js'
8+
import { removeCommand } from './commands/remove.js'
9+
import { gcCommand } from './commands/gc.js'
710

811
async function main(): Promise<void> {
912
await yargs(hideBin(process.argv))
1013
.scriptName('socket-patch')
1114
.usage('$0 <command> [options]')
1215
.command(applyCommand)
1316
.command(downloadCommand)
17+
.command(listCommand)
18+
.command(removeCommand)
19+
.command(gcCommand)
1420
.demandCommand(1, 'You must specify a command')
1521
.help()
1622
.alias('h', 'help')

src/commands/gc.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
import {
9+
cleanupUnusedBlobs,
10+
formatCleanupResult,
11+
} from '../utils/cleanup-blobs.js'
12+
13+
interface GCArgs {
14+
cwd: string
15+
'manifest-path': string
16+
'dry-run': boolean
17+
}
18+
19+
async function garbageCollect(
20+
manifestPath: string,
21+
dryRun: boolean,
22+
): Promise<void> {
23+
// Read and parse manifest
24+
const manifestContent = await fs.readFile(manifestPath, 'utf-8')
25+
const manifestData = JSON.parse(manifestContent)
26+
const manifest = PatchManifestSchema.parse(manifestData)
27+
28+
// Find .socket directory (contains blobs)
29+
const socketDir = path.dirname(manifestPath)
30+
const blobsPath = path.join(socketDir, 'blobs')
31+
32+
// Run cleanup
33+
const cleanupResult = await cleanupUnusedBlobs(manifest, blobsPath, dryRun)
34+
35+
// Display results
36+
if (cleanupResult.blobsChecked === 0) {
37+
console.log('No blobs directory found, nothing to clean up.')
38+
} else if (cleanupResult.blobsRemoved === 0) {
39+
console.log(
40+
`Checked ${cleanupResult.blobsChecked} blob(s), all are in use.`,
41+
)
42+
} else {
43+
console.log(formatCleanupResult(cleanupResult, dryRun))
44+
45+
if (!dryRun) {
46+
console.log('\nGarbage collection complete.')
47+
}
48+
}
49+
}
50+
51+
export const gcCommand: CommandModule<{}, GCArgs> = {
52+
command: 'gc',
53+
describe: 'Clean up unused blob files from .socket/blobs directory',
54+
builder: yargs => {
55+
return yargs
56+
.option('cwd', {
57+
describe: 'Working directory',
58+
type: 'string',
59+
default: process.cwd(),
60+
})
61+
.option('manifest-path', {
62+
alias: 'm',
63+
describe: 'Path to patch manifest file',
64+
type: 'string',
65+
default: DEFAULT_PATCH_MANIFEST_PATH,
66+
})
67+
.option('dry-run', {
68+
alias: 'd',
69+
describe: 'Show what would be removed without actually removing',
70+
type: 'boolean',
71+
default: false,
72+
})
73+
},
74+
handler: async argv => {
75+
try {
76+
const manifestPath = path.isAbsolute(argv['manifest-path'])
77+
? argv['manifest-path']
78+
: path.join(argv.cwd, argv['manifest-path'])
79+
80+
// Check if manifest exists
81+
try {
82+
await fs.access(manifestPath)
83+
} catch {
84+
console.error(`Manifest not found at ${manifestPath}`)
85+
process.exit(1)
86+
}
87+
88+
await garbageCollect(manifestPath, argv['dry-run'])
89+
process.exit(0)
90+
} catch (err) {
91+
const errorMessage = err instanceof Error ? err.message : String(err)
92+
console.error(`Error: ${errorMessage}`)
93+
process.exit(1)
94+
}
95+
},
96+
}

src/commands/list.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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

Comments
 (0)