Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .git2gus/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"productTag": "a1aEE000000pCaDYAU",
"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