@xudesheng/conflu 0.2.0
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 +25 -0
- package/bin/conflu.js +31 -0
- package/lib/diagnostics.mjs +18 -0
- package/lib/install.mjs +218 -0
- package/lib/platform.mjs +27 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @xudesheng/conflu
|
|
2
|
+
|
|
3
|
+
`@xudesheng/conflu` is a thin npm wrapper for the native Conflu CLI binaries.
|
|
4
|
+
|
|
5
|
+
It downloads the matching prebuilt binary from:
|
|
6
|
+
|
|
7
|
+
- <https://github.com/xudesheng/conflu/releases>
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -g @xudesheng/conflu
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Run
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
conflu --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## npx
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @xudesheng/conflu --version
|
|
25
|
+
```
|
package/bin/conflu.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const binaryName = process.platform === "win32" ? "conflu.exe" : "conflu";
|
|
10
|
+
const binaryPath = path.join(packageRoot, ".conflu-bin", binaryName);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(binaryPath)) {
|
|
13
|
+
console.error(
|
|
14
|
+
"[CNP006] Native Conflu binary is missing.\nHint: reinstall with `npm i -g @xudesheng/conflu`.",
|
|
15
|
+
);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
env: process.env,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
console.error(
|
|
26
|
+
`[CNP006] Failed to launch native Conflu binary.\nHint: ${result.error.message}`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class InstallError extends Error {
|
|
2
|
+
constructor(code, message, hint, cause) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "InstallError";
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.hint = hint;
|
|
7
|
+
this.cause = cause;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toDiagnostic(err) {
|
|
12
|
+
if (err instanceof InstallError) {
|
|
13
|
+
return `[${err.code}] ${err.message}\nHint: ${err.hint}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fallback = err && err.message ? err.message : String(err);
|
|
17
|
+
return `[CNP999] Unexpected installer failure\nHint: ${fallback}`;
|
|
18
|
+
}
|
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { createWriteStream } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
|
|
9
|
+
import AdmZip from "adm-zip";
|
|
10
|
+
import * as tar from "tar";
|
|
11
|
+
|
|
12
|
+
import { InstallError } from "./diagnostics.mjs";
|
|
13
|
+
import { resolvePlatform } from "./platform.mjs";
|
|
14
|
+
|
|
15
|
+
function releaseBase(version) {
|
|
16
|
+
if (process.env.CONFLU_NPM_RELEASE_BASE_URL) {
|
|
17
|
+
return `${process.env.CONFLU_NPM_RELEASE_BASE_URL}/v${version}`;
|
|
18
|
+
}
|
|
19
|
+
return `https://github.com/xudesheng/conflu/releases/download/v${version}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchText(url) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url);
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new InstallError(
|
|
27
|
+
"CNP003",
|
|
28
|
+
`Failed to fetch checksum file: HTTP ${res.status}`,
|
|
29
|
+
"Verify that the target release exists and is public.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return await res.text();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof InstallError) {
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
throw new InstallError(
|
|
38
|
+
"CNP003",
|
|
39
|
+
"Failed to fetch checksum file.",
|
|
40
|
+
"Check network access and retry install.",
|
|
41
|
+
err,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function downloadToFile(url, destPath) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(url);
|
|
49
|
+
if (!res.ok || !res.body) {
|
|
50
|
+
throw new InstallError(
|
|
51
|
+
"CNP002",
|
|
52
|
+
`Failed to download binary archive: HTTP ${res.status}`,
|
|
53
|
+
"Verify release asset URL and retry install.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof InstallError) {
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
throw new InstallError(
|
|
62
|
+
"CNP002",
|
|
63
|
+
"Failed while streaming archive download.",
|
|
64
|
+
"Check network stability and retry install.",
|
|
65
|
+
err,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseChecksums(content) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for (const rawLine of content.split("\n")) {
|
|
73
|
+
const line = rawLine.trim();
|
|
74
|
+
if (!line) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const [hash, ...rest] = line.split(/\s+/);
|
|
78
|
+
const filename = rest.join(" ").trim();
|
|
79
|
+
if (hash && filename) {
|
|
80
|
+
map.set(filename, hash);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return map;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function sha256File(filePath) {
|
|
87
|
+
const h = crypto.createHash("sha256");
|
|
88
|
+
const fd = await fs.open(filePath, "r");
|
|
89
|
+
try {
|
|
90
|
+
const buf = Buffer.allocUnsafe(1024 * 1024);
|
|
91
|
+
while (true) {
|
|
92
|
+
const { bytesRead } = await fd.read(buf, 0, buf.length, null);
|
|
93
|
+
if (bytesRead === 0) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
h.update(buf.subarray(0, bytesRead));
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await fd.close();
|
|
100
|
+
}
|
|
101
|
+
return h.digest("hex");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function extractArchive(archivePath, extractDir) {
|
|
105
|
+
if (archivePath.endsWith(".zip")) {
|
|
106
|
+
try {
|
|
107
|
+
const zip = new AdmZip(archivePath);
|
|
108
|
+
zip.extractAllTo(extractDir, true);
|
|
109
|
+
return;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new InstallError(
|
|
112
|
+
"CNP005",
|
|
113
|
+
"Failed to extract zip archive.",
|
|
114
|
+
"Check disk permissions and free space, then retry install.",
|
|
115
|
+
err,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (archivePath.endsWith(".tar.gz")) {
|
|
121
|
+
try {
|
|
122
|
+
await tar.x({
|
|
123
|
+
file: archivePath,
|
|
124
|
+
cwd: extractDir,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new InstallError(
|
|
129
|
+
"CNP005",
|
|
130
|
+
"Failed to extract tar.gz archive.",
|
|
131
|
+
"Check disk permissions and free space, then retry install.",
|
|
132
|
+
err,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new InstallError(
|
|
138
|
+
"CNP005",
|
|
139
|
+
`Unsupported archive format: ${archivePath}`,
|
|
140
|
+
"Please report this issue with release version and platform details.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function installBinary(version, packageRoot) {
|
|
145
|
+
const resolved = resolvePlatform();
|
|
146
|
+
if (!resolved) {
|
|
147
|
+
throw new InstallError(
|
|
148
|
+
"CNP001",
|
|
149
|
+
`Unsupported platform: ${os.platform()}-${os.arch()}`,
|
|
150
|
+
"Install from GitHub Releases manually for this platform.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const base = releaseBase(version);
|
|
155
|
+
const archiveName = `conflu-v${version}-${resolved.assetName}`;
|
|
156
|
+
const archiveUrl = `${base}/${archiveName}`;
|
|
157
|
+
const checksumsUrl = `${base}/conflu-v${version}-SHA256SUMS.txt`;
|
|
158
|
+
|
|
159
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "conflu-npm-"));
|
|
160
|
+
const archivePath = path.join(tempRoot, archiveName);
|
|
161
|
+
const extractDir = path.join(tempRoot, "extract");
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
165
|
+
await downloadToFile(archiveUrl, archivePath);
|
|
166
|
+
const checksumsRaw = await fetchText(checksumsUrl);
|
|
167
|
+
const checksums = parseChecksums(checksumsRaw);
|
|
168
|
+
const expected = checksums.get(archiveName);
|
|
169
|
+
if (!expected) {
|
|
170
|
+
throw new InstallError(
|
|
171
|
+
"CNP004",
|
|
172
|
+
`Checksum entry missing for ${archiveName}`,
|
|
173
|
+
"Verify release assets include SHA256SUMS and retry.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actual = await sha256File(archivePath);
|
|
178
|
+
if (actual !== expected) {
|
|
179
|
+
throw new InstallError(
|
|
180
|
+
"CNP004",
|
|
181
|
+
`Checksum mismatch for ${archiveName}`,
|
|
182
|
+
"Release asset integrity check failed. Abort install and investigate release assets.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await extractArchive(archivePath, extractDir);
|
|
187
|
+
const sourceBinary = path.join(extractDir, resolved.binaryName);
|
|
188
|
+
try {
|
|
189
|
+
await fs.stat(sourceBinary);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw new InstallError(
|
|
192
|
+
"CNP005",
|
|
193
|
+
`Extracted archive does not contain ${resolved.binaryName}`,
|
|
194
|
+
"Release packaging is inconsistent. Check release artifacts.",
|
|
195
|
+
err,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const binDir = path.join(packageRoot, ".conflu-bin");
|
|
200
|
+
const targetBinary = path.join(binDir, resolved.binaryName);
|
|
201
|
+
await fs.mkdir(binDir, { recursive: true });
|
|
202
|
+
await fs.copyFile(sourceBinary, targetBinary);
|
|
203
|
+
if (resolved.binaryName !== "conflu.exe") {
|
|
204
|
+
try {
|
|
205
|
+
await fs.chmod(targetBinary, 0o755);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
throw new InstallError(
|
|
208
|
+
"CNP007",
|
|
209
|
+
"Failed to set executable permission on conflu binary.",
|
|
210
|
+
"Check filesystem mount options and permissions, then retry install.",
|
|
211
|
+
err,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
package/lib/platform.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export function resolvePlatform() {
|
|
4
|
+
const platform = os.platform();
|
|
5
|
+
const arch = os.arch();
|
|
6
|
+
|
|
7
|
+
if (platform === "win32" && arch === "x64") {
|
|
8
|
+
return {
|
|
9
|
+
assetName: "windows-x64.zip",
|
|
10
|
+
binaryName: "conflu.exe",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (platform === "linux" && arch === "x64") {
|
|
14
|
+
return {
|
|
15
|
+
assetName: "linux-x64-musl.tar.gz",
|
|
16
|
+
binaryName: "conflu",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (platform === "darwin" && (arch === "x64" || arch === "arm64")) {
|
|
20
|
+
return {
|
|
21
|
+
assetName: "macos-universal.zip",
|
|
22
|
+
binaryName: "conflu",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xudesheng/conflu",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Thin npm wrapper for Conflu native binaries",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/desheng-io/conflu.git",
|
|
9
|
+
"directory": "distribution/npm-wrapper"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/xudesheng/conflu",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"conflu": "bin/conflu.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"lib/",
|
|
19
|
+
"scripts/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"adm-zip": "^0.5.16",
|
|
33
|
+
"tar": "^7.4.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { installBinary } from "../lib/install.mjs";
|
|
6
|
+
import { toDiagnostic } from "../lib/diagnostics.mjs";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
10
|
+
|
|
11
|
+
if (process.env.CONFLU_NPM_SKIP_POSTINSTALL === "1") {
|
|
12
|
+
console.log("[CNP000] Skipping postinstall because CONFLU_NPM_SKIP_POSTINSTALL=1");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const pkgPath = path.join(packageRoot, "package.json");
|
|
18
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
19
|
+
await installBinary(pkg.version, packageRoot);
|
|
20
|
+
console.log(`[CNP000] Installed Conflu binary for @xudesheng/conflu@${pkg.version}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((err) => {
|
|
24
|
+
console.error(toDiagnostic(err));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|