@dagu-org/dagu 1.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/index.js +84 -0
- package/install.js +135 -0
- package/lib/cache.js +230 -0
- package/lib/constants.js +60 -0
- package/lib/download.js +404 -0
- package/lib/platform.js +230 -0
- package/lib/validate.js +65 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @dagu-org/dagu
|
|
2
|
+
|
|
3
|
+
> A powerful Workflow Orchestration Engine with simple declarative YAML API
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@dagu-org/dagu)
|
|
6
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @dagu-org/dagu
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or add to your project:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @dagu-org/dagu
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Command Line
|
|
23
|
+
|
|
24
|
+
After installation, the `dagu` command will be available:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Start the web UI and scheduler
|
|
28
|
+
dagu start-all
|
|
29
|
+
|
|
30
|
+
# Run a workflow
|
|
31
|
+
dagu start my-workflow.yaml
|
|
32
|
+
|
|
33
|
+
# Check workflow status
|
|
34
|
+
dagu status my-workflow.yaml
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Programmatic Usage
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const { execute, getDaguPath } = require('@dagu-org/dagu');
|
|
41
|
+
|
|
42
|
+
// Get path to the binary
|
|
43
|
+
const daguPath = getDaguPath();
|
|
44
|
+
|
|
45
|
+
// Execute dagu commands
|
|
46
|
+
const child = execute(['start', 'workflow.yaml']);
|
|
47
|
+
|
|
48
|
+
// Or use async/await
|
|
49
|
+
const { executeAsync } = require('@dagu-org/dagu');
|
|
50
|
+
|
|
51
|
+
async function runWorkflow() {
|
|
52
|
+
const result = await executeAsync(['start', 'workflow.yaml']);
|
|
53
|
+
console.log('Exit code:', result.code);
|
|
54
|
+
console.log('Output:', result.stdout);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Supported Platforms
|
|
59
|
+
|
|
60
|
+
This package provides pre-built binaries for:
|
|
61
|
+
|
|
62
|
+
- **Linux**: x64, arm64, arm (v6/v7), ia32, ppc64le, s390x
|
|
63
|
+
- **macOS**: x64 (Intel), arm64 (Apple Silicon)
|
|
64
|
+
- **Windows**: x64, ia32, arm64
|
|
65
|
+
- **FreeBSD**: x64, arm64, ia32, arm
|
|
66
|
+
- **OpenBSD**: x64, arm64
|
|
67
|
+
|
|
68
|
+
If your platform is not supported, please build from source: https://github.com/dagu-org/dagu#building-from-source
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- **Zero Dependencies** - Single binary, no runtime requirements
|
|
73
|
+
- **Declarative YAML** - Define workflows in simple YAML format
|
|
74
|
+
- **Web UI** - Beautiful dashboard for monitoring and management
|
|
75
|
+
- **Powerful Scheduling** - Cron expressions, dependencies, and complex workflows
|
|
76
|
+
- **Language Agnostic** - Run any command, script, or executable
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
For detailed documentation, visit: https://github.com/dagu-org/dagu
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
GNU General Public License v3.0
|
package/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dagu npm package - programmatic interface
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { getBinaryPath } = require('./lib/platform');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the path to the Dagu binary
|
|
11
|
+
* @returns {string|null} Path to the binary or null if not found
|
|
12
|
+
*/
|
|
13
|
+
function getDaguPath() {
|
|
14
|
+
return getBinaryPath();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute Dagu with given arguments
|
|
19
|
+
* @param {string[]} args Command line arguments
|
|
20
|
+
* @param {object} options Child process spawn options
|
|
21
|
+
* @returns {ChildProcess} The spawned child process
|
|
22
|
+
*/
|
|
23
|
+
function execute(args = [], options = {}) {
|
|
24
|
+
const binaryPath = getDaguPath();
|
|
25
|
+
|
|
26
|
+
if (!binaryPath) {
|
|
27
|
+
throw new Error('Dagu binary not found. Please ensure Dagu is properly installed.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return spawn(binaryPath, args, {
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
...options
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute Dagu and return a promise
|
|
38
|
+
* @param {string[]} args Command line arguments
|
|
39
|
+
* @param {object} options Child process spawn options
|
|
40
|
+
* @returns {Promise<{code: number, signal: string|null}>} Exit code and signal
|
|
41
|
+
*/
|
|
42
|
+
function executeAsync(args = [], options = {}) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = execute(args, {
|
|
45
|
+
stdio: 'pipe',
|
|
46
|
+
...options
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let stdout = '';
|
|
50
|
+
let stderr = '';
|
|
51
|
+
|
|
52
|
+
if (child.stdout) {
|
|
53
|
+
child.stdout.on('data', (data) => {
|
|
54
|
+
stdout += data.toString();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (child.stderr) {
|
|
59
|
+
child.stderr.on('data', (data) => {
|
|
60
|
+
stderr += data.toString();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
child.on('error', reject);
|
|
65
|
+
|
|
66
|
+
child.on('close', (code, signal) => {
|
|
67
|
+
resolve({
|
|
68
|
+
code,
|
|
69
|
+
signal,
|
|
70
|
+
stdout,
|
|
71
|
+
stderr
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
getDaguPath,
|
|
79
|
+
execute,
|
|
80
|
+
executeAsync,
|
|
81
|
+
// Re-export useful functions
|
|
82
|
+
getBinaryPath,
|
|
83
|
+
getPlatformInfo: require('./lib/platform').getPlatformInfo
|
|
84
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getBinaryPath, getPlatformPackage, setPlatformBinary, getPlatformInfo } = require('./lib/platform');
|
|
6
|
+
const { downloadBinary, downloadBinaryFromNpm } = require('./lib/download');
|
|
7
|
+
const { validateBinary } = require('./lib/validate');
|
|
8
|
+
|
|
9
|
+
async function install() {
|
|
10
|
+
console.log('Installing Dagu...');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Check if running in CI or with --ignore-scripts
|
|
14
|
+
if (process.env.npm_config_ignore_scripts === 'true') {
|
|
15
|
+
console.log('Skipping postinstall script (--ignore-scripts flag detected)');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try to resolve platform-specific package
|
|
20
|
+
const platformPackage = getPlatformPackage();
|
|
21
|
+
if (!platformPackage) {
|
|
22
|
+
console.error(`
|
|
23
|
+
Error: Unsupported platform: ${process.platform}-${process.arch}
|
|
24
|
+
|
|
25
|
+
Dagu does not provide pre-built binaries for this platform.
|
|
26
|
+
Please build from source: https://github.com/dagu-org/dagu#building-from-source
|
|
27
|
+
`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Detected platform: ${process.platform}-${process.arch}`);
|
|
32
|
+
console.log(`Looking for package: ${platformPackage}`);
|
|
33
|
+
|
|
34
|
+
// Check for cross-platform scenario
|
|
35
|
+
const { checkCrossPlatformScenario } = require('./lib/platform');
|
|
36
|
+
const crossPlatformWarning = checkCrossPlatformScenario();
|
|
37
|
+
if (crossPlatformWarning) {
|
|
38
|
+
console.warn(`\n${crossPlatformWarning.message}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if binary already exists from optionalDependency
|
|
42
|
+
const existingBinary = getBinaryPath();
|
|
43
|
+
if (existingBinary && fs.existsSync(existingBinary)) {
|
|
44
|
+
console.log('Using pre-installed binary from optional dependency');
|
|
45
|
+
|
|
46
|
+
// Validate the binary
|
|
47
|
+
if (await validateBinary(existingBinary)) {
|
|
48
|
+
console.log('✓ Dagu installation complete!');
|
|
49
|
+
return;
|
|
50
|
+
} else {
|
|
51
|
+
console.warn('Binary validation failed, attempting to download...');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.log('Optional dependency not found, downloading binary...');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback: Download binary
|
|
59
|
+
try {
|
|
60
|
+
const binaryPath = path.join(__dirname, 'bin', process.platform === 'win32' ? 'dagu.exe' : 'dagu');
|
|
61
|
+
|
|
62
|
+
// Create bin directory if it doesn't exist
|
|
63
|
+
const binDir = path.dirname(binaryPath);
|
|
64
|
+
if (!fs.existsSync(binDir)) {
|
|
65
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Skip download in development if flag file exists
|
|
69
|
+
if (fs.existsSync(path.join(__dirname, '.skip-install'))) {
|
|
70
|
+
console.log('Development mode: skipping binary download (.skip-install file found)');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Download the binary
|
|
75
|
+
await downloadBinary(binaryPath, { method: 'auto' });
|
|
76
|
+
|
|
77
|
+
// Validate the downloaded binary
|
|
78
|
+
if (await validateBinary(binaryPath)) {
|
|
79
|
+
setPlatformBinary(binaryPath);
|
|
80
|
+
console.log('✓ Dagu installation complete!');
|
|
81
|
+
|
|
82
|
+
// Print warning about optionalDependencies if none were found
|
|
83
|
+
if (!hasAnyOptionalDependency()) {
|
|
84
|
+
console.warn(`
|
|
85
|
+
⚠ WARNING: optionalDependencies may be disabled in your environment.
|
|
86
|
+
For better performance and reliability, consider enabling them.
|
|
87
|
+
See: https://docs.npmjs.com/cli/v8/using-npm/config#optional
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error('Downloaded binary validation failed');
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to install Dagu:', error.message);
|
|
95
|
+
console.error(`
|
|
96
|
+
Platform details:
|
|
97
|
+
${JSON.stringify(getPlatformInfo(), null, 2)}
|
|
98
|
+
|
|
99
|
+
Please try one of the following:
|
|
100
|
+
1. Install manually from: https://github.com/dagu-org/dagu/releases
|
|
101
|
+
2. Build from source: https://github.com/dagu-org/dagu#building-from-source
|
|
102
|
+
3. Report this issue: https://github.com/dagu-org/dagu/issues
|
|
103
|
+
`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if any optional dependency is installed
|
|
109
|
+
function hasAnyOptionalDependency() {
|
|
110
|
+
const pkg = require('./package.json');
|
|
111
|
+
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
|
112
|
+
|
|
113
|
+
for (const dep of optionalDeps) {
|
|
114
|
+
try {
|
|
115
|
+
require.resolve(dep);
|
|
116
|
+
return true;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Continue checking
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle errors gracefully
|
|
126
|
+
process.on('unhandledRejection', (error) => {
|
|
127
|
+
console.error('Installation error:', error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Run installation
|
|
132
|
+
install().catch((error) => {
|
|
133
|
+
console.error('Installation failed:', error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get cache directory for binaries
|
|
8
|
+
* @returns {string} Cache directory path
|
|
9
|
+
*/
|
|
10
|
+
function getCacheDir() {
|
|
11
|
+
// Use standard cache locations based on platform
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
let cacheDir;
|
|
14
|
+
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
// Windows: %LOCALAPPDATA%\dagu-cache
|
|
17
|
+
cacheDir = path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 'dagu-cache');
|
|
18
|
+
} else if (process.platform === 'darwin') {
|
|
19
|
+
// macOS: ~/Library/Caches/dagu
|
|
20
|
+
cacheDir = path.join(homeDir, 'Library', 'Caches', 'dagu');
|
|
21
|
+
} else {
|
|
22
|
+
// Linux/BSD: ~/.cache/dagu
|
|
23
|
+
cacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(homeDir, '.cache'), 'dagu');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Allow override via environment variable
|
|
27
|
+
if (process.env.DAGU_CACHE_DIR) {
|
|
28
|
+
cacheDir = process.env.DAGU_CACHE_DIR;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return cacheDir;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get cached binary path
|
|
36
|
+
* @param {string} version Version of the binary
|
|
37
|
+
* @param {string} platform Platform identifier
|
|
38
|
+
* @returns {string} Path to cached binary
|
|
39
|
+
*/
|
|
40
|
+
function getCachedBinaryPath(version, platform) {
|
|
41
|
+
const cacheDir = getCacheDir();
|
|
42
|
+
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
|
43
|
+
return path.join(cacheDir, `${version}-${platform}`, binaryName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if binary exists in cache
|
|
48
|
+
* @param {string} version Version of the binary
|
|
49
|
+
* @param {string} platform Platform identifier
|
|
50
|
+
* @returns {boolean} True if cached, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
function isCached(version, platform) {
|
|
53
|
+
const cachedPath = getCachedBinaryPath(version, platform);
|
|
54
|
+
return fs.existsSync(cachedPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save binary to cache
|
|
59
|
+
* @param {string} sourcePath Path to the binary to cache
|
|
60
|
+
* @param {string} version Version of the binary
|
|
61
|
+
* @param {string} platform Platform identifier
|
|
62
|
+
* @returns {string} Path to cached binary
|
|
63
|
+
*/
|
|
64
|
+
function cacheBinary(sourcePath, version, platform) {
|
|
65
|
+
const cachedPath = getCachedBinaryPath(version, platform);
|
|
66
|
+
const cacheDir = path.dirname(cachedPath);
|
|
67
|
+
|
|
68
|
+
// Create cache directory if it doesn't exist
|
|
69
|
+
if (!fs.existsSync(cacheDir)) {
|
|
70
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy binary to cache
|
|
74
|
+
fs.copyFileSync(sourcePath, cachedPath);
|
|
75
|
+
|
|
76
|
+
// Preserve executable permissions
|
|
77
|
+
if (process.platform !== 'win32') {
|
|
78
|
+
fs.chmodSync(cachedPath, 0o755);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create a metadata file with cache info
|
|
82
|
+
const metadataPath = path.join(cacheDir, 'metadata.json');
|
|
83
|
+
const metadata = {
|
|
84
|
+
version,
|
|
85
|
+
platform,
|
|
86
|
+
cachedAt: new Date().toISOString(),
|
|
87
|
+
checksum: calculateChecksum(cachedPath)
|
|
88
|
+
};
|
|
89
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
90
|
+
|
|
91
|
+
return cachedPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get binary from cache
|
|
96
|
+
* @param {string} version Version of the binary
|
|
97
|
+
* @param {string} platform Platform identifier
|
|
98
|
+
* @returns {string|null} Path to cached binary or null if not found
|
|
99
|
+
*/
|
|
100
|
+
function getCachedBinary(version, platform) {
|
|
101
|
+
if (!isCached(version, platform)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cachedPath = getCachedBinaryPath(version, platform);
|
|
106
|
+
|
|
107
|
+
// Verify the cached binary still works
|
|
108
|
+
try {
|
|
109
|
+
fs.accessSync(cachedPath, fs.constants.X_OK);
|
|
110
|
+
return cachedPath;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Cached binary is corrupted or not executable
|
|
113
|
+
// Remove it from cache
|
|
114
|
+
cleanCacheEntry(version, platform);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Calculate checksum of a file
|
|
121
|
+
* @param {string} filePath Path to the file
|
|
122
|
+
* @returns {string} SHA256 checksum
|
|
123
|
+
*/
|
|
124
|
+
function calculateChecksum(filePath) {
|
|
125
|
+
const hash = crypto.createHash('sha256');
|
|
126
|
+
const data = fs.readFileSync(filePath);
|
|
127
|
+
hash.update(data);
|
|
128
|
+
return hash.digest('hex');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clean specific cache entry
|
|
133
|
+
* @param {string} version Version of the binary
|
|
134
|
+
* @param {string} platform Platform identifier
|
|
135
|
+
*/
|
|
136
|
+
function cleanCacheEntry(version, platform) {
|
|
137
|
+
const cacheDir = path.dirname(getCachedBinaryPath(version, platform));
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(cacheDir)) {
|
|
140
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clean old cache entries (older than specified days)
|
|
146
|
+
* @param {number} maxAgeDays Maximum age in days (default 30)
|
|
147
|
+
*/
|
|
148
|
+
function cleanOldCache(maxAgeDays = 30) {
|
|
149
|
+
const cacheDir = getCacheDir();
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(cacheDir)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const entries = fs.readdirSync(cacheDir);
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const entryPath = path.join(cacheDir, entry);
|
|
163
|
+
const metadataPath = path.join(entryPath, 'metadata.json');
|
|
164
|
+
|
|
165
|
+
if (fs.existsSync(metadataPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
168
|
+
const cachedAt = new Date(metadata.cachedAt).getTime();
|
|
169
|
+
|
|
170
|
+
if (now - cachedAt > maxAgeMs) {
|
|
171
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Invalid metadata, remove entry
|
|
175
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Ignore errors during cleanup
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get cache size
|
|
186
|
+
* @returns {number} Total size in bytes
|
|
187
|
+
*/
|
|
188
|
+
function getCacheSize() {
|
|
189
|
+
const cacheDir = getCacheDir();
|
|
190
|
+
|
|
191
|
+
if (!fs.existsSync(cacheDir)) {
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let totalSize = 0;
|
|
196
|
+
|
|
197
|
+
function calculateDirSize(dirPath) {
|
|
198
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
199
|
+
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
202
|
+
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
calculateDirSize(fullPath);
|
|
205
|
+
} else {
|
|
206
|
+
const stats = fs.statSync(fullPath);
|
|
207
|
+
totalSize += stats.size;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
calculateDirSize(cacheDir);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// Ignore errors
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return totalSize;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
getCacheDir,
|
|
223
|
+
getCachedBinaryPath,
|
|
224
|
+
isCached,
|
|
225
|
+
cacheBinary,
|
|
226
|
+
getCachedBinary,
|
|
227
|
+
cleanCacheEntry,
|
|
228
|
+
cleanOldCache,
|
|
229
|
+
getCacheSize
|
|
230
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for Dagu npm distribution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const GITHUB_ORG = 'dagu-org';
|
|
6
|
+
const GITHUB_REPO = 'dagu';
|
|
7
|
+
const NPM_ORG = '@dagu-org';
|
|
8
|
+
|
|
9
|
+
// Tier classification for platform support
|
|
10
|
+
const PLATFORM_TIERS = {
|
|
11
|
+
TIER_1: [
|
|
12
|
+
'linux-x64',
|
|
13
|
+
'linux-arm64',
|
|
14
|
+
'darwin-x64',
|
|
15
|
+
'darwin-arm64',
|
|
16
|
+
'win32-x64'
|
|
17
|
+
],
|
|
18
|
+
TIER_2: [
|
|
19
|
+
'linux-ia32',
|
|
20
|
+
'linux-armv7',
|
|
21
|
+
'win32-ia32',
|
|
22
|
+
'freebsd-x64'
|
|
23
|
+
],
|
|
24
|
+
TIER_3: [
|
|
25
|
+
'linux-armv6',
|
|
26
|
+
'linux-ppc64',
|
|
27
|
+
'linux-s390x',
|
|
28
|
+
'win32-arm64',
|
|
29
|
+
'freebsd-arm64',
|
|
30
|
+
'freebsd-ia32',
|
|
31
|
+
'freebsd-arm',
|
|
32
|
+
'openbsd-x64',
|
|
33
|
+
'openbsd-arm64'
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Error messages
|
|
38
|
+
const ERRORS = {
|
|
39
|
+
UNSUPPORTED_PLATFORM: 'Unsupported platform',
|
|
40
|
+
DOWNLOAD_FAILED: 'Failed to download binary',
|
|
41
|
+
VALIDATION_FAILED: 'Binary validation failed',
|
|
42
|
+
CHECKSUM_MISMATCH: 'Checksum verification failed',
|
|
43
|
+
EXTRACTION_FAILED: 'Failed to extract archive'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// URLs
|
|
47
|
+
const URLS = {
|
|
48
|
+
RELEASES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases`,
|
|
49
|
+
ISSUES: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/issues`,
|
|
50
|
+
BUILD_DOCS: `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}#building-from-source`
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
GITHUB_ORG,
|
|
55
|
+
GITHUB_REPO,
|
|
56
|
+
NPM_ORG,
|
|
57
|
+
PLATFORM_TIERS,
|
|
58
|
+
ERRORS,
|
|
59
|
+
URLS
|
|
60
|
+
};
|
package/lib/download.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
const tar = require('tar');
|
|
7
|
+
const { getCachedBinary, cacheBinary, cleanOldCache } = require('./cache');
|
|
8
|
+
|
|
9
|
+
// Get package version
|
|
10
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
11
|
+
const GITHUB_RELEASES_URL = 'https://github.com/dagu-org/dagu/releases/download';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map Node.js platform/arch to goreleaser asset names
|
|
15
|
+
*/
|
|
16
|
+
function getAssetName(version) {
|
|
17
|
+
const platform = process.platform;
|
|
18
|
+
const arch = process.arch;
|
|
19
|
+
|
|
20
|
+
// Platform name mapping (matches goreleaser output - lowercase)
|
|
21
|
+
const osMap = {
|
|
22
|
+
'darwin': 'darwin',
|
|
23
|
+
'linux': 'linux',
|
|
24
|
+
'win32': 'windows',
|
|
25
|
+
'freebsd': 'freebsd',
|
|
26
|
+
'openbsd': 'openbsd'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Architecture name mapping (matches goreleaser output)
|
|
30
|
+
const archMap = {
|
|
31
|
+
'x64': 'amd64',
|
|
32
|
+
'ia32': '386',
|
|
33
|
+
'arm64': 'arm64',
|
|
34
|
+
'ppc64': 'ppc64le',
|
|
35
|
+
's390x': 's390x'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let osName = osMap[platform] || platform;
|
|
39
|
+
let archName = archMap[arch] || arch;
|
|
40
|
+
|
|
41
|
+
// Special handling for ARM
|
|
42
|
+
if (arch === 'arm' && platform === 'linux') {
|
|
43
|
+
const { getArmVariant } = require('./platform');
|
|
44
|
+
const variant = getArmVariant();
|
|
45
|
+
archName = `armv${variant}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// All assets are .tar.gz now (goreleaser changed this)
|
|
49
|
+
const ext = '.tar.gz';
|
|
50
|
+
return `dagu_${version}_${osName}_${archName}${ext}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Make HTTP request and return buffer (Sentry-style)
|
|
55
|
+
*/
|
|
56
|
+
function makeRequest(url) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
https
|
|
59
|
+
.get(url, (response) => {
|
|
60
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
63
|
+
response.on('end', () => {
|
|
64
|
+
resolve(Buffer.concat(chunks));
|
|
65
|
+
});
|
|
66
|
+
} else if (
|
|
67
|
+
response.statusCode >= 300 &&
|
|
68
|
+
response.statusCode < 400 &&
|
|
69
|
+
response.headers.location
|
|
70
|
+
) {
|
|
71
|
+
// Follow redirects
|
|
72
|
+
makeRequest(response.headers.location).then(resolve, reject);
|
|
73
|
+
} else {
|
|
74
|
+
reject(
|
|
75
|
+
new Error(
|
|
76
|
+
`Server responded with status code ${response.statusCode} when downloading the package!`
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
.on('error', (error) => {
|
|
82
|
+
reject(error);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Download file with progress reporting
|
|
89
|
+
*/
|
|
90
|
+
async function downloadFile(url, destination, options = {}) {
|
|
91
|
+
const { onProgress, maxRetries = 3 } = options;
|
|
92
|
+
let lastError;
|
|
93
|
+
|
|
94
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
return await downloadFileAttempt(url, destination, { onProgress, attempt });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
lastError = error;
|
|
99
|
+
if (attempt < maxRetries) {
|
|
100
|
+
console.log(`Download failed (attempt ${attempt}/${maxRetries}), retrying...`);
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw lastError;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Single download attempt
|
|
111
|
+
*/
|
|
112
|
+
function downloadFileAttempt(url, destination, options = {}) {
|
|
113
|
+
const { onProgress, attempt = 1 } = options;
|
|
114
|
+
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const tempFile = `${destination}.download.${process.pid}.tmp`;
|
|
117
|
+
|
|
118
|
+
https.get(url, (response) => {
|
|
119
|
+
// Handle redirects
|
|
120
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
121
|
+
const redirectUrl = response.headers.location;
|
|
122
|
+
if (!redirectUrl) {
|
|
123
|
+
reject(new Error('Redirect location not provided'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
downloadFileAttempt(redirectUrl, destination, options)
|
|
127
|
+
.then(resolve)
|
|
128
|
+
.catch(reject);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (response.statusCode !== 200) {
|
|
133
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
138
|
+
let downloadedSize = 0;
|
|
139
|
+
|
|
140
|
+
const fileStream = fs.createWriteStream(tempFile);
|
|
141
|
+
|
|
142
|
+
response.on('data', (chunk) => {
|
|
143
|
+
downloadedSize += chunk.length;
|
|
144
|
+
if (onProgress && totalSize) {
|
|
145
|
+
const percentage = Math.round((downloadedSize / totalSize) * 100);
|
|
146
|
+
onProgress(percentage, downloadedSize, totalSize);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
response.pipe(fileStream);
|
|
151
|
+
|
|
152
|
+
fileStream.on('finish', () => {
|
|
153
|
+
fileStream.close(() => {
|
|
154
|
+
// Move temp file to final destination
|
|
155
|
+
fs.renameSync(tempFile, destination);
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
fileStream.on('error', (err) => {
|
|
161
|
+
fs.unlinkSync(tempFile);
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
}).on('error', (err) => {
|
|
165
|
+
if (fs.existsSync(tempFile)) {
|
|
166
|
+
fs.unlinkSync(tempFile);
|
|
167
|
+
}
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Extract archive based on file extension
|
|
175
|
+
*/
|
|
176
|
+
async function extractArchive(archivePath, outputDir) {
|
|
177
|
+
const ext = path.extname(archivePath).toLowerCase();
|
|
178
|
+
|
|
179
|
+
if (ext === '.gz' || archivePath.endsWith('.tar.gz')) {
|
|
180
|
+
// Extract tar.gz
|
|
181
|
+
await tar.extract({
|
|
182
|
+
file: archivePath,
|
|
183
|
+
cwd: outputDir,
|
|
184
|
+
filter: (path) => path === 'dagu' || path === 'dagu.exe'
|
|
185
|
+
});
|
|
186
|
+
} else if (ext === '.zip') {
|
|
187
|
+
// For Windows, we need a zip extractor
|
|
188
|
+
// Using built-in Windows extraction via PowerShell
|
|
189
|
+
const { execSync } = require('child_process');
|
|
190
|
+
const command = `powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${outputDir}' -Force"`;
|
|
191
|
+
execSync(command);
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Unsupported archive format: ${ext}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Download and verify checksums
|
|
199
|
+
*/
|
|
200
|
+
async function downloadChecksums(version) {
|
|
201
|
+
const checksumsUrl = `${GITHUB_RELEASES_URL}/v${version}/checksums.txt`;
|
|
202
|
+
const tempFile = path.join(require('os').tmpdir(), `dagu-checksums-${process.pid}.txt`);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await downloadFile(checksumsUrl, tempFile);
|
|
206
|
+
const content = fs.readFileSync(tempFile, 'utf8');
|
|
207
|
+
|
|
208
|
+
// Parse checksums file
|
|
209
|
+
const checksums = {};
|
|
210
|
+
content.split('\n').forEach(line => {
|
|
211
|
+
const match = line.match(/^([a-f0-9]{64})\s+(.+)$/);
|
|
212
|
+
if (match) {
|
|
213
|
+
checksums[match[2]] = match[1];
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return checksums;
|
|
218
|
+
} finally {
|
|
219
|
+
if (fs.existsSync(tempFile)) {
|
|
220
|
+
fs.unlinkSync(tempFile);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Verify file checksum
|
|
227
|
+
*/
|
|
228
|
+
function verifyChecksum(filePath, expectedChecksum) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const hash = crypto.createHash('sha256');
|
|
231
|
+
const stream = fs.createReadStream(filePath);
|
|
232
|
+
|
|
233
|
+
stream.on('data', (data) => hash.update(data));
|
|
234
|
+
stream.on('end', () => {
|
|
235
|
+
const actualChecksum = hash.digest('hex');
|
|
236
|
+
if (actualChecksum === expectedChecksum) {
|
|
237
|
+
resolve(true);
|
|
238
|
+
} else {
|
|
239
|
+
reject(new Error(`Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}`));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
stream.on('error', reject);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extract file from npm tarball (aligned with Sentry approach)
|
|
248
|
+
*/
|
|
249
|
+
function extractFileFromTarball(tarballBuffer, filepath) {
|
|
250
|
+
// Tar archives are organized in 512 byte blocks.
|
|
251
|
+
// Blocks can either be header blocks or data blocks.
|
|
252
|
+
// Header blocks contain file names of the archive in the first 100 bytes, terminated by a null byte.
|
|
253
|
+
// The size of a file is contained in bytes 124-135 of a header block and in octal format.
|
|
254
|
+
// The following blocks will be data blocks containing the file.
|
|
255
|
+
let offset = 0;
|
|
256
|
+
while (offset < tarballBuffer.length) {
|
|
257
|
+
const header = tarballBuffer.subarray(offset, offset + 512);
|
|
258
|
+
offset += 512;
|
|
259
|
+
|
|
260
|
+
const fileName = header.toString('utf-8', 0, 100).replace(/\0.*/g, '');
|
|
261
|
+
const fileSize = parseInt(header.toString('utf-8', 124, 136).replace(/\0.*/g, ''), 8);
|
|
262
|
+
|
|
263
|
+
if (fileName === filepath) {
|
|
264
|
+
return tarballBuffer.subarray(offset, offset + fileSize);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Clamp offset to the upper multiple of 512
|
|
268
|
+
offset = (offset + fileSize + 511) & ~511;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
throw new Error(`File ${filepath} not found in tarball`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Download binary from npm registry (Sentry-style)
|
|
276
|
+
*/
|
|
277
|
+
async function downloadBinaryFromNpm(version) {
|
|
278
|
+
const { getPlatformPackage } = require('./platform');
|
|
279
|
+
const platformPackage = getPlatformPackage();
|
|
280
|
+
|
|
281
|
+
if (!platformPackage) {
|
|
282
|
+
throw new Error('Platform not supported!');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const packageName = platformPackage.replace('@dagu-org/', '');
|
|
286
|
+
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
|
287
|
+
|
|
288
|
+
console.log(`Downloading ${platformPackage} from npm registry...`);
|
|
289
|
+
|
|
290
|
+
// Download the tarball of the right binary distribution package
|
|
291
|
+
const tarballUrl = `https://registry.npmjs.org/${platformPackage}/-/${packageName}-${version}.tgz`;
|
|
292
|
+
const tarballDownloadBuffer = await makeRequest(tarballUrl);
|
|
293
|
+
const tarballBuffer = zlib.unzipSync(tarballDownloadBuffer);
|
|
294
|
+
|
|
295
|
+
// Extract binary from package
|
|
296
|
+
const binaryData = extractFileFromTarball(tarballBuffer, `package/bin/${binaryName}`);
|
|
297
|
+
|
|
298
|
+
return binaryData;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Main download function
|
|
303
|
+
*/
|
|
304
|
+
async function downloadBinary(destination, options = {}) {
|
|
305
|
+
const version = options.version || PACKAGE_VERSION;
|
|
306
|
+
const { method = 'auto', useCache = true } = options;
|
|
307
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
308
|
+
|
|
309
|
+
console.log(`Installing Dagu v${version} for ${platformKey}...`);
|
|
310
|
+
|
|
311
|
+
// Check cache first
|
|
312
|
+
if (useCache) {
|
|
313
|
+
const cachedBinary = getCachedBinary(version, platformKey);
|
|
314
|
+
if (cachedBinary) {
|
|
315
|
+
console.log('✓ Using cached binary');
|
|
316
|
+
fs.copyFileSync(cachedBinary, destination);
|
|
317
|
+
if (process.platform !== 'win32') {
|
|
318
|
+
fs.chmodSync(destination, 0o755);
|
|
319
|
+
}
|
|
320
|
+
// Clean old cache entries
|
|
321
|
+
cleanOldCache();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
let binaryData;
|
|
328
|
+
|
|
329
|
+
if (method === 'npm' || method === 'auto') {
|
|
330
|
+
// Try npm registry first (following Sentry's approach)
|
|
331
|
+
try {
|
|
332
|
+
binaryData = await downloadBinaryFromNpm(version);
|
|
333
|
+
console.log('✓ Downloaded from npm registry');
|
|
334
|
+
} catch (npmError) {
|
|
335
|
+
if (method === 'npm') {
|
|
336
|
+
throw npmError;
|
|
337
|
+
}
|
|
338
|
+
console.log('npm registry download failed, trying GitHub releases...');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!binaryData && (method === 'github' || method === 'auto')) {
|
|
343
|
+
// Fallback to GitHub releases
|
|
344
|
+
const assetName = getAssetName(version);
|
|
345
|
+
const downloadUrl = `${GITHUB_RELEASES_URL}/v${version}/${assetName}`;
|
|
346
|
+
|
|
347
|
+
const tempFile = path.join(require('os').tmpdir(), `dagu-${process.pid}-${Date.now()}.tmp`);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
await downloadFile(downloadUrl, tempFile, {
|
|
351
|
+
onProgress: (percentage, downloaded, total) => {
|
|
352
|
+
const mb = (size) => (size / 1024 / 1024).toFixed(2);
|
|
353
|
+
process.stdout.write(`\rProgress: ${percentage}% (${mb(downloaded)}MB / ${mb(total)}MB)`);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
console.log('\n✓ Downloaded from GitHub releases');
|
|
357
|
+
|
|
358
|
+
// Extract from archive
|
|
359
|
+
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
|
360
|
+
|
|
361
|
+
// All files are .tar.gz now
|
|
362
|
+
const archiveData = fs.readFileSync(tempFile);
|
|
363
|
+
const tarData = zlib.gunzipSync(archiveData);
|
|
364
|
+
binaryData = extractFileFromTarball(tarData, binaryName);
|
|
365
|
+
} finally {
|
|
366
|
+
if (fs.existsSync(tempFile)) {
|
|
367
|
+
fs.unlinkSync(tempFile);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!binaryData) {
|
|
373
|
+
throw new Error('Failed to download binary from any source');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Write binary to destination
|
|
377
|
+
const dir = path.dirname(destination);
|
|
378
|
+
if (!fs.existsSync(dir)) {
|
|
379
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fs.writeFileSync(destination, binaryData, { mode: 0o755 });
|
|
383
|
+
console.log('✓ Binary installed successfully');
|
|
384
|
+
|
|
385
|
+
// Cache the binary for future use
|
|
386
|
+
if (useCache) {
|
|
387
|
+
try {
|
|
388
|
+
cacheBinary(destination, version, platformKey);
|
|
389
|
+
console.log('✓ Binary cached for future installations');
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// Caching failed, but installation succeeded
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw new Error(`Failed to download binary: ${error.message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
module.exports = {
|
|
401
|
+
downloadBinary,
|
|
402
|
+
downloadBinaryFromNpm,
|
|
403
|
+
getAssetName
|
|
404
|
+
};
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Platform mapping from Node.js to npm package names
|
|
6
|
+
const PLATFORM_MAP = {
|
|
7
|
+
// Tier 1 - Most common platforms
|
|
8
|
+
'linux-x64': '@dagu-org/dagu-linux-x64',
|
|
9
|
+
'linux-arm64': '@dagu-org/dagu-linux-arm64',
|
|
10
|
+
'darwin-x64': '@dagu-org/dagu-darwin-x64',
|
|
11
|
+
'darwin-arm64': '@dagu-org/dagu-darwin-arm64',
|
|
12
|
+
'win32-x64': '@dagu-org/dagu-win32-x64',
|
|
13
|
+
|
|
14
|
+
// Tier 2 - Common but less frequent
|
|
15
|
+
'linux-ia32': '@dagu-org/dagu-linux-ia32',
|
|
16
|
+
'win32-ia32': '@dagu-org/dagu-win32-ia32',
|
|
17
|
+
'freebsd-x64': '@dagu-org/dagu-freebsd-x64',
|
|
18
|
+
|
|
19
|
+
// Tier 3 - Rare platforms
|
|
20
|
+
'win32-arm64': '@dagu-org/dagu-win32-arm64',
|
|
21
|
+
'linux-ppc64': '@dagu-org/dagu-linux-ppc64',
|
|
22
|
+
'linux-s390x': '@dagu-org/dagu-linux-s390x',
|
|
23
|
+
'freebsd-arm64': '@dagu-org/dagu-freebsd-arm64',
|
|
24
|
+
'freebsd-ia32': '@dagu-org/dagu-freebsd-ia32',
|
|
25
|
+
'freebsd-arm': '@dagu-org/dagu-freebsd-arm',
|
|
26
|
+
'openbsd-x64': '@dagu-org/dagu-openbsd-x64',
|
|
27
|
+
'openbsd-arm64': '@dagu-org/dagu-openbsd-arm64',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Cache for binary path
|
|
31
|
+
let cachedBinaryPath = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect ARM variant on Linux systems
|
|
35
|
+
* @returns {string} ARM variant ('6' or '7')
|
|
36
|
+
*/
|
|
37
|
+
function getArmVariant() {
|
|
38
|
+
// First try process.config
|
|
39
|
+
if (process.config && process.config.variables && process.config.variables.arm_version) {
|
|
40
|
+
return String(process.config.variables.arm_version);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// On Linux, check /proc/cpuinfo
|
|
44
|
+
if (process.platform === 'linux') {
|
|
45
|
+
try {
|
|
46
|
+
const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
|
|
47
|
+
|
|
48
|
+
// Check for specific ARM architecture indicators
|
|
49
|
+
if (cpuinfo.includes('ARMv6') || cpuinfo.includes('ARM926') || cpuinfo.includes('ARM1176')) {
|
|
50
|
+
return '6';
|
|
51
|
+
}
|
|
52
|
+
if (cpuinfo.includes('ARMv7') || cpuinfo.includes('Cortex-A')) {
|
|
53
|
+
return '7';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check CPU architecture field
|
|
57
|
+
const archMatch = cpuinfo.match(/^CPU architecture:\s*(\d+)/m);
|
|
58
|
+
if (archMatch && archMatch[1]) {
|
|
59
|
+
const arch = parseInt(archMatch[1], 10);
|
|
60
|
+
if (arch >= 7) return '7';
|
|
61
|
+
if (arch === 6) return '6';
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// Ignore errors, fall through to default
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default to ARMv7 (more common)
|
|
69
|
+
return '7';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the npm package name for the current platform
|
|
74
|
+
* @returns {string|null} Package name or null if unsupported
|
|
75
|
+
*/
|
|
76
|
+
function getPlatformPackage() {
|
|
77
|
+
let platform = process.platform;
|
|
78
|
+
let arch = process.arch;
|
|
79
|
+
|
|
80
|
+
// Special handling for ARM on Linux
|
|
81
|
+
if (platform === 'linux' && arch === 'arm') {
|
|
82
|
+
const variant = getArmVariant();
|
|
83
|
+
return `@dagu-org/dagu-linux-armv${variant}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const key = `${platform}-${arch}`;
|
|
87
|
+
return PLATFORM_MAP[key] || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get supported platforms list for error messages
|
|
92
|
+
* @returns {string} Formatted list of supported platforms
|
|
93
|
+
*/
|
|
94
|
+
function getSupportedPlatforms() {
|
|
95
|
+
const platforms = [
|
|
96
|
+
'Linux: x64, arm64, arm (v6/v7), ia32, ppc64le, s390x',
|
|
97
|
+
'macOS: x64 (Intel), arm64 (Apple Silicon)',
|
|
98
|
+
'Windows: x64, ia32, arm64',
|
|
99
|
+
'FreeBSD: x64, arm64, ia32, arm',
|
|
100
|
+
'OpenBSD: x64, arm64'
|
|
101
|
+
];
|
|
102
|
+
return platforms.join('\n - ');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the path to the Dagu binary
|
|
107
|
+
* @returns {string|null} Path to binary or null if not found
|
|
108
|
+
*/
|
|
109
|
+
function getBinaryPath() {
|
|
110
|
+
// Return cached path if available
|
|
111
|
+
if (cachedBinaryPath && fs.existsSync(cachedBinaryPath)) {
|
|
112
|
+
return cachedBinaryPath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
|
116
|
+
|
|
117
|
+
// First, try platform-specific package
|
|
118
|
+
const platformPackage = getPlatformPackage();
|
|
119
|
+
if (platformPackage) {
|
|
120
|
+
try {
|
|
121
|
+
// Try to resolve the binary using require.resolve (Sentry approach)
|
|
122
|
+
const binaryPath = require.resolve(`${platformPackage}/bin/${binaryName}`);
|
|
123
|
+
if (fs.existsSync(binaryPath)) {
|
|
124
|
+
cachedBinaryPath = binaryPath;
|
|
125
|
+
return binaryPath;
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Package not installed or binary not found
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Fallback to local binary in main package
|
|
133
|
+
const localBinary = path.join(__dirname, '..', 'bin', binaryName);
|
|
134
|
+
if (fs.existsSync(localBinary)) {
|
|
135
|
+
cachedBinaryPath = localBinary;
|
|
136
|
+
return localBinary;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set the cached binary path
|
|
144
|
+
* @param {string} binaryPath Path to the binary
|
|
145
|
+
*/
|
|
146
|
+
function setPlatformBinary(binaryPath) {
|
|
147
|
+
cachedBinaryPath = binaryPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get platform details for debugging
|
|
152
|
+
* @returns {object} Platform information
|
|
153
|
+
*/
|
|
154
|
+
function getPlatformInfo() {
|
|
155
|
+
return {
|
|
156
|
+
platform: process.platform,
|
|
157
|
+
arch: process.arch,
|
|
158
|
+
nodeVersion: process.version,
|
|
159
|
+
v8Version: process.versions.v8,
|
|
160
|
+
systemPlatform: os.platform(),
|
|
161
|
+
systemArch: os.arch(),
|
|
162
|
+
systemRelease: os.release(),
|
|
163
|
+
armVariant: process.platform === 'linux' && process.arch === 'arm' ? getArmVariant() : null,
|
|
164
|
+
detectedPackage: getPlatformPackage()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if platform-specific package is installed
|
|
170
|
+
* @returns {boolean} True if installed, false otherwise
|
|
171
|
+
*/
|
|
172
|
+
function isPlatformSpecificPackageInstalled() {
|
|
173
|
+
const platformPackage = getPlatformPackage();
|
|
174
|
+
if (!platformPackage) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const binaryName = process.platform === 'win32' ? 'dagu.exe' : 'dagu';
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Resolving will fail if the optionalDependency was not installed
|
|
182
|
+
require.resolve(`${platformPackage}/bin/${binaryName}`);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if we're in a cross-platform scenario (node_modules moved between architectures)
|
|
191
|
+
* @returns {object|null} Warning info if cross-platform detected
|
|
192
|
+
*/
|
|
193
|
+
function checkCrossPlatformScenario() {
|
|
194
|
+
const pkg = require('../package.json');
|
|
195
|
+
const optionalDeps = Object.keys(pkg.optionalDependencies || {});
|
|
196
|
+
const currentPlatformPackage = getPlatformPackage();
|
|
197
|
+
|
|
198
|
+
// Check if any platform package is installed but it's not the right one
|
|
199
|
+
for (const dep of optionalDeps) {
|
|
200
|
+
try {
|
|
201
|
+
require.resolve(`${dep}/package.json`);
|
|
202
|
+
// Package is installed
|
|
203
|
+
if (dep !== currentPlatformPackage) {
|
|
204
|
+
// Wrong platform package is installed
|
|
205
|
+
const installedPlatform = dep.replace('@dagu-org/dagu-', '');
|
|
206
|
+
const currentPlatform = `${process.platform}-${process.arch}`;
|
|
207
|
+
return {
|
|
208
|
+
installed: installedPlatform,
|
|
209
|
+
current: currentPlatform,
|
|
210
|
+
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.`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// Package not installed, continue checking
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
getPlatformPackage,
|
|
223
|
+
getBinaryPath,
|
|
224
|
+
setPlatformBinary,
|
|
225
|
+
getSupportedPlatforms,
|
|
226
|
+
getPlatformInfo,
|
|
227
|
+
getArmVariant,
|
|
228
|
+
isPlatformSpecificPackageInstalled,
|
|
229
|
+
checkCrossPlatformScenario
|
|
230
|
+
};
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validate that the binary is executable and returns expected output
|
|
6
|
+
* @param {string} binaryPath Path to the binary
|
|
7
|
+
* @returns {Promise<boolean>} True if valid, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
async function validateBinary(binaryPath) {
|
|
10
|
+
// Check if file exists
|
|
11
|
+
if (!fs.existsSync(binaryPath)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Check if file is executable (on Unix-like systems)
|
|
16
|
+
if (process.platform !== 'win32') {
|
|
17
|
+
try {
|
|
18
|
+
fs.accessSync(binaryPath, fs.constants.X_OK);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error('Binary is not executable');
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try to run the binary with --version flag
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const proc = spawn(binaryPath, ['--version'], {
|
|
28
|
+
timeout: 5000, // 5 second timeout
|
|
29
|
+
windowsHide: true
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let stdout = '';
|
|
33
|
+
let stderr = '';
|
|
34
|
+
|
|
35
|
+
proc.stdout.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
proc.stderr.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
proc.on('error', (error) => {
|
|
44
|
+
console.error('Failed to execute binary:', error.message);
|
|
45
|
+
resolve(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
proc.on('close', (code) => {
|
|
49
|
+
// Check if the binary executed successfully and returned version info
|
|
50
|
+
if (code === 0 && stdout.toLowerCase().includes('dagu')) {
|
|
51
|
+
resolve(true);
|
|
52
|
+
} else {
|
|
53
|
+
console.error(`Binary validation failed: exit code ${code}`);
|
|
54
|
+
if (stderr) {
|
|
55
|
+
console.error('stderr:', stderr);
|
|
56
|
+
}
|
|
57
|
+
resolve(false);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
validateBinary
|
|
65
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dagu-org/dagu",
|
|
3
|
+
"version": "1.17.4",
|
|
4
|
+
"description": "A powerful Workflow Orchestration Engine with simple declarative YAML API. Zero-dependency, single binary for Linux, macOS, and Windows.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"workflow",
|
|
7
|
+
"automation",
|
|
8
|
+
"orchestration",
|
|
9
|
+
"dag",
|
|
10
|
+
"pipeline",
|
|
11
|
+
"scheduler",
|
|
12
|
+
"task-runner",
|
|
13
|
+
"workflow-engine",
|
|
14
|
+
"devops",
|
|
15
|
+
"ci-cd"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/dagu-org/dagu",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/dagu-org/dagu/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/dagu-org/dagu.git"
|
|
24
|
+
},
|
|
25
|
+
"license": "GPL-3.0",
|
|
26
|
+
"author": "Dagu Contributors",
|
|
27
|
+
"bin": {
|
|
28
|
+
"dagu": "./bin/dagu"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"postinstall": "node install.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tar": "^6.1.15"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"@dagu-org/dagu-linux-x64": "1.17.4",
|
|
38
|
+
"@dagu-org/dagu-linux-arm64": "1.17.4",
|
|
39
|
+
"@dagu-org/dagu-darwin-x64": "1.17.4",
|
|
40
|
+
"@dagu-org/dagu-darwin-arm64": "1.17.4",
|
|
41
|
+
"@dagu-org/dagu-win32-x64": "1.17.4",
|
|
42
|
+
"@dagu-org/dagu-linux-ia32": "1.17.4",
|
|
43
|
+
"@dagu-org/dagu-linux-armv7": "1.17.4",
|
|
44
|
+
"@dagu-org/dagu-linux-armv6": "1.17.4",
|
|
45
|
+
"@dagu-org/dagu-win32-ia32": "1.17.4",
|
|
46
|
+
"@dagu-org/dagu-freebsd-x64": "1.17.4"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=14.0.0"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"bin/",
|
|
53
|
+
"lib/",
|
|
54
|
+
"install.js",
|
|
55
|
+
"index.js",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
]
|
|
59
|
+
}
|