Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@socketsecurity/socket-patch",
"version": "0.3.0",
"version": "1.0.0",
"packageManager": "pnpm@10.16.1",
"description": "CLI tool for applying security patches to dependencies",
"main": "dist/index.js",
Expand Down Expand Up @@ -48,6 +48,11 @@
"types": "./dist/package-json/index.d.ts",
"require": "./dist/package-json/index.js",
"import": "./dist/package-json/index.js"
},
"./run": {
"types": "./dist/run.d.ts",
"require": "./dist/run.js",
"import": "./dist/run.js"
}
},
"scripts": {
Expand Down
32 changes: 29 additions & 3 deletions src/commands/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
formatFetchResult,
} from '../utils/blob-fetcher.js'
import { getGlobalPrefix } from '../utils/global-packages.js'
import {
trackPatchApplied,
trackPatchApplyFailed,
} from '../utils/telemetry.js'

interface ApplyArgs {
cwd: string
Expand Down Expand Up @@ -223,19 +227,24 @@ export const applyCommand: CommandModule<{}, ApplyArgs> = {
.example('$0 apply --dry-run', 'Preview patches without applying')
},
handler: async argv => {
// Get API credentials for authenticated telemetry (optional).
const apiToken = process.env['SOCKET_API_TOKEN']
const orgSlug = process.env['SOCKET_ORG_SLUG']

try {
const manifestPath = path.isAbsolute(argv['manifest-path'])
? argv['manifest-path']
: path.join(argv.cwd, argv['manifest-path'])

// Check if manifest exists
// Check if manifest exists - exit successfully if no .socket folder is set up
try {
await fs.access(manifestPath)
} catch {
// No manifest means no patches to apply - this is a successful no-op
if (!argv.silent) {
console.error(`Manifest not found at ${manifestPath}`)
console.log('No .socket folder found, skipping patch application.')
}
process.exit(1)
process.exit(0)
}

const { success, results } = await applyPatches(
Expand Down Expand Up @@ -275,8 +284,25 @@ export const applyCommand: CommandModule<{}, ApplyArgs> = {
}
}

// Track telemetry event.
const patchedCount = results.filter(r => r.success && r.filesPatched.length > 0).length
if (success) {
await trackPatchApplied(patchedCount, argv['dry-run'], apiToken, orgSlug)
} else {
await trackPatchApplyFailed(
new Error('One or more patches failed to apply'),
argv['dry-run'],
apiToken,
orgSlug,
)
}

process.exit(success ? 0 : 1)
} catch (err) {
// Track telemetry for unexpected errors.
const error = err instanceof Error ? err : new Error(String(err))
await trackPatchApplyFailed(error, argv['dry-run'], apiToken, orgSlug)

if (!argv.silent) {
const errorMessage = err instanceof Error ? err.message : String(err)
console.error(`Error: ${errorMessage}`)
Expand Down
25 changes: 25 additions & 0 deletions src/commands/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
formatCleanupResult,
} from '../utils/cleanup-blobs.js'
import { rollbackPatches } from './rollback.js'
import {
trackPatchRemoved,
trackPatchRemoveFailed,
} from '../utils/telemetry.js'

interface RemoveArgs {
identifier: string
Expand Down Expand Up @@ -118,6 +122,10 @@ export const removeCommand: CommandModule<{}, RemoveArgs> = {
)
},
handler: async argv => {
// Get API credentials for authenticated telemetry (optional).
const apiToken = process.env['SOCKET_API_TOKEN']
const orgSlug = process.env['SOCKET_ORG_SLUG']

try {
const manifestPath = path.isAbsolute(argv['manifest-path'])
? argv['manifest-path']
Expand Down Expand Up @@ -147,6 +155,11 @@ export const removeCommand: CommandModule<{}, RemoveArgs> = {
)

if (!rollbackSuccess) {
await trackPatchRemoveFailed(
new Error('Rollback failed during patch removal'),
apiToken,
orgSlug,
)
console.error(
'\nRollback failed. Use --skip-rollback to remove from manifest without restoring files.',
)
Expand Down Expand Up @@ -184,6 +197,11 @@ export const removeCommand: CommandModule<{}, RemoveArgs> = {
)

if (notFound) {
await trackPatchRemoveFailed(
new Error(`No patch found matching identifier: ${argv.identifier}`),
apiToken,
orgSlug,
)
console.error(`No patch found matching identifier: ${argv.identifier}`)
process.exit(1)
}
Expand All @@ -203,8 +221,15 @@ export const removeCommand: CommandModule<{}, RemoveArgs> = {
console.log(`\n${formatCleanupResult(cleanupResult, false)}`)
}

// Track successful removal.
await trackPatchRemoved(removed.length, apiToken, orgSlug)

process.exit(0)
} catch (err) {
// Track telemetry for unexpected errors.
const error = err instanceof Error ? err : new Error(String(err))
await trackPatchRemoveFailed(error, apiToken, orgSlug)

const errorMessage = err instanceof Error ? err.message : String(err)
console.error(`Error: ${errorMessage}`)
process.exit(1)
Expand Down
36 changes: 36 additions & 0 deletions src/commands/rollback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
} from '../utils/blob-fetcher.js'
import { getGlobalPrefix } from '../utils/global-packages.js'
import { getAPIClientFromEnv } from '../utils/api-client.js'
import {
trackPatchRolledBack,
trackPatchRollbackFailed,
} from '../utils/telemetry.js'

interface RollbackArgs {
identifier?: string
Expand Down Expand Up @@ -363,6 +367,10 @@ export const rollbackCommand: CommandModule<{}, RollbackArgs> = {
})
},
handler: async argv => {
// Get API credentials for authenticated telemetry (optional).
const apiToken = argv['api-token'] || process.env['SOCKET_API_TOKEN']
const orgSlug = argv.org || process.env['SOCKET_ORG_SLUG']

try {
// Handle one-off mode (no manifest required)
if (argv['one-off']) {
Expand All @@ -377,6 +385,18 @@ export const rollbackCommand: CommandModule<{}, RollbackArgs> = {
argv['api-url'],
argv['api-token'],
)

// Track telemetry for one-off rollback.
if (success) {
await trackPatchRolledBack(1, apiToken, orgSlug)
} else {
await trackPatchRollbackFailed(
new Error('One-off rollback failed'),
apiToken,
orgSlug,
)
}

process.exit(success ? 0 : 1)
}

Expand Down Expand Up @@ -444,8 +464,24 @@ export const rollbackCommand: CommandModule<{}, RollbackArgs> = {
}
}

// Track telemetry event.
const rolledBackCount = results.filter(r => r.success && r.filesRolledBack.length > 0).length
if (success) {
await trackPatchRolledBack(rolledBackCount, apiToken, orgSlug)
} else {
await trackPatchRollbackFailed(
new Error('One or more rollbacks failed'),
apiToken,
orgSlug,
)
}

process.exit(success ? 0 : 1)
} catch (err) {
// Track telemetry for unexpected errors.
const error = err instanceof Error ? err : new Error(String(err))
await trackPatchRollbackFailed(error, apiToken, orgSlug)

if (!argv.silent) {
const errorMessage = err instanceof Error ? err.message : String(err)
console.error(`Error: ${errorMessage}`)
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export * from './manifest/recovery.js'

// Re-export constants
export * from './constants.js'

// Re-export programmatic API
export { runPatch } from './run.js'
export type { PatchOptions } from './run.js'
85 changes: 73 additions & 12 deletions src/package-json/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,40 @@ describe('isPostinstallConfigured', () => {
assert.equal(result.configured, true)
assert.equal(result.needsUpdate, false)
})

it('should detect socket patch apply (Socket CLI subcommand) as configured', () => {
const packageJson = {
name: 'test',
version: '1.0.0',
scripts: {
postinstall: 'socket patch apply',
},
}

const result = isPostinstallConfigured(packageJson)

assert.equal(
result.configured,
true,
'socket patch apply (CLI subcommand) should be recognized',
)
assert.equal(result.needsUpdate, false)
})

it('should detect socket patch apply with --silent flag as configured', () => {
const packageJson = {
name: 'test',
version: '1.0.0',
scripts: {
postinstall: 'socket patch apply --silent',
},
}

const result = isPostinstallConfigured(packageJson)

assert.equal(result.configured, true)
assert.equal(result.needsUpdate, false)
})
})

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

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

it('should prepend to existing script', () => {
const result = generateUpdatedPostinstall('echo "Hello"')
assert.equal(
result,
'npx @socketsecurity/socket-patch apply && echo "Hello"',
'socket patch apply --silent && echo "Hello"',
)
})

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

it('should prepend to script with socket apply (main CLI)', () => {
it('should preserve socket patch apply (CLI subcommand)', () => {
const existing = 'socket patch apply'
const result = generateUpdatedPostinstall(existing)
assert.equal(result, existing)
})

it('should preserve socket patch apply --silent', () => {
const existing = 'socket patch apply --silent'
const result = generateUpdatedPostinstall(existing)
assert.equal(result, existing)
})

it('should prepend to script with socket apply (non-patch command)', () => {
const existing = 'socket apply'
const result = generateUpdatedPostinstall(existing)
assert.equal(
result,
'npx @socketsecurity/socket-patch apply && socket apply',
'Should add socket-patch even if socket apply is present',
'socket patch apply --silent && socket apply',
'Should add socket patch apply even if socket apply is present',
)
})
})
Expand All @@ -430,7 +476,7 @@ describe('updatePackageJsonContent', () => {
assert.ok(updated.scripts)
assert.equal(
updated.scripts.postinstall,
'npx @socketsecurity/socket-patch apply',
'socket patch apply --silent',
)
})

Expand All @@ -450,7 +496,7 @@ describe('updatePackageJsonContent', () => {
const updated = JSON.parse(result.content)
assert.equal(
updated.scripts.postinstall,
'npx @socketsecurity/socket-patch apply',
'socket patch apply --silent',
)
assert.equal(updated.scripts.test, 'jest', 'Should preserve other scripts')
assert.equal(updated.scripts.build, 'tsc', 'Should preserve other scripts')
Expand All @@ -471,11 +517,11 @@ describe('updatePackageJsonContent', () => {
assert.equal(result.oldScript, 'echo "Setup complete"')
assert.equal(
result.newScript,
'npx @socketsecurity/socket-patch apply && echo "Setup complete"',
'socket patch apply --silent && echo "Setup complete"',
)
})

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

it('should not modify when already configured with socket patch apply', () => {
const content = JSON.stringify({
name: 'test',
version: '1.0.0',
scripts: {
postinstall: 'socket patch apply --silent',
},
})

const result = updatePackageJsonContent(content)

assert.equal(result.modified, false)
assert.equal(result.content, content)
})

it('should throw error for invalid JSON', () => {
const content = '{ invalid json }'

Expand All @@ -514,7 +575,7 @@ describe('updatePackageJsonContent', () => {
const updated = JSON.parse(result.content)
assert.equal(
updated.scripts.postinstall,
'npx @socketsecurity/socket-patch apply',
'socket patch apply --silent',
)
})

Expand All @@ -533,7 +594,7 @@ describe('updatePackageJsonContent', () => {
const updated = JSON.parse(result.content)
assert.equal(
updated.scripts.postinstall,
'npx @socketsecurity/socket-patch apply',
'socket patch apply --silent',
)
})

Expand Down
Loading