#!/usr/bin/env node
/**
* @fileoverview Utility script to make files executable (chmod +x) on Unix-like systems.
* @module scripts/make-executable
* On Windows, this script does nothing but exits successfully.
* Useful for CLI applications where built output needs executable permissions.
* Default target (if no args): dist/index.js.
* Ensures output paths are within the project directory for security.
*
* @example
* // Add to package.json build script:
* // "build": "tsc && ts-node --esm scripts/make-executable.ts dist/index.js"
*
* @example
* // Run directly with custom files:
* // ts-node --esm scripts/make-executable.ts path/to/script1 path/to/script2
*/
import fs from "fs/promises";
import os from "os";
import path from "path";
const isUnix = os.platform() !== "win32";
const projectRoot = process.cwd();
const EXECUTABLE_MODE = 0o755; // rwxr-xr-x
/**
* Represents the result of an attempt to make a file executable.
* @property file - The relative path of the file targeted.
* @property status - The outcome of the operation ('success', 'error', or 'skipped').
* @property reason - If status is 'error' or 'skipped', an explanation.
*/
interface ExecutableResult {
file: string;
status: "success" | "error" | "skipped";
reason?: string;
}
/**
* Main function to make specified files executable.
* Skips operation on Windows. Processes command-line arguments for target files
* or defaults to 'dist/index.js'. Reports status for each file.
*/
const makeExecutable = async (): Promise<void> => {
try {
const targetFiles: string[] =
process.argv.slice(2).length > 0
? process.argv.slice(2)
: ["dist/index.js"];
if (!isUnix) {
console.log(
"Skipping chmod operation: Script is running on Windows (not applicable).",
);
return;
}
console.log(
`Attempting to make files executable: ${targetFiles.join(", ")}`,
);
const results = await Promise.allSettled(
targetFiles.map(async (targetFile): Promise<ExecutableResult> => {
const normalizedPath = path.resolve(projectRoot, targetFile);
if (
!normalizedPath.startsWith(projectRoot + path.sep) &&
normalizedPath !== projectRoot
) {
return {
file: targetFile,
status: "error",
reason: `Path resolves outside project boundary: ${normalizedPath}`,
};
}
try {
await fs.access(normalizedPath); // Check if file exists
await fs.chmod(normalizedPath, EXECUTABLE_MODE);
return { file: targetFile, status: "success" };
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ENOENT") {
return {
file: targetFile,
status: "error",
reason: "File not found",
};
}
console.error(
`Error setting executable permission for ${targetFile}: ${err.message}`,
);
return { file: targetFile, status: "error", reason: err.message };
}
}),
);
let hasErrors = false;
results.forEach((result) => {
if (result.status === "fulfilled") {
const { file, status, reason } = result.value;
if (status === "success") {
console.log(`Successfully made executable: ${file}`);
} else if (status === "error") {
console.error(`Error for ${file}: ${reason}`);
hasErrors = true;
} else if (status === "skipped") {
// This status is not currently generated by the mapAsync logic but kept for future flexibility
console.warn(`Skipped ${file}: ${reason}`);
}
} else {
console.error(
`Unexpected failure for one of the files: ${result.reason}`,
);
hasErrors = true;
}
});
if (hasErrors) {
console.error(
"One or more files could not be made executable. Please check the errors above.",
);
// process.exit(1); // Uncomment to exit with error if any file fails
} else {
console.log("All targeted files processed successfully.");
}
} catch (error) {
console.error(
"A fatal error occurred during the make-executable script:",
error instanceof Error ? error.message : error,
);
process.exit(1);
}
};
makeExecutable();