@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 ADDED
@@ -0,0 +1,84 @@
1
+ # @dagu-org/dagu
2
+
3
+ > A powerful Workflow Orchestration Engine with simple declarative YAML API
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@dagu-org/dagu.svg)](https://www.npmjs.com/package/@dagu-org/dagu)
6
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](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
+ };
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };
@@ -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
+ }