Skip to content

Commit 06d5d8f

Browse files
Add programmatic API and update to v1.0.0 (#19)
* Add programmatic API and update to v1.0.0 - Add runPatch() function for programmatic usage from socket-cli - Add ./run export in package.json for subpath import - Update postinstall detection to use socket CLI subcommand format - Change generated postinstall from npx @socketsecurity/socket-patch to socket patch apply - Add debug logging support via SOCKET_PATCH_DEBUG env var - Bump version to 1.0.0 for first stable release * Exit successfully when no .socket folder exists The apply command now exits with code 0 when no manifest is found, treating it as a successful no-op. This allows postinstall scripts to run without failing when no patches are configured.
1 parent 8b96959 commit 06d5d8f

File tree

7 files changed

+239
-31
lines changed

7 files changed

+239
-31
lines changed

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@socketsecurity/socket-patch",
3-
"version": "0.3.0",
3+
"version": "1.0.0",
44
"packageManager": "[email protected]",
55
"description": "CLI tool for applying security patches to dependencies",
66
"main": "dist/index.js",
@@ -48,6 +48,11 @@
4848
"types": "./dist/package-json/index.d.ts",
4949
"require": "./dist/package-json/index.js",
5050
"import": "./dist/package-json/index.js"
51+
},
52+
"./run": {
53+
"types": "./dist/run.d.ts",
54+
"require": "./dist/run.js",
55+
"import": "./dist/run.js"
5156
}
5257
},
5358
"scripts": {

src/commands/apply.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,14 +228,15 @@ export const applyCommand: CommandModule<{}, ApplyArgs> = {
228228
? argv['manifest-path']
229229
: path.join(argv.cwd, argv['manifest-path'])
230230

231-
// Check if manifest exists
231+
// Check if manifest exists - exit successfully if no .socket folder is set up
232232
try {
233233
await fs.access(manifestPath)
234234
} catch {
235+
// No manifest means no patches to apply - this is a successful no-op
235236
if (!argv.silent) {
236-
console.error(`Manifest not found at ${manifestPath}`)
237+
console.log('No .socket folder found, skipping patch application.')
237238
}
238-
process.exit(1)
239+
process.exit(0)
239240
}
240241

241242
const { success, results } = await applyPatches(

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export * from './manifest/recovery.js'
1515

1616
// Re-export constants
1717
export * from './constants.js'
18+
19+
// Re-export programmatic API
20+
export { runPatch } from './run.js'
21+
export type { PatchOptions } from './run.js'

src/package-json/detect.test.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,40 @@ describe('isPostinstallConfigured', () => {
313313
assert.equal(result.configured, true)
314314
assert.equal(result.needsUpdate, false)
315315
})
316+
317+
it('should detect socket patch apply (Socket CLI subcommand) as configured', () => {
318+
const packageJson = {
319+
name: 'test',
320+
version: '1.0.0',
321+
scripts: {
322+
postinstall: 'socket patch apply',
323+
},
324+
}
325+
326+
const result = isPostinstallConfigured(packageJson)
327+
328+
assert.equal(
329+
result.configured,
330+
true,
331+
'socket patch apply (CLI subcommand) should be recognized',
332+
)
333+
assert.equal(result.needsUpdate, false)
334+
})
335+
336+
it('should detect socket patch apply with --silent flag as configured', () => {
337+
const packageJson = {
338+
name: 'test',
339+
version: '1.0.0',
340+
scripts: {
341+
postinstall: 'socket patch apply --silent',
342+
},
343+
}
344+
345+
const result = isPostinstallConfigured(packageJson)
346+
347+
assert.equal(result.configured, true)
348+
assert.equal(result.needsUpdate, false)
349+
})
316350
})
317351

318352
describe('Edge Case 5: Invalid or malformed data', () => {
@@ -377,19 +411,19 @@ describe('isPostinstallConfigured', () => {
377411
describe('generateUpdatedPostinstall', () => {
378412
it('should create command for empty string', () => {
379413
const result = generateUpdatedPostinstall('')
380-
assert.equal(result, 'npx @socketsecurity/socket-patch apply')
414+
assert.equal(result, 'socket patch apply --silent')
381415
})
382416

383417
it('should create command for whitespace-only string', () => {
384418
const result = generateUpdatedPostinstall(' \n\t ')
385-
assert.equal(result, 'npx @socketsecurity/socket-patch apply')
419+
assert.equal(result, 'socket patch apply --silent')
386420
})
387421

388422
it('should prepend to existing script', () => {
389423
const result = generateUpdatedPostinstall('echo "Hello"')
390424
assert.equal(
391425
result,
392-
'npx @socketsecurity/socket-patch apply && echo "Hello"',
426+
'socket patch apply --silent && echo "Hello"',
393427
)
394428
})
395429

@@ -405,13 +439,25 @@ describe('generateUpdatedPostinstall', () => {
405439
assert.equal(result, existing)
406440
})
407441

408-
it('should prepend to script with socket apply (main CLI)', () => {
442+
it('should preserve socket patch apply (CLI subcommand)', () => {
443+
const existing = 'socket patch apply'
444+
const result = generateUpdatedPostinstall(existing)
445+
assert.equal(result, existing)
446+
})
447+
448+
it('should preserve socket patch apply --silent', () => {
449+
const existing = 'socket patch apply --silent'
450+
const result = generateUpdatedPostinstall(existing)
451+
assert.equal(result, existing)
452+
})
453+
454+
it('should prepend to script with socket apply (non-patch command)', () => {
409455
const existing = 'socket apply'
410456
const result = generateUpdatedPostinstall(existing)
411457
assert.equal(
412458
result,
413-
'npx @socketsecurity/socket-patch apply && socket apply',
414-
'Should add socket-patch even if socket apply is present',
459+
'socket patch apply --silent && socket apply',
460+
'Should add socket patch apply even if socket apply is present',
415461
)
416462
})
417463
})
@@ -430,7 +476,7 @@ describe('updatePackageJsonContent', () => {
430476
assert.ok(updated.scripts)
431477
assert.equal(
432478
updated.scripts.postinstall,
433-
'npx @socketsecurity/socket-patch apply',
479+
'socket patch apply --silent',
434480
)
435481
})
436482

@@ -450,7 +496,7 @@ describe('updatePackageJsonContent', () => {
450496
const updated = JSON.parse(result.content)
451497
assert.equal(
452498
updated.scripts.postinstall,
453-
'npx @socketsecurity/socket-patch apply',
499+
'socket patch apply --silent',
454500
)
455501
assert.equal(updated.scripts.test, 'jest', 'Should preserve other scripts')
456502
assert.equal(updated.scripts.build, 'tsc', 'Should preserve other scripts')
@@ -471,11 +517,11 @@ describe('updatePackageJsonContent', () => {
471517
assert.equal(result.oldScript, 'echo "Setup complete"')
472518
assert.equal(
473519
result.newScript,
474-
'npx @socketsecurity/socket-patch apply && echo "Setup complete"',
520+
'socket patch apply --silent && echo "Setup complete"',
475521
)
476522
})
477523

478-
it('should not modify when already configured', () => {
524+
it('should not modify when already configured with legacy format', () => {
479525
const content = JSON.stringify({
480526
name: 'test',
481527
version: '1.0.0',
@@ -490,6 +536,21 @@ describe('updatePackageJsonContent', () => {
490536
assert.equal(result.content, content)
491537
})
492538

539+
it('should not modify when already configured with socket patch apply', () => {
540+
const content = JSON.stringify({
541+
name: 'test',
542+
version: '1.0.0',
543+
scripts: {
544+
postinstall: 'socket patch apply --silent',
545+
},
546+
})
547+
548+
const result = updatePackageJsonContent(content)
549+
550+
assert.equal(result.modified, false)
551+
assert.equal(result.content, content)
552+
})
553+
493554
it('should throw error for invalid JSON', () => {
494555
const content = '{ invalid json }'
495556

@@ -514,7 +575,7 @@ describe('updatePackageJsonContent', () => {
514575
const updated = JSON.parse(result.content)
515576
assert.equal(
516577
updated.scripts.postinstall,
517-
'npx @socketsecurity/socket-patch apply',
578+
'socket patch apply --silent',
518579
)
519580
})
520581

@@ -533,7 +594,7 @@ describe('updatePackageJsonContent', () => {
533594
const updated = JSON.parse(result.content)
534595
assert.equal(
535596
updated.scripts.postinstall,
536-
'npx @socketsecurity/socket-patch apply',
597+
'socket patch apply --silent',
537598
)
538599
})
539600

src/package-json/detect.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
/**
2-
* Shared logic for detecting and generating postinstall scripts
3-
* Used by both CLI and GitHub bot
2+
* Shared logic for detecting and generating postinstall scripts.
3+
* Used by both CLI and GitHub bot.
44
*/
55

6-
const SOCKET_PATCH_COMMAND = 'npx @socketsecurity/socket-patch apply'
6+
// The command to run for applying patches via socket CLI.
7+
const SOCKET_PATCH_COMMAND = 'socket patch apply --silent'
8+
9+
// Legacy command patterns to detect existing configurations.
10+
const LEGACY_PATCH_PATTERNS = [
11+
'socket-patch apply',
12+
'npx @socketsecurity/socket-patch apply',
13+
'socket patch apply',
14+
]
715

816
export interface PostinstallStatus {
917
configured: boolean
@@ -34,11 +42,13 @@ export function isPostinstallConfigured(
3442
}
3543

3644
const rawPostinstall = packageJson.scripts?.postinstall
37-
// Handle non-string values (null, object, array) by treating as empty string
45+
// Handle non-string values (null, object, array) by treating as empty string.
3846
const currentScript = typeof rawPostinstall === 'string' ? rawPostinstall : ''
3947

40-
// Check if socket-patch apply is already present
41-
const configured = currentScript.includes('socket-patch apply')
48+
// Check if any socket-patch apply variant is already present.
49+
const configured = LEGACY_PATCH_PATTERNS.some(pattern =>
50+
currentScript.includes(pattern),
51+
)
4252

4353
return {
4454
configured,
@@ -48,25 +58,28 @@ export function isPostinstallConfigured(
4858
}
4959

5060
/**
51-
* Generate an updated postinstall script that includes socket-patch
61+
* Generate an updated postinstall script that includes socket-patch.
5262
*/
5363
export function generateUpdatedPostinstall(
5464
currentPostinstall: string,
5565
): string {
5666
const trimmed = currentPostinstall.trim()
5767

58-
// If empty, just add the socket-patch command
68+
// If empty, just add the socket-patch command.
5969
if (!trimmed) {
6070
return SOCKET_PATCH_COMMAND
6171
}
6272

63-
// If socket-patch is already present, return unchanged
64-
if (trimmed.includes('socket-patch apply')) {
73+
// If any socket-patch variant is already present, return unchanged.
74+
const alreadyConfigured = LEGACY_PATCH_PATTERNS.some(pattern =>
75+
trimmed.includes(pattern),
76+
)
77+
if (alreadyConfigured) {
6578
return trimmed
6679
}
6780

68-
// Prepend socket-patch command so it runs first, then existing script
69-
// Using && ensures existing script only runs if patching succeeds
81+
// Prepend socket-patch command so it runs first, then existing script.
82+
// Using && ensures existing script only runs if patching succeeds.
7083
return `${SOCKET_PATCH_COMMAND} && ${trimmed}`
7184
}
7285

src/run.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import yargs from 'yargs'
2+
import { applyCommand } from './commands/apply.js'
3+
import { getCommand } from './commands/get.js'
4+
import { listCommand } from './commands/list.js'
5+
import { removeCommand } from './commands/remove.js'
6+
import { rollbackCommand } from './commands/rollback.js'
7+
import { repairCommand } from './commands/repair.js'
8+
import { setupCommand } from './commands/setup.js'
9+
10+
/**
11+
* Configuration options for running socket-patch programmatically.
12+
*/
13+
export interface PatchOptions {
14+
/** Socket API URL (e.g., https://api.socket.dev). */
15+
apiUrl?: string
16+
/** Socket API token for authentication. */
17+
apiToken?: string
18+
/** Organization slug. */
19+
orgSlug?: string
20+
/** Public patch API proxy URL. */
21+
patchProxyUrl?: string
22+
/** HTTP proxy URL for all requests. */
23+
httpProxy?: string
24+
/** Enable debug logging. */
25+
debug?: boolean
26+
}
27+
28+
/**
29+
* Run socket-patch programmatically with provided arguments and options.
30+
* Maps options to environment variables before executing yargs commands.
31+
*
32+
* @param args - Command line arguments to pass to yargs (e.g., ['get', 'CVE-2021-44228']).
33+
* @param options - Configuration options that override environment variables.
34+
* @returns Exit code (0 for success, non-zero for failure).
35+
*/
36+
export async function runPatch(
37+
args: string[],
38+
options?: PatchOptions
39+
): Promise<number> {
40+
// Map options to environment variables.
41+
if (options?.apiUrl) {
42+
process.env.SOCKET_API_URL = options.apiUrl
43+
}
44+
if (options?.apiToken) {
45+
process.env.SOCKET_API_TOKEN = options.apiToken
46+
}
47+
if (options?.orgSlug) {
48+
process.env.SOCKET_ORG_SLUG = options.orgSlug
49+
}
50+
if (options?.patchProxyUrl) {
51+
process.env.SOCKET_PATCH_PROXY_URL = options.patchProxyUrl
52+
}
53+
if (options?.httpProxy) {
54+
process.env.SOCKET_PATCH_HTTP_PROXY = options.httpProxy
55+
}
56+
if (options?.debug) {
57+
process.env.SOCKET_PATCH_DEBUG = '1'
58+
}
59+
60+
try {
61+
await yargs(args)
62+
.scriptName('socket patch')
63+
.usage('$0 <command> [options]')
64+
.command(getCommand)
65+
.command(applyCommand)
66+
.command(rollbackCommand)
67+
.command(removeCommand)
68+
.command(listCommand)
69+
.command(setupCommand)
70+
.command(repairCommand)
71+
.demandCommand(1, 'You must specify a command')
72+
.help()
73+
.alias('h', 'help')
74+
.strict()
75+
.parse()
76+
77+
return 0
78+
} catch (error) {
79+
if (process.env.SOCKET_PATCH_DEBUG) {
80+
console.error('socket-patch error:', error)
81+
}
82+
return 1
83+
}
84+
}

0 commit comments

Comments
 (0)