@dagu-org/dagu 1.17.4 → 1.22.10
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 +2 -2
- package/bin/cli +44 -0
- package/index.js +35 -76
- package/install.js +97 -123
- package/lib/platform.js +65 -119
- package/package.json +14 -14
- package/lib/cache.js +0 -230
- package/lib/constants.js +0 -60
- package/lib/download.js +0 -404
- package/lib/validate.js +0 -65
package/lib/download.js
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
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/validate.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
};
|