Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions .git2gus/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"productTag": "a1aB00000004Bx8IAE",
Comment thread
joroscoSF marked this conversation as resolved.
Outdated
"defaultBuild": "offcore.tooling.56",
"issueTypeLabels": {
"feature": "USER STORY",
"regression": "BUG P1",
"bug": "BUG P3"
}
}
24 changes: 6 additions & 18 deletions src/utils/datacodeBinaryChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { SfError } from '@salesforce/core';
import { Messages } from '@salesforce/core';

const execAsync = promisify(exec);
import { spawnAsync } from './spawnHelper.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-data-code-extension', 'datacodeBinaryChecker');
Expand Down Expand Up @@ -74,9 +71,8 @@ export class DatacodeBinaryChecker {
*/
private static async isCommandAvailable(command: string): Promise<boolean> {
try {
// Use 'which' on Unix-like systems, 'where' on Windows
const checkCommand = process.platform === 'win32' ? 'where' : 'which';
await execAsync(`${checkCommand} ${command}`);
await spawnAsync(checkCommand, [command]);
return true;
} catch {
return false;
Expand All @@ -91,25 +87,19 @@ export class DatacodeBinaryChecker {
*/
private static async getBinaryVersion(command: string): Promise<DatacodeBinaryInfo | null> {
try {
const { stdout } = await execAsync(`${command} version`);
const { stdout } = await spawnAsync(command, ['version']);

// Parse the version output
// Expected format might be something like "datacustomcode version 1.2.3" or just "1.2.3"
// We'll handle multiple possible formats
const versionMatch = stdout.match(/(\d+\.\d+(?:\.\d+)?(?:[-\w.]*)?)/);

if (versionMatch) {
const version = versionMatch[1];

// Try to get the binary path (optional)
let path: string | undefined;
try {
// On Unix-like systems use 'which', on Windows use 'where'
const pathCommand = process.platform === 'win32' ? 'where' : 'which';
const { stdout: pathOutput } = await execAsync(`${pathCommand} ${command}`);
path = pathOutput.trim().split('\n')[0]; // Get first path if multiple
const { stdout: pathOutput } = await spawnAsync(pathCommand, [command]);
path = pathOutput.trim().split('\n')[0];
} catch {
// Path lookup is optional, don't fail if it doesn't work
path = undefined;
}

Expand All @@ -120,14 +110,12 @@ export class DatacodeBinaryChecker {
};
}

// If we can't parse the version but the command executed, still return basic info
return {
command,
version: 'unknown',
path: undefined,
};
} catch (error) {
// Command not found or failed to execute
} catch {
return null;
}
}
Expand Down
75 changes: 31 additions & 44 deletions src/utils/datacodeBinaryExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { exec, spawn, type ExecException } from 'node:child_process';
import { promisify } from 'node:util';
import { spawn } from 'node:child_process';
import { SfError } from '@salesforce/core';
import { Messages } from '@salesforce/core';
import { type PythonVersionInfo } from './pythonChecker.js';
import { type PipPackageInfo } from './pipChecker.js';
import { type DatacodeBinaryInfo } from './datacodeBinaryChecker.js';

const execAsync = promisify(exec);
import { spawnAsync, type SpawnError } from './spawnHelper.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-data-code-extension', 'datacodeBinaryExecutor');
Expand Down Expand Up @@ -89,11 +87,9 @@ export class DatacodeBinaryExecutor {
codeType: 'script' | 'function',
packageDir: string
): Promise<DatacodeInitExecutionResult> {
const command = `datacustomcode init --code-type ${codeType} ${packageDir}`;

try {
const { stdout, stderr } = await execAsync(command, {
timeout: 30_000, // 30 second timeout
const { stdout, stderr } = await spawnAsync('datacustomcode', ['init', '--code-type', codeType, packageDir], {
timeout: 30_000,
});

// Parse created files from output if available
Expand All @@ -111,8 +107,8 @@ export class DatacodeBinaryExecutor {
projectPath: packageDir,
};
} catch (error) {
const execError = error as ExecException & { stderr?: string };
const binaryOutput = execError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
const spawnError = error as SpawnError;
const binaryOutput = spawnError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
throw new SfError(
messages.getMessage('error.initExecutionFailed', [packageDir, binaryOutput]),
'InitExecutionFailed',
Expand All @@ -138,30 +134,26 @@ export class DatacodeBinaryExecutor {
noRequirements: boolean = false,
configFile?: string
): Promise<DatacodeScanExecutionResult> {
// Build the command with optional flags
let command = 'datacustomcode scan';
const args = ['scan'];

// Add boolean flags FIRST (before positional argument)
if (dryRun) {
command += ' --dry-run';
args.push('--dry-run');
}

if (noRequirements) {
command += ' --no-requirements';
args.push('--no-requirements');
}

if (configFile) {
command += ` --config "${configFile}"`;
args.push('--config', configFile);
}

// Add entrypoint as positional argument LAST (with proper quoting for paths with spaces)
const configPath = config ?? 'payload/config.json';
command += ` "${configPath}"`;
args.push(config ?? 'payload/config.json');

try {
const { stdout, stderr } = await execAsync(command, {
const { stdout, stderr } = await spawnAsync('datacustomcode', args, {
cwd: workingDir,
timeout: 60_000, // 60 second timeout (longer than init's 30 seconds)
timeout: 60_000,
});

// Parse scan results from output
Expand Down Expand Up @@ -197,8 +189,8 @@ export class DatacodeBinaryExecutor {
filesScanned: filesScanned.length > 0 ? filesScanned : undefined,
};
} catch (error) {
const execError = error as ExecException & { stderr?: string };
const binaryOutput = execError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
const spawnError = error as SpawnError;
const binaryOutput = spawnError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
throw new SfError(
messages.getMessage('error.scanExecutionFailed', [workingDir, binaryOutput]),
'ScanExecutionFailed',
Expand All @@ -216,20 +208,17 @@ export class DatacodeBinaryExecutor {
* @throws SfError if execution fails
*/
public static async executeBinaryZip(packageDir: string, network?: string): Promise<DatacodeZipExecutionResult> {
// Build the command with optional network flag
let command = 'datacustomcode zip';
const args = ['zip'];

// Add network flag if provided (before positional argument)
if (network) {
command += ` --network "${network}"`;
args.push('--network', network);
}

// Add package directory as positional argument (with proper quoting for paths with spaces)
command += ` "${packageDir}"`;
args.push(packageDir);

try {
const { stdout, stderr } = await execAsync(command, {
timeout: 120_000, // 120 second timeout (zipping can take time for large packages)
const { stdout, stderr } = await spawnAsync('datacustomcode', args, {
timeout: 120_000,
});

// Parse archive path from output
Expand Down Expand Up @@ -264,8 +253,8 @@ export class DatacodeBinaryExecutor {
archiveSize,
};
} catch (error) {
const execError = error as ExecException & { stderr?: string };
const binaryOutput = execError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
const spawnError = error as SpawnError;
const binaryOutput = spawnError.stderr?.trim() ?? (error instanceof Error ? error.message : String(error));
throw new SfError(
messages.getMessage('error.zipExecutionFailed', [packageDir, binaryOutput]),
'ZipExecutionFailed',
Expand Down Expand Up @@ -432,23 +421,21 @@ export class DatacodeBinaryExecutor {
configFile?: string,
dependencies?: string
): Promise<DatacodeRunExecutionResult> {
// Build the command — flags before the positional argument
let command = 'datacustomcode run';
command += ` --sf-cli-org "${targetOrg}"`;
const args = ['run', '--sf-cli-org', targetOrg];

if (configFile) {
command += ` --config-file "${configFile}"`;
args.push('--config-file', configFile);
}

if (dependencies) {
command += ` --dependencies "${dependencies}"`;
args.push('--dependencies', dependencies);
}

command += ` "${packageDir}"`;
args.push(packageDir);

try {
const { stdout, stderr } = await execAsync(command, {
timeout: 300_000, // 5 minute timeout
const { stdout, stderr } = await spawnAsync('datacustomcode', args, {
timeout: 300_000,
});

// Parse status from output
Expand All @@ -474,8 +461,8 @@ export class DatacodeBinaryExecutor {
output,
};
} catch (error) {
const execError = error as ExecException & { stderr?: string };
const errorMessage = execError.message ?? String(error);
const spawnError = error as SpawnError;
const errorMessage = spawnError.message ?? String(error);

if (errorMessage.includes('Authentication failed') || errorMessage.includes('Invalid credentials')) {
throw new SfError(
Expand All @@ -488,7 +475,7 @@ export class DatacodeBinaryExecutor {
// Surface the binary's stderr directly so any runtime error is shown as-is.
// File-existence checks for entrypoint and config-file are already handled by
// the CLI flag layer (exists: true), so those patterns are not matched here.
const binaryOutput = execError.stderr?.trim() ?? errorMessage;
const binaryOutput = spawnError.stderr?.trim() ?? errorMessage;
throw new SfError(
messages.getMessage('error.runExecutionFailed', [binaryOutput]),
'RunExecutionFailed',
Expand Down
51 changes: 24 additions & 27 deletions src/utils/pipChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { SfError } from '@salesforce/core';
import { Messages } from '@salesforce/core';

const execAsync = promisify(exec);
import { spawnAsync } from './spawnHelper.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-data-code-extension', 'pipChecker');
Expand All @@ -30,7 +27,16 @@ export type PipPackageInfo = {
pipCommand: string;
};

type PipCommand = { cmd: string; args: string[] };

export class PipChecker {
private static readonly PIP_COMMANDS: PipCommand[] = [
{ cmd: 'pip3', args: [] },
{ cmd: 'pip', args: [] },
{ cmd: 'python3', args: ['-m', 'pip'] },
{ cmd: 'python', args: ['-m', 'pip'] },
];

/**
* Checks if a specific pip package is installed on the system.
*
Expand All @@ -39,36 +45,29 @@ export class PipChecker {
* @throws SfError if pip is not found or package is not installed
*/
public static async checkPackage(packageName: string): Promise<PipPackageInfo> {
// Try different pip commands in order of preference
const pipCommands = ['pip3', 'pip', 'python3 -m pip', 'python -m pip'];

for (const command of pipCommands) {
for (const pipCommand of this.PIP_COMMANDS) {
try {
// eslint-disable-next-line no-await-in-loop
const packageInfo = await this.getPackageInfo(command, packageName);
const packageInfo = await this.getPackageInfo(pipCommand, packageName);

if (packageInfo) {
return packageInfo;
}
} catch (error) {
// Continue to try the next command
} catch {
continue;
}
}

// Check if pip is available at all
const pipAvailable = await this.isPipAvailable(pipCommands);
const pipAvailable = await this.isPipAvailable(this.PIP_COMMANDS);

if (!pipAvailable) {
// Pip not found with any command
throw new SfError(
messages.getMessage('error.pipNotFound'),
'PipNotFound',
messages.getMessages('actions.pipNotFound')
);
}

// Pip is available but package is not installed
throw new SfError(
messages.getMessage('error.packageNotInstalled', [packageName]),
'PackageNotInstalled',
Expand All @@ -79,15 +78,14 @@ export class PipChecker {
/**
* Gets the package information for a specific pip command and package name.
*
* @param pipCommand The pip command to use
* @param pipCommand The pip command descriptor to use
* @param packageName The name of the package to check
* @returns PipPackageInfo if package is found, null otherwise
*/
private static async getPackageInfo(pipCommand: string, packageName: string): Promise<PipPackageInfo | null> {
private static async getPackageInfo(pipCommand: PipCommand, packageName: string): Promise<PipPackageInfo | null> {
try {
const { stdout } = await execAsync(`${pipCommand} show ${packageName}`);
const { stdout } = await spawnAsync(pipCommand.cmd, [...pipCommand.args, 'show', packageName]);

// Parse the output to extract package information
const nameMatch = stdout.match(/Name:\s+(.+)/);
const versionMatch = stdout.match(/Version:\s+(.+)/);
const locationMatch = stdout.match(/Location:\s+(.+)/);
Expand All @@ -97,30 +95,29 @@ export class PipChecker {
name: nameMatch[1].trim(),
version: versionMatch[1].trim(),
location: locationMatch[1].trim(),
pipCommand: pipCommand.split(' ')[0], // Extract the base command (pip3, pip, python3, python)
pipCommand: pipCommand.cmd,
};
}

return null;
} catch (error) {
// Package not found or pip command failed
} catch {
return null;
}
}

/**
* Checks if pip is available with any of the given commands.
*
* @param pipCommands List of pip commands to try
* @param pipCommands List of pip command descriptors to try
* @returns true if pip is available, false otherwise
*/
private static async isPipAvailable(pipCommands: string[]): Promise<boolean> {
for (const command of pipCommands) {
private static async isPipAvailable(pipCommands: PipCommand[]): Promise<boolean> {
for (const { cmd, args } of pipCommands) {
try {
// eslint-disable-next-line no-await-in-loop
await execAsync(`${command} --version`);
await spawnAsync(cmd, [...args, '--version']);
return true;
} catch (error) {
} catch {
continue;
}
}
Expand Down
Loading
Loading