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
17 changes: 9 additions & 8 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ export async function handleUnpack(
`Extracting JS from native binary: ${chalk.cyan(installation.path)} (v${installation.version})`
);

const content = await readContent(installation);
const { content } = await readContent(installation);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await fs.writeFile(outputJsPath, content, 'utf8');

Expand Down Expand Up @@ -447,8 +447,9 @@ export async function handleRepack(
);

const newJs = await fs.readFile(inputJsPath, 'utf8');
const clearBytecode = !newJs.startsWith('// @bun @bytecode');

await writeContent(installation, newJs);
await writeContent(installation, newJs, clearBytecode);

console.log(
chalk.green(
Expand All @@ -471,7 +472,7 @@ async function handleAdhocPatchString(
installation: Installation,
skipConfirmation = false
): Promise<void> {
const content = await readContent(installation);
const { content, clearBytecode } = await readContent(installation);

let modified: string;
let count: number;
Expand Down Expand Up @@ -531,7 +532,7 @@ async function handleAdhocPatchString(
return;
}

await writeContent(installation, modified);
await writeContent(installation, modified, clearBytecode);

console.log(
chalk.green(
Expand Down Expand Up @@ -597,7 +598,7 @@ async function handleAdhocPatchRegex(
installation: Installation,
skipConfirmation = false
): Promise<void> {
const content = await readContent(installation);
const { content, clearBytecode } = await readContent(installation);

let parsed: { pattern: string; flags: string };
try {
Expand Down Expand Up @@ -671,7 +672,7 @@ async function handleAdhocPatchRegex(
return;
}

await writeContent(installation, modified);
await writeContent(installation, modified, clearBytecode);

console.log(
chalk.green(
Expand All @@ -689,7 +690,7 @@ async function handleAdhocPatchScriptImpl(
skipConfirmation = false,
dangerousNoScriptSandbox = false
): Promise<void> {
const content = await readContent(installation);
const { content, clearBytecode } = await readContent(installation);

const script = await resolveScriptSource(scriptArg);

Expand Down Expand Up @@ -742,7 +743,7 @@ async function handleAdhocPatchScriptImpl(
return;
}

await writeContent(installation, modified);
await writeContent(installation, modified, clearBytecode);

console.log(
chalk.green(`✓ Script patch applied to ${chalk.cyan(installation.path)}`)
Expand Down
2 changes: 1 addition & 1 deletion src/installationDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ async function extractVersionFromJsFile(cliPath: string): Promise<string> {
async function extractVersionFromNativeBinary(
binaryPath: string
): Promise<string> {
const claudeJsBuffer =
const { data: claudeJsBuffer } =
await extractClaudeJsFromNativeInstallation(binaryPath);

if (!claudeJsBuffer) {
Expand Down
20 changes: 12 additions & 8 deletions src/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ import { Installation } from './types';
* @param installation - The installation to read from
* @returns The JavaScript content as a string
*/
export async function readContent(installation: Installation): Promise<string> {
export async function readContent(
installation: Installation
): Promise<{ content: string; clearBytecode: boolean }> {
if (installation.kind === 'native') {
const buffer = await extractClaudeJsFromNativeInstallation(
installation.path
);
const { data: buffer, clearBytecode } =
await extractClaudeJsFromNativeInstallation(installation.path);
if (!buffer) {
throw new Error(
`Failed to extract JavaScript from native installation: ${installation.path}`
);
}
return buffer.toString('utf8');
return { content: buffer.toString('utf8'), clearBytecode };
} else {
return fs.readFile(installation.path, { encoding: 'utf8' });
const content = await fs.readFile(installation.path, { encoding: 'utf8' });
return { content, clearBytecode: false };
}
}

Expand All @@ -54,14 +56,16 @@ export async function readContent(installation: Installation): Promise<string> {
*/
export async function writeContent(
installation: Installation,
content: string
content: string,
clearBytecode: boolean
): Promise<void> {
if (installation.kind === 'native') {
const modifiedBuffer = Buffer.from(content, 'utf8');
await repackNativeInstallation(
installation.path,
modifiedBuffer,
installation.path
installation.path,
clearBytecode
);
} else {
await replaceFileBreakingHardLinks(installation.path, content, 'patch');
Expand Down
6 changes: 3 additions & 3 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
* await backupFile(installation.path, './backup');
*
* // Read, patch, write
* let content = await readContent(installation);
* content = content.replace(/something/g, 'something else');
* await writeContent(installation, content);
* const { content, clearBytecode } = await readContent(installation);
* const modified = content.replace(/something/g, 'something else');
* await writeContent(installation, modified, clearBytecode);
* ```
*/

Expand Down
108 changes: 94 additions & 14 deletions src/nativeInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
*/

import fs from 'node:fs';
import { execSync } from 'node:child_process';
import path from 'node:path';
import os from 'node:os';
import { execSync, execFileSync } from 'node:child_process';
import LIEF from 'node-lief';
import { isDebug, debug } from './utils';

Expand Down Expand Up @@ -151,6 +153,7 @@ export function resolveNixBinaryWrapper(binaryPath: string): string | null {
* - flags: u32
*/
const BUN_TRAILER = Buffer.from('\n---- Bun! ----\n');
const BUN_BYTECODE_PREFIX = '// @bun @bytecode';

// Size constants for binary structures
const SIZEOF_OFFSETS = 32;
Expand Down Expand Up @@ -701,9 +704,59 @@ function getBunData(
* real binary path here. This is handled at detection time in
* `installationDetection.ts`.
*/
function fetchNpmSource(version: string): Buffer | null {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tweakcc-npm-'));
try {
debug(`fetchNpmSource: Downloading @anthropic-ai/claude-code@${version}`);
execFileSync(
'npm',
[
'pack',
`@anthropic-ai/claude-code@${version}`,
'--pack-destination',
tmpDir,
],
{ stdio: 'pipe', timeout: 30_000, cwd: tmpDir }
);

const files = fs.readdirSync(tmpDir);
const tgz = files.find(f => f.endsWith('.tgz'));
if (!tgz) {
debug('fetchNpmSource: No .tgz file found after npm pack');
return null;
}

execFileSync('tar', ['xzf', path.join(tmpDir, tgz), 'package/cli.js'], {
stdio: 'pipe',
timeout: 30_000,
cwd: tmpDir,
});

const cliJsPath = path.join(tmpDir, 'package', 'cli.js');
if (!fs.existsSync(cliJsPath)) {
debug('fetchNpmSource: cli.js not found in extracted package');
return null;
}

const content = fs.readFileSync(cliJsPath);
debug(`fetchNpmSource: Got cli.js, ${content.length} bytes`);
return content;
} catch (error) {
debug('fetchNpmSource: Failed to fetch npm source:', error);
return null;
} finally {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
}

export function extractClaudeJsFromNativeInstallation(
nativeInstallationPath: string
): Buffer | null {
nativeInstallationPath: string,
version?: string
): { data: Buffer | null; clearBytecode: boolean } {
try {
LIEF.logging.disable();
const binary = LIEF.parse(nativeInstallationPath);
Expand All @@ -722,9 +775,6 @@ export function extractClaudeJsFromNativeInstallation(
`extractClaudeJsFromNativeInstallation: Module ${index}: ${moduleName}`
);

// Module name is typically:
// - Unix/macOS: /$bunfs/root/claude
// - Windows: B:/~BUN/root/claude.exe
if (!isClaudeModule(moduleName)) return undefined;

const moduleContents = getStringPointerContent(
Expand All @@ -741,29 +791,54 @@ export function extractClaudeJsFromNativeInstallation(
);

if (result) {
return result;
const head = result.subarray(0, 30).toString('utf8');
if (head.startsWith(BUN_BYTECODE_PREFIX)) {
debug(
'extractClaudeJsFromNativeInstallation: Extracted content is Bun bytecode — falling back to npm source'
);

if (version) {
const npmSource = fetchNpmSource(version);
if (npmSource) {
debug(
`extractClaudeJsFromNativeInstallation: Using npm source (${npmSource.length} bytes) instead of bytecode`
);
return { data: npmSource, clearBytecode: true };
}
debug(
'extractClaudeJsFromNativeInstallation: npm source fetch failed, returning bytecode content as-is'
);
} else {
debug(
'extractClaudeJsFromNativeInstallation: No version provided, cannot fetch npm source'
);
}
}

return { data: result, clearBytecode: false };
}

debug(
'extractClaudeJsFromNativeInstallation: claude module not found in any module'
);

return null;
return { data: null, clearBytecode: false };
} catch (error) {
debug(
'extractClaudeJsFromNativeInstallation: Error during extraction:',
error
);

return null;
return { data: null, clearBytecode: false };
}
}

function rebuildBunData(
bunData: Buffer,
bunOffsets: BunOffsets,
modifiedClaudeJs: Buffer | null,
moduleStructSize: number
moduleStructSize: number,
clearBytecode: boolean
): Buffer {
// Phase 1: Collect all string data
const stringsData: Buffer[] = [];
Expand All @@ -786,14 +861,18 @@ function rebuildBunData(

// Check if this is claude.js and we have modified contents
let contentsBytes: Buffer;
let bytecodeBytes: Buffer;
if (modifiedClaudeJs && isClaudeModule(moduleName)) {
contentsBytes = modifiedClaudeJs;
bytecodeBytes = clearBytecode
? Buffer.alloc(0)
: getStringPointerContent(bunData, module.bytecode);
} else {
contentsBytes = getStringPointerContent(bunData, module.contents);
bytecodeBytes = getStringPointerContent(bunData, module.bytecode);
}

const sourcemapBytes = getStringPointerContent(bunData, module.sourcemap);
const bytecodeBytes = getStringPointerContent(bunData, module.bytecode);
const moduleInfoBytes = getStringPointerContent(bunData, module.moduleInfo);
const bytecodeOriginPathBytes = getStringPointerContent(
bunData,
Expand Down Expand Up @@ -1392,19 +1471,20 @@ function repackELFOverlay(
export function repackNativeInstallation(
binPath: string,
modifiedClaudeJs: Buffer,
outputPath: string
outputPath: string,
clearBytecode: boolean
): void {
LIEF.logging.disable();
const binary = LIEF.parse(binPath);

// Extract Bun data and rebuild with modified claude.js
const { bunOffsets, bunData, sectionHeaderSize, moduleStructSize } =
getBunData(binary);
const newBuffer = rebuildBunData(
bunData,
bunOffsets,
modifiedClaudeJs,
moduleStructSize
moduleStructSize,
clearBytecode
);

switch (binary.format) {
Expand Down
23 changes: 16 additions & 7 deletions src/nativeInstallationLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,17 @@ async function tryLoadNativeInstallationModule(): Promise<NativeInstallationModu
* Returns null if node-lief is not available or extraction fails.
*/
export async function extractClaudeJsFromNativeInstallation(
nativeInstallationPath: string
): Promise<Buffer | null> {
nativeInstallationPath: string,
version?: string
): Promise<{ data: Buffer | null; clearBytecode: boolean }> {
const mod = await tryLoadNativeInstallationModule();
if (!mod) {
return null;
return { data: null, clearBytecode: false };
}
return mod.extractClaudeJsFromNativeInstallation(nativeInstallationPath);
return mod.extractClaudeJsFromNativeInstallation(
nativeInstallationPath,
version
);
}

/**
Expand All @@ -71,17 +75,22 @@ export async function extractClaudeJsFromNativeInstallation(
export async function repackNativeInstallation(
binPath: string,
modifiedClaudeJs: Buffer,
outputPath: string
outputPath: string,
clearBytecode: boolean
): Promise<void> {
// The module should already be cached from a prior extractClaudeJsFromNativeInstallation() call
const mod = await tryLoadNativeInstallationModule();
if (!mod) {
throw new Error(
'`repackNativeInstallation()` called but `node-lief` is not available. ' +
'This is unexpected - `extractClaudeJsFromNativeInstallation()` should have been called first.'
);
}
mod.repackNativeInstallation(binPath, modifiedClaudeJs, outputPath);
mod.repackNativeInstallation(
binPath,
modifiedClaudeJs,
outputPath,
clearBytecode
);
}

/**
Expand Down
Loading