From fae48f2f8592909bddff2a8e5afbead5ed9b08c4 Mon Sep 17 00:00:00 2001 From: Yota Hamada Date: Tue, 29 Jul 2025 20:27:29 +0900 Subject: [PATCH] chore: fix npm package setup --- npm/dagu/bin/cli | 25 +++ npm/dagu/index.js | 92 ++------- npm/dagu/install.js | 224 ++++++++++----------- npm/dagu/lib/cache.js | 230 ---------------------- npm/dagu/lib/constants.js | 60 ------ npm/dagu/lib/download.js | 404 -------------------------------------- npm/dagu/lib/platform.js | 184 ++++++----------- npm/dagu/lib/validate.js | 66 ------- npm/dagu/package.json | 6 +- 9 files changed, 206 insertions(+), 1085 deletions(-) create mode 100755 npm/dagu/bin/cli delete mode 100644 npm/dagu/lib/cache.js delete mode 100644 npm/dagu/lib/constants.js delete mode 100644 npm/dagu/lib/download.js delete mode 100644 npm/dagu/lib/validate.js diff --git a/npm/dagu/bin/cli b/npm/dagu/bin/cli new file mode 100755 index 00000000..c2999c1e --- /dev/null +++ b/npm/dagu/bin/cli @@ -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", +}); diff --git a/npm/dagu/index.js b/npm/dagu/index.js index f74b0ded..20145efa 100644 --- a/npm/dagu/index.js +++ b/npm/dagu/index.js @@ -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 -}; \ No newline at end of file +}; diff --git a/npm/dagu/install.js b/npm/dagu/install.js index bbcc1b33..ee9d02d9 100644 --- a/npm/dagu/install.js +++ b/npm/dagu/install.js @@ -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); -}); \ No newline at end of file +// 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." + ); +} diff --git a/npm/dagu/lib/cache.js b/npm/dagu/lib/cache.js deleted file mode 100644 index 0c5f46b9..00000000 --- a/npm/dagu/lib/cache.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/npm/dagu/lib/constants.js b/npm/dagu/lib/constants.js deleted file mode 100644 index 2c3052c9..00000000 --- a/npm/dagu/lib/constants.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/npm/dagu/lib/download.js b/npm/dagu/lib/download.js deleted file mode 100644 index a9c582da..00000000 --- a/npm/dagu/lib/download.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/npm/dagu/lib/platform.js b/npm/dagu/lib/platform.js index 34238fa0..0b597015 100644 --- a/npm/dagu/lib/platform.js +++ b/npm/dagu/lib/platform.js @@ -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 -}; \ No newline at end of file + getPlatformInfo: () => ({ + platform: process.platform, + arch: process.arch, + nodeVersion: process.version, + detectedPackage: getPlatformPackage(), + }), +}; diff --git a/npm/dagu/lib/validate.js b/npm/dagu/lib/validate.js deleted file mode 100644 index 496f2b5b..00000000 --- a/npm/dagu/lib/validate.js +++ /dev/null @@ -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} 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, -}; diff --git a/npm/dagu/package.json b/npm/dagu/package.json index 844d96ec..cd889fdd 100644 --- a/npm/dagu/package.json +++ b/npm/dagu/package.json @@ -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" ] -} \ No newline at end of file +}