@delicious233/dida-cli 0.2.0 → 0.2.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/bin/dida +0 -0
- package/package.json +4 -3
- package/scripts/install.js +198 -55
package/bin/dida
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delicious233/dida-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "JSON-first CLI for Dida365 / TickTick task automation with stable JSON output.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dida": "bin/dida"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"postinstall": "node scripts/install.js"
|
|
10
|
+
"postinstall": "node scripts/install.js",
|
|
11
|
+
"test": "node --test scripts/install.test.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"bin/dida",
|
package/scripts/install.js
CHANGED
|
@@ -6,97 +6,224 @@ const path = require("path");
|
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const https = require("https");
|
|
8
8
|
const { execFileSync } = require("child_process");
|
|
9
|
+
const pkg = require("../package.json");
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
11
|
+
const defaultRepo = "DeliciousBuding/dida-cli";
|
|
12
|
+
const defaultMaxDownloadBytes = 200 * 1024 * 1024;
|
|
12
13
|
const binDir = path.join(__dirname, "..", "bin");
|
|
13
14
|
|
|
14
|
-
function platformName() {
|
|
15
|
-
if (
|
|
16
|
-
if (
|
|
17
|
-
if (
|
|
18
|
-
throw new Error(`unsupported platform: ${
|
|
15
|
+
function platformName(platform = process.platform) {
|
|
16
|
+
if (platform === "win32") return "windows";
|
|
17
|
+
if (platform === "linux") return "linux";
|
|
18
|
+
if (platform === "darwin") return "darwin";
|
|
19
|
+
throw new Error(`unsupported platform: ${platform}`);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function archName() {
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
24
|
-
throw new Error(`unsupported architecture: ${
|
|
22
|
+
function archName(arch = process.arch) {
|
|
23
|
+
if (arch === "x64") return "amd64";
|
|
24
|
+
if (arch === "arm64") return "arm64";
|
|
25
|
+
throw new Error(`unsupported architecture: ${arch}`);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (res.statusCode !== 200) {
|
|
35
|
-
reject(new Error(`download failed ${res.statusCode}: ${url}`));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const chunks = [];
|
|
39
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
40
|
-
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
41
|
-
}).on("error", reject);
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function sha256(buffer) {
|
|
46
|
-
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function main() {
|
|
50
|
-
const osName = platformName();
|
|
51
|
-
const arch = archName();
|
|
28
|
+
function createInstallPlan(options = {}) {
|
|
29
|
+
const env = options.env || process.env;
|
|
30
|
+
const packageJson = options.packageJson || pkg;
|
|
31
|
+
const repo = options.repo || env.DIDA_REPO || defaultRepo;
|
|
32
|
+
const version = env.DIDA_VERSION || `v${packageJson.version}`;
|
|
33
|
+
const osName = platformName(options.platform);
|
|
34
|
+
const arch = archName(options.arch);
|
|
52
35
|
const ext = osName === "windows" ? "zip" : "tar.gz";
|
|
53
36
|
const exe = osName === "windows" ? "dida.exe" : "dida";
|
|
54
37
|
const installedExe = osName === "windows" ? "dida.exe" : "dida-bin";
|
|
55
|
-
|
|
56
|
-
let base = version
|
|
38
|
+
const base = version
|
|
57
39
|
? `https://github.com/${repo}/releases/download/${version}`
|
|
58
40
|
: `https://github.com/${repo}/releases/latest/download`;
|
|
59
|
-
|
|
41
|
+
|
|
42
|
+
return { repo, version, osName, arch, ext, exe, installedExe, base };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function escapeRegExp(value) {
|
|
46
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveAssetFromChecksums(checksumsText, plan) {
|
|
50
|
+
let resolvedVersion = plan.version;
|
|
60
51
|
let asset = "";
|
|
61
52
|
|
|
62
53
|
if (resolvedVersion) {
|
|
63
|
-
asset = `dida_${resolvedVersion}_${osName}_${arch}.${ext}`;
|
|
54
|
+
asset = `dida_${resolvedVersion}_${plan.osName}_${plan.arch}.${plan.ext}`;
|
|
64
55
|
} else {
|
|
65
|
-
const suffix = `_${osName}_${arch}.${ext}`;
|
|
66
|
-
asset =
|
|
56
|
+
const suffix = `_${plan.osName}_${plan.arch}.${plan.ext}`;
|
|
57
|
+
asset = checksumsText.split(/\r?\n/)
|
|
67
58
|
.map((line) => line.trim().split(/\s+/)[1])
|
|
68
|
-
.find((name) => name && name.startsWith("dida_v") && name.endsWith(suffix));
|
|
59
|
+
.find((name) => name && name.startsWith("dida_v") && name.endsWith(suffix)) || "";
|
|
69
60
|
if (asset) {
|
|
70
|
-
const
|
|
61
|
+
const pattern = `^dida_(v[^_]+)_${plan.osName}_${plan.arch}\\.${escapeRegExp(plan.ext)}$`;
|
|
62
|
+
const match = asset.match(new RegExp(pattern));
|
|
71
63
|
if (match) resolvedVersion = match[1];
|
|
72
64
|
}
|
|
73
65
|
}
|
|
66
|
+
|
|
74
67
|
if (!resolvedVersion || !asset) {
|
|
75
|
-
throw new Error(`could not resolve latest release asset for ${osName}/${arch}`);
|
|
68
|
+
throw new Error(`could not resolve latest release asset for ${plan.osName}/${plan.arch}`);
|
|
76
69
|
}
|
|
70
|
+
return { asset, resolvedVersion };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sleep(ms) {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
function responseTooLargeError(url, actualBytes, maxBytes) {
|
|
78
|
+
return new Error(`response too large for ${url}: ${actualBytes} bytes exceeds ${maxBytes} bytes`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function retryableError(message) {
|
|
82
|
+
const error = new Error(message);
|
|
83
|
+
error.retryable = true;
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldRetryDownload(error) {
|
|
88
|
+
return error && error.retryable === true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requestBuffer(url, options = {}) {
|
|
92
|
+
const requestOptions = typeof options === "number" ? { redirects: options } : options;
|
|
93
|
+
const redirects = requestOptions.redirects || 0;
|
|
94
|
+
const maxRedirects = requestOptions.maxRedirects ?? 5;
|
|
95
|
+
const retries = requestOptions.retries ?? 2;
|
|
96
|
+
const retryDelayMs = requestOptions.retryDelayMs ?? 250;
|
|
97
|
+
const wait = requestOptions.wait || requestOptions.sleep || sleep;
|
|
98
|
+
const get = requestOptions.get || https.get;
|
|
99
|
+
|
|
100
|
+
return requestBufferWithRetries(url, { ...requestOptions, redirects, maxRedirects, retries, retryDelayMs, wait, get });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function requestBufferWithRetries(url, requestOptions) {
|
|
104
|
+
let lastError = null;
|
|
105
|
+
for (let attempt = 0; attempt <= requestOptions.retries; attempt += 1) {
|
|
106
|
+
try {
|
|
107
|
+
return await requestBufferOnce(url, requestOptions);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
lastError = error;
|
|
110
|
+
if (attempt >= requestOptions.retries || !shouldRetryDownload(error)) {
|
|
111
|
+
if (attempt > 0) {
|
|
112
|
+
throw new Error(`download failed after ${attempt + 1} attempts: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
const delay = requestOptions.retryDelayMs * (2 ** attempt);
|
|
117
|
+
if (delay > 0) await requestOptions.wait(delay);
|
|
118
|
+
else await requestOptions.wait(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw lastError;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function requestBufferOnce(url, requestOptions) {
|
|
125
|
+
const maxBytes = requestOptions.maxBytes ?? defaultMaxDownloadBytes;
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
let settled = false;
|
|
129
|
+
const finish = (error, value) => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
if (error) reject(error);
|
|
133
|
+
else resolve(value);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const req = requestOptions.get(url, { headers: { "User-Agent": "dida-cli-npm-installer" }, timeout: 60000 }, (res) => {
|
|
137
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
138
|
+
res.resume();
|
|
139
|
+
if (requestOptions.redirects >= requestOptions.maxRedirects) {
|
|
140
|
+
finish(new Error(`too many redirects: ${url}`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const next = new URL(res.headers.location, url).toString();
|
|
144
|
+
requestBuffer(next, { ...requestOptions, redirects: requestOptions.redirects + 1 })
|
|
145
|
+
.then((value) => finish(null, value), (error) => finish(error));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (res.statusCode !== 200) {
|
|
149
|
+
res.resume();
|
|
150
|
+
const error = res.statusCode >= 500
|
|
151
|
+
? retryableError(`download failed ${res.statusCode}: ${url}`)
|
|
152
|
+
: new Error(`download failed ${res.statusCode}: ${url}`);
|
|
153
|
+
finish(error);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const contentLength = Number(res.headers["content-length"]);
|
|
157
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
158
|
+
res.resume();
|
|
159
|
+
finish(responseTooLargeError(url, contentLength, maxBytes));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const chunks = [];
|
|
163
|
+
let received = 0;
|
|
164
|
+
res.on("data", (chunk) => {
|
|
165
|
+
received += chunk.length;
|
|
166
|
+
if (received > maxBytes) {
|
|
167
|
+
if (typeof res.destroy === "function") res.destroy();
|
|
168
|
+
finish(responseTooLargeError(url, received, maxBytes));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
chunks.push(chunk);
|
|
172
|
+
});
|
|
173
|
+
res.on("end", () => finish(null, Buffer.concat(chunks)));
|
|
174
|
+
res.on("error", (error) => {
|
|
175
|
+
error.retryable = true;
|
|
176
|
+
finish(error);
|
|
177
|
+
});
|
|
178
|
+
}).on("error", (error) => {
|
|
179
|
+
error.retryable = true;
|
|
180
|
+
finish(error);
|
|
181
|
+
});
|
|
182
|
+
req.on("timeout", () => {
|
|
183
|
+
req.destroy();
|
|
184
|
+
finish(retryableError(`request timed out: ${url}`));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sha256(buffer) {
|
|
190
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function verifyArchiveChecksum(checksumsText, asset, archive) {
|
|
194
|
+
const line = checksumsText.split(/\r?\n/).find((item) => item.endsWith(` ${asset}`));
|
|
80
195
|
if (!line) throw new Error(`checksum not found for ${asset}`);
|
|
81
196
|
const expected = line.split(/\s+/)[0].toLowerCase();
|
|
82
197
|
const actual = sha256(archive);
|
|
83
198
|
if (actual !== expected) throw new Error(`checksum mismatch for ${asset}`);
|
|
199
|
+
return expected;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function main(options = {}) {
|
|
203
|
+
const plan = createInstallPlan(options);
|
|
204
|
+
const download = options.requestBuffer || requestBuffer;
|
|
205
|
+
const checksums = await download(`${plan.base}/checksums.txt`);
|
|
206
|
+
const checksumsText = checksums.toString("utf8");
|
|
207
|
+
const { asset } = resolveAssetFromChecksums(checksumsText, plan);
|
|
208
|
+
|
|
209
|
+
const archive = await download(`${plan.base}/${asset}`);
|
|
210
|
+
verifyArchiveChecksum(checksumsText, asset, archive);
|
|
84
211
|
|
|
85
212
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "dida-npm-"));
|
|
86
213
|
try {
|
|
87
214
|
const archivePath = path.join(temp, asset);
|
|
88
215
|
fs.writeFileSync(archivePath, archive);
|
|
89
|
-
if (ext === "zip") {
|
|
216
|
+
if (plan.ext === "zip") {
|
|
90
217
|
execFileSync("powershell", ["-NoProfile", "-Command", `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${temp.replace(/'/g, "''")}' -Force`], { stdio: "inherit" });
|
|
91
218
|
} else {
|
|
92
219
|
execFileSync("tar", ["-xzf", archivePath, "-C", temp], { stdio: "inherit" });
|
|
93
220
|
}
|
|
94
|
-
const found = findFile(temp, exe);
|
|
221
|
+
const found = findFile(temp, plan.exe);
|
|
95
222
|
if (!found) throw new Error("binary not found in archive");
|
|
96
223
|
fs.mkdirSync(binDir, { recursive: true });
|
|
97
|
-
const target = path.join(binDir, installedExe);
|
|
224
|
+
const target = path.join(binDir, plan.installedExe);
|
|
98
225
|
fs.copyFileSync(found, target);
|
|
99
|
-
if (osName !== "windows") fs.chmodSync(target, 0o755);
|
|
226
|
+
if (plan.osName !== "windows") fs.chmodSync(target, 0o755);
|
|
100
227
|
} finally {
|
|
101
228
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
102
229
|
}
|
|
@@ -115,7 +242,23 @@ function findFile(dir, fileName) {
|
|
|
115
242
|
return null;
|
|
116
243
|
}
|
|
117
244
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
245
|
+
module.exports = {
|
|
246
|
+
archName,
|
|
247
|
+
createInstallPlan,
|
|
248
|
+
findFile,
|
|
249
|
+
main,
|
|
250
|
+
platformName,
|
|
251
|
+
requestBuffer,
|
|
252
|
+
responseTooLargeError,
|
|
253
|
+
resolveAssetFromChecksums,
|
|
254
|
+
sha256,
|
|
255
|
+
shouldRetryDownload,
|
|
256
|
+
verifyArchiveChecksum,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (require.main === module) {
|
|
260
|
+
main().catch((error) => {
|
|
261
|
+
console.error(`dida-cli install failed: ${error.message}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
}
|