chore: fix npm package setup

This commit is contained in:
Yota Hamada 2025-07-29 20:27:29 +09:00
parent 5058ebf93f
commit fae48f2f85
9 changed files with 206 additions and 1085 deletions

25
npm/dagu/bin/cli Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env node
const path = require("path");
const childProcess = require("child_process");
const { getPlatformPackage } = require("../lib/platform");
// Windows binaries end with .exe so we need to special case them.
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
function getBinaryPath() {
// Determine package name for this platform
const platformSpecificPackageName = getPlatformPackage();
try {
// Resolving will fail if the optionalDependency was not installed
return require.resolve(`${platformSpecificPackageName}/bin/${binaryName}`);
} catch (e) {
return path.join(__dirname, "..", binaryName);
}
}
childProcess.execFileSync(getBinaryPath(), process.argv.slice(2), {
stdio: "inherit",
});

View File

@ -1,84 +1,20 @@
/**
* Dagu npm package - programmatic interface
*/
const path = require("path");
const childProcess = require("child_process");
const { getBinaryPath } = require('./lib/platform');
const { spawn } = require('child_process');
const path = require('path');
const { getPlatformPackage } = require("./lib/platform");
/**
* Get the path to the Dagu binary
* @returns {string|null} Path to the binary or null if not found
*/
function getDaguPath() {
return getBinaryPath();
}
/**
* Execute Dagu with given arguments
* @param {string[]} args Command line arguments
* @param {object} options Child process spawn options
* @returns {ChildProcess} The spawned child process
*/
function execute(args = [], options = {}) {
const binaryPath = getDaguPath();
if (!binaryPath) {
throw new Error('Dagu binary not found. Please ensure Dagu is properly installed.');
function getBinaryPath() {
try {
const platformSpecificPackageName = getPlatformPackage();
// Resolving will fail if the optionalDependency was not installed
return require.resolve(`${platformSpecificPackageName}/bin/${binaryName}`);
} catch (e) {
return path.join(__dirname, "..", binaryName);
}
return spawn(binaryPath, args, {
stdio: 'inherit',
...options
});
}
/**
* Execute Dagu and return a promise
* @param {string[]} args Command line arguments
* @param {object} options Child process spawn options
* @returns {Promise<{code: number, signal: string|null}>} Exit code and signal
*/
function executeAsync(args = [], options = {}) {
return new Promise((resolve, reject) => {
const child = execute(args, {
stdio: 'pipe',
...options
});
let stdout = '';
let stderr = '';
if (child.stdout) {
child.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (child.stderr) {
child.stderr.on('data', (data) => {
stderr += data.toString();
});
}
child.on('error', reject);
child.on('close', (code, signal) => {
resolve({
code,
signal,
stdout,
stderr
});
});
module.exports.runBinary = function (...args) {
childProcess.execFileSync(getBinaryPath(), args, {
stdio: "inherit",
});
}
module.exports = {
getDaguPath,
execute,
executeAsync,
// Re-export useful functions
getBinaryPath,
getPlatformInfo: require('./lib/platform').getPlatformInfo
};
};

View File

@ -1,135 +1,109 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { getBinaryPath, getPlatformPackage, setPlatformBinary, getPlatformInfo } = require('./lib/platform');
const { downloadBinary, downloadBinaryFromNpm } = require('./lib/download');
const { validateBinary } = require('./lib/validate');
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const https = require("https");
async function install() {
console.log('Installing Dagu...');
try {
// Check if running in CI or with --ignore-scripts
if (process.env.npm_config_ignore_scripts === 'true') {
console.log('Skipping postinstall script (--ignore-scripts flag detected)');
return;
}
// Try to resolve platform-specific package
const platformPackage = getPlatformPackage();
if (!platformPackage) {
console.error(`
Error: Unsupported platform: ${process.platform}-${process.arch}
const { getPlatformPackage } = require("./lib/platform");
Dagu does not provide pre-built binaries for this platform.
Please build from source: https://github.com/dagu-org/dagu#building-from-source
`);
process.exit(1);
}
console.log(`Detected platform: ${process.platform}-${process.arch}`);
console.log(`Looking for package: ${platformPackage}`);
// Check for cross-platform scenario
const { checkCrossPlatformScenario } = require('./lib/platform');
const crossPlatformWarning = checkCrossPlatformScenario();
if (crossPlatformWarning) {
console.warn(`\n${crossPlatformWarning.message}\n`);
}
// Check if binary already exists from optionalDependency
const existingBinary = getBinaryPath();
if (existingBinary && fs.existsSync(existingBinary)) {
console.log('Using pre-installed binary from optional dependency');
// Validate the binary
if (await validateBinary(existingBinary)) {
console.log('✓ Dagu installation complete!');
return;
} else {
console.warn('Binary validation failed, attempting to download...');
}
}
} catch (e) {
console.log('Optional dependency not found, downloading binary...');
}
// Fallback: Download binary
try {
const binaryPath = path.join(__dirname, 'bin', process.platform === 'win32' ? 'dagu.exe' : 'dagu');
// Create bin directory if it doesn't exist
const binDir = path.dirname(binaryPath);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// Skip download in development if flag file exists
if (fs.existsSync(path.join(__dirname, '.skip-install'))) {
console.log('Development mode: skipping binary download (.skip-install file found)');
return;
}
// Download the binary
await downloadBinary(binaryPath, { method: 'auto' });
// Validate the downloaded binary
if (await validateBinary(binaryPath)) {
setPlatformBinary(binaryPath);
console.log('✓ Dagu installation complete!');
// Print warning about optionalDependencies if none were found
if (!hasAnyOptionalDependency()) {
console.warn(`
WARNING: optionalDependencies may be disabled in your environment.
For better performance and reliability, consider enabling them.
See: https://docs.npmjs.com/cli/v8/using-npm/config#optional
`);
}
} else {
throw new Error('Downloaded binary validation failed');
}
} catch (error) {
console.error('Failed to install Dagu:', error.message);
console.error(`
Platform details:
${JSON.stringify(getPlatformInfo(), null, 2)}
// Windows binaries end with .exe so we need to special case them.
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
Please try one of the following:
1. Install manually from: https://github.com/dagu-org/dagu/releases
2. Build from source: https://github.com/dagu-org/dagu#building-from-source
3. Report this issue: https://github.com/dagu-org/dagu/issues
`);
process.exit(1);
// Adjust the version you want to install. You can also make this dynamic.
const PACKAGE_VERSION = require("./package.json").version;
// Compute the path we want to emit the fallback binary to
const fallbackBinaryPath = path.join(__dirname, binaryName);
function makeRequest(url) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
const chunks = [];
response.on("data", (chunk) => chunks.push(chunk));
response.on("end", () => {
resolve(Buffer.concat(chunks));
});
} else if (
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
// Follow redirects
makeRequest(response.headers.location).then(resolve, reject);
} else {
reject(
new Error(
`npm responded with status code ${response.statusCode} when downloading the package!`
)
);
}
})
.on("error", (error) => {
reject(error);
});
});
}
function extractFileFromTarball(tarballBuffer, filepath) {
// Tar archives are organized in 512 byte blocks.
// Blocks can either be header blocks or data blocks.
// Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
// The size of a file is contained in bytes 124-135 of a header block and in octal format.
// The following blocks will be data blocks containing the file.
let offset = 0;
while (offset < tarballBuffer.length) {
const header = tarballBuffer.subarray(offset, offset + 512);
offset += 512;
const fileName = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
const fileSize = parseInt(
header.toString("utf-8", 124, 136).replace(/\0.*/g, ""),
8
);
if (fileName === filepath) {
return tarballBuffer.subarray(offset, offset + fileSize);
}
// Clamp offset to the uppoer multiple of 512
offset = (offset + fileSize + 511) & ~511;
}
}
// Check if any optional dependency is installed
function hasAnyOptionalDependency() {
const pkg = require('./package.json');
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
for (const dep of optionalDeps) {
try {
require.resolve(dep);
return true;
} catch (e) {
// Continue checking
}
}
return false;
async function downloadBinaryFromNpm() {
console.log({
getPlatformPackage,
});
// Determine package name for this platform
const platformSpecificPackageName = getPlatformPackage();
const url = `https://registry.npmjs.org/@dagu-org/${platformSpecificPackageName}/-/${platformSpecificPackageName}-${PACKAGE_VERSION}.tgz`;
console.log(`Downloading binary distribution package from ${url}...`);
// Download the tarball of the right binary distribution package
const tarballDownloadBuffer = await makeRequest(url);
const tarballBuffer = zlib.unzipSync(tarballDownloadBuffer);
console.log(fallbackBinaryPath);
// Extract binary from package and write to disk
fs.writeFileSync(
fallbackBinaryPath,
extractFileFromTarball(tarballBuffer, `package/bin/${binaryName}`),
{ mode: 0o755 } // Make binary file executable
);
}
// Handle errors gracefully
process.on('unhandledRejection', (error) => {
console.error('Installation error:', error);
process.exit(1);
});
// Run installation
install().catch((error) => {
console.error('Installation failed:', error);
process.exit(1);
});
// Skip downloading the binary if it was already installed via optionalDependencies
if (!isPlatformSpecificPackageInstalled()) {
console.log(
"Platform specific package not found. Will manually download binary."
);
downloadBinaryFromNpm();
} else {
console.log(
"Platform specific package already installed. Will fall back to manually downloading binary."
);
}

View File

@ -1,230 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const os = require('os');
/**
* Get cache directory for binaries
* @returns {string} Cache directory path
*/
function getCacheDir() {
// Use standard cache locations based on platform
const homeDir = os.homedir();
let cacheDir;
if (process.platform === 'win32') {
// Windows: %LOCALAPPDATA%\dagu-cache
cacheDir = path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 'dagu-cache');
} else if (process.platform === 'darwin') {
// macOS: ~/Library/Caches/dagu
cacheDir = path.join(homeDir, 'Library', 'Caches', 'dagu');
} else {
// Linux/BSD: ~/.cache/dagu
cacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(homeDir, '.cache'), 'dagu');
}
// Allow override via environment variable
if (process.env.DAGU_CACHE_DIR) {
cacheDir = process.env.DAGU_CACHE_DIR;
}
return cacheDir;
}
/**
* Get cached binary path
* @param {string} version Version of the binary
* @param {string} platform Platform identifier
* @returns {string} Path to cached binary
*/
function getCachedBinaryPath(version, platform) {
const cacheDir = getCacheDir();
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
return path.join(cacheDir, `${version}-${platform}`, binaryName);
}
/**
* Check if binary exists in cache
* @param {string} version Version of the binary
* @param {string} platform Platform identifier
* @returns {boolean} True if cached, false otherwise
*/
function isCached(version, platform) {
const cachedPath = getCachedBinaryPath(version, platform);
return fs.existsSync(cachedPath);
}
/**
* Save binary to cache
* @param {string} sourcePath Path to the binary to cache
* @param {string} version Version of the binary
* @param {string} platform Platform identifier
* @returns {string} Path to cached binary
*/
function cacheBinary(sourcePath, version, platform) {
const cachedPath = getCachedBinaryPath(version, platform);
const cacheDir = path.dirname(cachedPath);
// Create cache directory if it doesn't exist
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Copy binary to cache
fs.copyFileSync(sourcePath, cachedPath);
// Preserve executable permissions
if (process.platform !== 'win32') {
fs.chmodSync(cachedPath, 0o755);
}
// Create a metadata file with cache info
const metadataPath = path.join(cacheDir, 'metadata.json');
const metadata = {
version,
platform,
cachedAt: new Date().toISOString(),
checksum: calculateChecksum(cachedPath)
};
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
return cachedPath;
}
/**
* Get binary from cache
* @param {string} version Version of the binary
* @param {string} platform Platform identifier
* @returns {string|null} Path to cached binary or null if not found
*/
function getCachedBinary(version, platform) {
if (!isCached(version, platform)) {
return null;
}
const cachedPath = getCachedBinaryPath(version, platform);
// Verify the cached binary still works
try {
fs.accessSync(cachedPath, fs.constants.X_OK);
return cachedPath;
} catch (e) {
// Cached binary is corrupted or not executable
// Remove it from cache
cleanCacheEntry(version, platform);
return null;
}
}
/**
* Calculate checksum of a file
* @param {string} filePath Path to the file
* @returns {string} SHA256 checksum
*/
function calculateChecksum(filePath) {
const hash = crypto.createHash('sha256');
const data = fs.readFileSync(filePath);
hash.update(data);
return hash.digest('hex');
}
/**
* Clean specific cache entry
* @param {string} version Version of the binary
* @param {string} platform Platform identifier
*/
function cleanCacheEntry(version, platform) {
const cacheDir = path.dirname(getCachedBinaryPath(version, platform));
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true, force: true });
}
}
/**
* Clean old cache entries (older than specified days)
* @param {number} maxAgeDays Maximum age in days (default 30)
*/
function cleanOldCache(maxAgeDays = 30) {
const cacheDir = getCacheDir();
if (!fs.existsSync(cacheDir)) {
return;
}
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
const now = Date.now();
try {
const entries = fs.readdirSync(cacheDir);
for (const entry of entries) {
const entryPath = path.join(cacheDir, entry);
const metadataPath = path.join(entryPath, 'metadata.json');
if (fs.existsSync(metadataPath)) {
try {
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
const cachedAt = new Date(metadata.cachedAt).getTime();
if (now - cachedAt > maxAgeMs) {
fs.rmSync(entryPath, { recursive: true, force: true });
}
} catch (e) {
// Invalid metadata, remove entry
fs.rmSync(entryPath, { recursive: true, force: true });
}
}
}
} catch (e) {
// Ignore errors during cleanup
}
}
/**
* Get cache size
* @returns {number} Total size in bytes
*/
function getCacheSize() {
const cacheDir = getCacheDir();
if (!fs.existsSync(cacheDir)) {
return 0;
}
let totalSize = 0;
function calculateDirSize(dirPath) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
calculateDirSize(fullPath);
} else {
const stats = fs.statSync(fullPath);
totalSize += stats.size;
}
}
}
try {
calculateDirSize(cacheDir);
} catch (e) {
// Ignore errors
}
return totalSize;
}
module.exports = {
getCacheDir,
getCachedBinaryPath,
isCached,
cacheBinary,
getCachedBinary,
cleanCacheEntry,
cleanOldCache,
getCacheSize
};

View File

@ -1,60 +0,0 @@
/**
* Constants for Dagu npm distribution
*/
const GITHUB_ORG = 'dagu-org';
const GITHUB_REPO = 'dagu';
const NPM_ORG = '@dagu-org';
// Tier classification for platform support
const PLATFORM_TIERS = {
TIER_1: [
'linux-x64',
'linux-arm64',
'darwin-x64',
'darwin-arm64',
'win32-x64'
],
TIER_2: [
'linux-ia32',
'linux-armv7',
'win32-ia32',
'freebsd-x64'
],
TIER_3: [
'linux-armv6',
'linux-ppc64',
'linux-s390x',
'win32-arm64',
'freebsd-arm64',
'freebsd-ia32',
'freebsd-arm',
'openbsd-x64',
'openbsd-arm64'
]
};
// Error messages
const ERRORS = {
UNSUPPORTED_PLATFORM: 'Unsupported platform',
DOWNLOAD_FAILED: 'Failed to download binary',
VALIDATION_FAILED: 'Binary validation failed',
CHECKSUM_MISMATCH: 'Checksum verification failed',
EXTRACTION_FAILED: 'Failed to extract archive'
};
// URLs
const URLS = {
RELEASES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases`,
ISSUES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/issues`,
BUILD_DOCS: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}#building-from-source`
};
module.exports = {
GITHUB_ORG,
GITHUB_REPO,
NPM_ORG,
PLATFORM_TIERS,
ERRORS,
URLS
};

View File

@ -1,404 +0,0 @@
const https = require('https');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
const tar = require('tar');
const { getCachedBinary, cacheBinary, cleanOldCache } = require('./cache');
// Get package version
const PACKAGE_VERSION = require('../package.json').version;
const GITHUB_RELEASES_URL = 'https://github.com/dagu-org/dagu/releases/download';
/**
* Map Node.js platform/arch to goreleaser asset names
*/
function getAssetName(version) {
const platform = process.platform;
const arch = process.arch;
// Platform name mapping (matches goreleaser output - lowercase)
const osMap = {
'darwin': 'darwin',
'linux': 'linux',
'win32': 'windows',
'freebsd': 'freebsd',
'openbsd': 'openbsd'
};
// Architecture name mapping (matches goreleaser output)
const archMap = {
'x64': 'amd64',
'ia32': '386',
'arm64': 'arm64',
'ppc64': 'ppc64le',
's390x': 's390x'
};
let osName = osMap[platform] || platform;
let archName = archMap[arch] || arch;
// Special handling for ARM
if (arch === 'arm' && platform === 'linux') {
const { getArmVariant } = require('./platform');
const variant = getArmVariant();
archName = `armv${variant}`;
}
// All assets are .tar.gz now (goreleaser changed this)
const ext = '.tar.gz';
return `dagu_${version}_${osName}_${archName}${ext}`;
}
/**
* Make HTTP request and return buffer (Sentry-style)
*/
function makeRequest(url) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
resolve(Buffer.concat(chunks));
});
} else if (
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
// Follow redirects
makeRequest(response.headers.location).then(resolve, reject);
} else {
reject(
new Error(
`Server responded with status code ${response.statusCode} when downloading the package!`
)
);
}
})
.on('error', (error) => {
reject(error);
});
});
}
/**
* Download file with progress reporting
*/
async function downloadFile(url, destination, options = {}) {
const { onProgress, maxRetries = 3 } = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await downloadFileAttempt(url, destination, { onProgress, attempt });
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
console.log(`Download failed (attempt ${attempt}/${maxRetries}), retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
}
}
}
throw lastError;
}
/**
* Single download attempt
*/
function downloadFileAttempt(url, destination, options = {}) {
const { onProgress, attempt = 1 } = options;
return new Promise((resolve, reject) => {
const tempFile = `${destination}.download.${process.pid}.tmp`;
https.get(url, (response) => {
// Handle redirects
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
if (!redirectUrl) {
reject(new Error('Redirect location not provided'));
return;
}
downloadFileAttempt(redirectUrl, destination, options)
.then(resolve)
.catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
const fileStream = fs.createWriteStream(tempFile);
response.on('data', (chunk) => {
downloadedSize += chunk.length;
if (onProgress && totalSize) {
const percentage = Math.round((downloadedSize / totalSize) * 100);
onProgress(percentage, downloadedSize, totalSize);
}
});
response.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close(() => {
// Move temp file to final destination
fs.renameSync(tempFile, destination);
resolve();
});
});
fileStream.on('error', (err) => {
fs.unlinkSync(tempFile);
reject(err);
});
}).on('error', (err) => {
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
reject(err);
});
});
}
/**
* Extract archive based on file extension
*/
async function extractArchive(archivePath, outputDir) {
const ext = path.extname(archivePath).toLowerCase();
if (ext === '.gz' || archivePath.endsWith('.tar.gz')) {
// Extract tar.gz
await tar.extract({
file: archivePath,
cwd: outputDir,
filter: (path) => path === 'dagu' || path === 'dagu.exe'
});
} else if (ext === '.zip') {
// For Windows, we need a zip extractor
// Using built-in Windows extraction via PowerShell
const { execSync } = require('child_process');
const command = `powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`;
execSync(command);
} else {
throw new Error(`Unsupported archive format: ${ext}`);
}
}
/**
* Download and verify checksums
*/
async function downloadChecksums(version) {
const checksumsUrl = `${GITHUB_RELEASES_URL}/v${version}/checksums.txt`;
const tempFile = path.join(require('os').tmpdir(), `dagu-checksums-${process.pid}.txt`);
try {
await downloadFile(checksumsUrl, tempFile);
const content = fs.readFileSync(tempFile, 'utf8');
// Parse checksums file
const checksums = {};
content.split('\n').forEach(line => {
const match = line.match(/^([a-f0-9]{64})\s+(.+)$/);
if (match) {
checksums[match[2]] = match[1];
}
});
return checksums;
} finally {
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
}
/**
* Verify file checksum
*/
function verifyChecksum(filePath, expectedChecksum) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (data) => hash.update(data));
stream.on('end', () => {
const actualChecksum = hash.digest('hex');
if (actualChecksum === expectedChecksum) {
resolve(true);
} else {
reject(new Error(`Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`));
}
});
stream.on('error', reject);
});
}
/**
* Extract file from npm tarball (aligned with Sentry approach)
*/
function extractFileFromTarball(tarballBuffer, filepath) {
// Tar archives are organized in 512 byte blocks.
// Blocks can either be header blocks or data blocks.
// Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
// The size of a file is contained in bytes 124-135 of a header block and in octal format.
// The following blocks will be data blocks containing the file.
let offset = 0;
while (offset < tarballBuffer.length) {
const header = tarballBuffer.subarray(offset, offset + 512);
offset += 512;
const fileName = header.toString('utf-8', 0, 100).replace(/\0.*/g, '');
const fileSize = parseInt(header.toString('utf-8', 124, 136).replace(/\0.*/g, ''), 8);
if (fileName === filepath) {
return tarballBuffer.subarray(offset, offset + fileSize);
}
// Clamp offset to the upper multiple of 512
offset = (offset + fileSize + 511) & ~511;
}
throw new Error(`File ${filepath} not found in tarball`);
}
/**
* Download binary from npm registry (Sentry-style)
*/
async function downloadBinaryFromNpm(version) {
const { getPlatformPackage } = require('./platform');
const platformPackage = getPlatformPackage();
if (!platformPackage) {
throw new Error('Platform not supported!');
}
const packageName = platformPackage.replace('@dagu-org/', '');
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
console.log(`Downloading ${platformPackage} from npm registry...`);
// Download the tarball of the right binary distribution package
const tarballUrl = `https://registry.npmjs.org/${platformPackage}/-/${packageName}-${version}.tgz`;
const tarballDownloadBuffer = await makeRequest(tarballUrl);
const tarballBuffer = zlib.unzipSync(tarballDownloadBuffer);
// Extract binary from package
const binaryData = extractFileFromTarball(tarballBuffer, `package/bin/${binaryName}`);
return binaryData;
}
/**
* Main download function
*/
async function downloadBinary(destination, options = {}) {
const version = options.version || PACKAGE_VERSION;
const { method = 'auto', useCache = true } = options;
const platformKey = `${process.platform}-${process.arch}`;
console.log(`Installing Dagu v${version} for ${platformKey}...`);
// Check cache first
if (useCache) {
const cachedBinary = getCachedBinary(version, platformKey);
if (cachedBinary) {
console.log('✓ Using cached binary');
fs.copyFileSync(cachedBinary, destination);
if (process.platform !== 'win32') {
fs.chmodSync(destination, 0o755);
}
// Clean old cache entries
cleanOldCache();
return;
}
}
try {
let binaryData;
if (method === 'npm' || method === 'auto') {
// Try npm registry first (following Sentry's approach)
try {
binaryData = await downloadBinaryFromNpm(version);
console.log('✓ Downloaded from npm registry');
} catch (npmError) {
if (method === 'npm') {
throw npmError;
}
console.log('npm registry download failed, trying GitHub releases...');
}
}
if (!binaryData && (method === 'github' || method === 'auto')) {
// Fallback to GitHub releases
const assetName = getAssetName(version);
const downloadUrl = `${GITHUB_RELEASES_URL}/v${version}/${assetName}`;
const tempFile = path.join(require('os').tmpdir(), `dagu-${process.pid}-${Date.now()}.tmp`);
try {
await downloadFile(downloadUrl, tempFile, {
onProgress: (percentage, downloaded, total) => {
const mb = (size) => (size / 1024 / 1024).toFixed(2);
process.stdout.write(`\rProgress: ${percentage}% (${mb(downloaded)}MB / ${mb(total)}MB)`);
}
});
console.log('\n✓ Downloaded from GitHub releases');
// Extract from archive
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
// All files are .tar.gz now
const archiveData = fs.readFileSync(tempFile);
const tarData = zlib.gunzipSync(archiveData);
binaryData = extractFileFromTarball(tarData, binaryName);
} finally {
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
}
if (!binaryData) {
throw new Error('Failed to download binary from any source');
}
// Write binary to destination
const dir = path.dirname(destination);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(destination, binaryData, { mode: 0o755 });
console.log('✓ Binary installed successfully');
// Cache the binary for future use
if (useCache) {
try {
cacheBinary(destination, version, platformKey);
console.log('✓ Binary cached for future installations');
} catch (e) {
// Caching failed, but installation succeeded
}
}
} catch (error) {
throw new Error(`Failed to download binary: ${error.message}`);
}
}
module.exports = {
downloadBinary,
downloadBinaryFromNpm,
getAssetName
};

View File

@ -1,30 +1,29 @@
const os = require('os');
const path = require('path');
const fs = require('fs');
const path = require("path");
const fs = require("fs");
// Platform mapping from Node.js to npm package names
const PLATFORM_MAP = {
// Tier 1 - Most common platforms
'linux-x64': '@dagu-org/dagu-linux-x64',
'linux-arm64': '@dagu-org/dagu-linux-arm64',
'darwin-x64': '@dagu-org/dagu-darwin-x64',
'darwin-arm64': '@dagu-org/dagu-darwin-arm64',
'win32-x64': '@dagu-org/dagu-win32-x64',
"linux-x64": "dagu-linux-x64",
"linux-arm64": "dagu-linux-arm64",
"darwin-x64": "dagu-darwin-x64",
"darwin-arm64": "dagu-darwin-arm64",
"win32-x64": "dagu-win32-x64",
// Tier 2 - Common but less frequent
'linux-ia32': '@dagu-org/dagu-linux-ia32',
'win32-ia32': '@dagu-org/dagu-win32-ia32',
'freebsd-x64': '@dagu-org/dagu-freebsd-x64',
"linux-ia32": "dagu-linux-ia32",
"win32-ia32": "dagu-win32-ia32",
"freebsd-x64": "dagu-freebsd-x64",
// Tier 3 - Rare platforms
'win32-arm64': '@dagu-org/dagu-win32-arm64',
'linux-ppc64': '@dagu-org/dagu-linux-ppc64',
'linux-s390x': '@dagu-org/dagu-linux-s390x',
'freebsd-arm64': '@dagu-org/dagu-freebsd-arm64',
'freebsd-ia32': '@dagu-org/dagu-freebsd-ia32',
'freebsd-arm': '@dagu-org/dagu-freebsd-arm',
'openbsd-x64': '@dagu-org/dagu-openbsd-x64',
'openbsd-arm64': '@dagu-org/dagu-openbsd-arm64',
"win32-arm64": "dagu-win32-arm64",
"linux-ppc64": "dagu-linux-ppc64",
"linux-s390x": "dagu-linux-s390x",
"freebsd-arm64": "dagu-freebsd-arm64",
"freebsd-ia32": "dagu-freebsd-ia32",
"freebsd-arm": "dagu-freebsd-arm",
"openbsd-x64": "dagu-openbsd-x64",
"openbsd-arm64": "dagu-openbsd-arm64",
};
// Cache for binary path
@ -36,37 +35,45 @@ let cachedBinaryPath = null;
*/
function getArmVariant() {
// First try process.config
if (process.config && process.config.variables && process.config.variables.arm_version) {
if (
process.config &&
process.config.variables &&
process.config.variables.arm_version
) {
return String(process.config.variables.arm_version);
}
// On Linux, check /proc/cpuinfo
if (process.platform === 'linux') {
if (process.platform === "linux") {
try {
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8");
// Check for specific ARM architecture indicators
if (cpuinfo.includes('ARMv6') || cpuinfo.includes('ARM926') || cpuinfo.includes('ARM1176')) {
return '6';
if (
cpuinfo.includes("ARMv6") ||
cpuinfo.includes("ARM926") ||
cpuinfo.includes("ARM1176")
) {
return "6";
}
if (cpuinfo.includes('ARMv7') || cpuinfo.includes('Cortex-A')) {
return '7';
if (cpuinfo.includes("ARMv7") || cpuinfo.includes("Cortex-A")) {
return "7";
}
// Check CPU architecture field
const archMatch = cpuinfo.match(/^CPU architecture:\s*(\d+)/m);
if (archMatch && archMatch[1]) {
const arch = parseInt(archMatch[1], 10);
if (arch >= 7) return '7';
if (arch === 6) return '6';
if (arch >= 7) return "7";
if (arch === 6) return "6";
}
} catch (e) {
// Ignore errors, fall through to default
}
}
// Default to ARMv7 (more common)
return '7';
return "7";
}
/**
@ -76,32 +83,17 @@ function getArmVariant() {
function getPlatformPackage() {
let platform = process.platform;
let arch = process.arch;
// Special handling for ARM on Linux
if (platform === 'linux' && arch === 'arm') {
if (platform === "linux" && arch === "arm") {
const variant = getArmVariant();
return `@dagu-org/dagu-linux-armv${variant}`;
return `dagu-linux-armv${variant}`;
}
const key = `${platform}-${arch}`;
return PLATFORM_MAP[key] || null;
}
/**
* Get supported platforms list for error messages
* @returns {string} Formatted list of supported platforms
*/
function getSupportedPlatforms() {
const platforms = [
'Linux: x64, arm64, arm (v6/v7), ia32, ppc64le, s390x',
'macOS: x64 (Intel), arm64 (Apple Silicon)',
'Windows: x64, ia32, arm64',
'FreeBSD: x64, arm64, ia32, arm',
'OpenBSD: x64, arm64'
];
return platforms.join('\n - ');
}
/**
* Get the path to the Dagu binary
* @returns {string|null} Path to binary or null if not found
@ -111,15 +103,17 @@ function getBinaryPath() {
if (cachedBinaryPath && fs.existsSync(cachedBinaryPath)) {
return cachedBinaryPath;
}
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
// First, try platform-specific package
const platformPackage = getPlatformPackage();
if (platformPackage) {
try {
// Try to resolve the binary using require.resolve (Sentry approach)
const binaryPath = require.resolve(`${platformPackage}/bin/${binaryName}`);
const binaryPath = require.resolve(
`${platformPackage}/bin/${binaryName}`
);
if (fs.existsSync(binaryPath)) {
cachedBinaryPath = binaryPath;
return binaryPath;
@ -128,14 +122,14 @@ function getBinaryPath() {
// Package not installed or binary not found
}
}
// Fallback to local binary in main package
const localBinary = path.join(__dirname, '..', 'bin', binaryName);
const localBinary = path.join(__dirname, "..", "bin", binaryName);
if (fs.existsSync(localBinary)) {
cachedBinaryPath = localBinary;
return localBinary;
}
return null;
}
@ -147,24 +141,6 @@ function setPlatformBinary(binaryPath) {
cachedBinaryPath = binaryPath;
}
/**
* Get platform details for debugging
* @returns {object} Platform information
*/
function getPlatformInfo() {
return {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
v8Version: process.versions.v8,
systemPlatform: os.platform(),
systemArch: os.arch(),
systemRelease: os.release(),
armVariant: process.platform === 'linux' && process.arch === 'arm' ? getArmVariant() : null,
detectedPackage: getPlatformPackage()
};
}
/**
* Check if platform-specific package is installed
* @returns {boolean} True if installed, false otherwise
@ -174,9 +150,9 @@ function isPlatformSpecificPackageInstalled() {
if (!platformPackage) {
return false;
}
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
const binaryName = process.platform === "win32" ? "dagu.exe" : "dagu";
try {
// Resolving will fail if the optionalDependency was not installed
require.resolve(`${platformPackage}/bin/${binaryName}`);
@ -186,45 +162,15 @@ function isPlatformSpecificPackageInstalled() {
}
}
/**
* Check if we're in a cross-platform scenario (node_modules moved between architectures)
* @returns {object|null} Warning info if cross-platform detected
*/
function checkCrossPlatformScenario() {
const pkg = require('../package.json');
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
const currentPlatformPackage = getPlatformPackage();
// Check if any platform package is installed but it's not the right one
for (const dep of optionalDeps) {
try {
require.resolve(`${dep}/package.json`);
// Package is installed
if (dep !== currentPlatformPackage) {
// Wrong platform package is installed
const installedPlatform = dep.replace('@dagu-org/dagu-', '');
const currentPlatform = `${process.platform}-${process.arch}`;
return {
installed: installedPlatform,
current: currentPlatform,
message: `WARNING: Found binary for ${installedPlatform} but current platform is ${currentPlatform}.\nThis usually happens when node_modules are copied between different systems.\nPlease reinstall @dagu-org/dagu to get the correct binary.`
};
}
} catch (e) {
// Package not installed, continue checking
}
}
return null;
}
module.exports = {
getPlatformPackage,
getBinaryPath,
setPlatformBinary,
getSupportedPlatforms,
getPlatformInfo,
getArmVariant,
isPlatformSpecificPackageInstalled,
checkCrossPlatformScenario
};
getPlatformInfo: () => ({
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
detectedPackage: getPlatformPackage(),
}),
};

View File

@ -1,66 +0,0 @@
const { spawn } = require("child_process");
const fs = require("fs");
/**
* Validate that the binary is executable and returns expected output
* @param {string} binaryPath Path to the binary
* @returns {Promise<boolean>} True if valid, false otherwise
*/
async function validateBinary(binaryPath) {
// Check if file exists
if (!fs.existsSync(binaryPath)) {
return false;
}
// Check if file is executable (on Unix-like systems)
if (process.platform !== "win32") {
try {
fs.accessSync(binaryPath, fs.constants.X_OK);
} catch (e) {
console.error("Binary is not executable");
return false;
}
}
// Try to run the binary with version subcommand (Dagu uses 'version' not '--version')
return new Promise((resolve) => {
const proc = spawn(binaryPath, ["version"], {
timeout: 5000, // 5 second timeout
windowsHide: true,
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
proc.on("error", (error) => {
console.error("Failed to execute binary:", error.message);
resolve(false);
});
proc.on("close", (code) => {
// Check if the binary executed successfully and returned version info
// Dagu version command returns version string to stderr like "1.18.0".
if (code === 0 && stderr.trim().length > 0) {
resolve(true);
} else {
console.error(`Binary validation failed: exit code ${code}`);
if (stderr) {
console.error("stderr:", stderr);
}
resolve(false);
}
});
});
}
module.exports = {
validateBinary,
};

View File

@ -25,10 +25,10 @@
"license": "GPL-3.0",
"author": "Dagu Contributors",
"bin": {
"dagu": "./bin/dagu"
"dagu": "bin/cli"
},
"scripts": {
"postinstall": "node install.js"
"postinstall": "node ./install.js"
},
"dependencies": {
"tar": "^7.4.3"
@ -56,4 +56,4 @@
"README.md",
"LICENSE"
]
}
}