@droposs/plugin-cli 0.1.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/bin/drop-plugin.js +39 -0
- package/dist/signer.d.ts +10 -0
- package/dist/signer.js +107 -0
- package/package.json +46 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { signPlugin, packPlugin } from "../dist/signer.js";
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2];
|
|
5
|
+
const args = process.argv.slice(3);
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
switch (command) {
|
|
9
|
+
case "sign": {
|
|
10
|
+
const dir = args[0] || ".";
|
|
11
|
+
const res = await signPlugin(dir);
|
|
12
|
+
console.log(`Signed bundle at ${dir}: ${res.fileCount} files verified (signature: ${res.signed ? "yes" : "no"})`);
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
case "pack": {
|
|
16
|
+
const dir = args[0] || ".";
|
|
17
|
+
const outDir = args[1];
|
|
18
|
+
const res = await packPlugin(dir, outDir);
|
|
19
|
+
console.log(`Packed plugin '${res.id}' v${res.version} to ${res.packagePath}`);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case "help":
|
|
23
|
+
default:
|
|
24
|
+
console.log(`Drop Plugin CLI (drop-plugin)
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
drop-plugin sign [dir] Calculate SHA-256 digests and sign drop-plugin.json
|
|
28
|
+
drop-plugin pack [dir] [out] Verify, sign, and package bundle into .dropplugin archive
|
|
29
|
+
drop-plugin build Compile plugin bundle
|
|
30
|
+
drop-plugin test Run plugin tests
|
|
31
|
+
`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch((err) => {
|
|
37
|
+
console.error(`Error: ${err.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function listFiles(root: string, prefix?: string): Promise<string[]>;
|
|
2
|
+
export declare function signPlugin(targetDir: string, signingKey?: string): Promise<{
|
|
3
|
+
fileCount: number;
|
|
4
|
+
signed: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function packPlugin(targetDir: string, outputDir?: string): Promise<{
|
|
7
|
+
packagePath: string;
|
|
8
|
+
id: string;
|
|
9
|
+
version: string;
|
|
10
|
+
}>;
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, realpath, stat, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MANIFEST_FILE = "drop-plugin.json";
|
|
5
|
+
function isInside(base, candidate) {
|
|
6
|
+
const relative = path.relative(base, candidate);
|
|
7
|
+
return (relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
8
|
+
}
|
|
9
|
+
export async function listFiles(root, prefix = "") {
|
|
10
|
+
const results = [];
|
|
11
|
+
const entries = await readdir(path.join(root, prefix), { withFileTypes: true });
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
16
|
+
continue;
|
|
17
|
+
results.push(...(await listFiles(root, rel)));
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile()) {
|
|
20
|
+
if (!prefix && entry.name === MANIFEST_FILE)
|
|
21
|
+
continue;
|
|
22
|
+
results.push(rel);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
26
|
+
}
|
|
27
|
+
export async function signPlugin(targetDir, signingKey) {
|
|
28
|
+
const resolvedPath = path.resolve(process.cwd(), targetDir);
|
|
29
|
+
const bundleDir = await realpath(resolvedPath).catch(() => null);
|
|
30
|
+
if (!bundleDir) {
|
|
31
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
32
|
+
}
|
|
33
|
+
const manifestPath = path.join(bundleDir, MANIFEST_FILE);
|
|
34
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
35
|
+
// Identify primary entry (v1 or v2 server/client entry)
|
|
36
|
+
const entry = manifest.entry ??
|
|
37
|
+
manifest.server?.entry ??
|
|
38
|
+
manifest.client?.entry ??
|
|
39
|
+
"index.js";
|
|
40
|
+
const entryPath = path.resolve(bundleDir, entry);
|
|
41
|
+
const entryExists = await stat(entryPath).catch(() => null);
|
|
42
|
+
if (entryExists) {
|
|
43
|
+
const entryBytes = await readFile(entryPath);
|
|
44
|
+
manifest.checksum = createHash("sha256").update(entryBytes).digest("hex");
|
|
45
|
+
}
|
|
46
|
+
const files = await listFiles(bundleDir);
|
|
47
|
+
const fileChecksums = {};
|
|
48
|
+
const aggregate = createHash("sha256");
|
|
49
|
+
for (const rel of files) {
|
|
50
|
+
const bytes = await readFile(path.join(bundleDir, rel));
|
|
51
|
+
fileChecksums[rel] = createHash("sha256").update(bytes).digest("hex");
|
|
52
|
+
aggregate.update(rel);
|
|
53
|
+
aggregate.update("\0");
|
|
54
|
+
aggregate.update(String(bytes.length));
|
|
55
|
+
aggregate.update("\0");
|
|
56
|
+
aggregate.update(bytes);
|
|
57
|
+
}
|
|
58
|
+
manifest.files = fileChecksums;
|
|
59
|
+
const key = signingKey ?? process.env.DROP_PLUGIN_SIGNING_KEY;
|
|
60
|
+
if (key) {
|
|
61
|
+
manifest.signature = createHmac("sha256", key)
|
|
62
|
+
.update(aggregate.digest("hex"))
|
|
63
|
+
.digest("hex");
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
delete manifest.signature;
|
|
67
|
+
}
|
|
68
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
69
|
+
return { fileCount: files.length, signed: Boolean(key) };
|
|
70
|
+
}
|
|
71
|
+
export async function packPlugin(targetDir, outputDir) {
|
|
72
|
+
const resolvedPath = path.resolve(process.cwd(), targetDir);
|
|
73
|
+
const bundleDir = await realpath(resolvedPath).catch(() => null);
|
|
74
|
+
if (!bundleDir) {
|
|
75
|
+
throw new Error(`Directory not found: ${targetDir}`);
|
|
76
|
+
}
|
|
77
|
+
// Ensure bundle is signed and validated
|
|
78
|
+
await signPlugin(bundleDir);
|
|
79
|
+
const manifestPath = path.join(bundleDir, MANIFEST_FILE);
|
|
80
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
81
|
+
const id = manifest.id;
|
|
82
|
+
const version = manifest.version;
|
|
83
|
+
if (!id || !version) {
|
|
84
|
+
throw new Error("Plugin manifest must specify 'id' and 'version'");
|
|
85
|
+
}
|
|
86
|
+
const outDir = outputDir
|
|
87
|
+
? path.resolve(process.cwd(), outputDir)
|
|
88
|
+
: path.join(bundleDir, "dist-package");
|
|
89
|
+
await mkdir(outDir, { recursive: true });
|
|
90
|
+
const files = await listFiles(bundleDir);
|
|
91
|
+
const bundleMap = {};
|
|
92
|
+
for (const rel of files) {
|
|
93
|
+
const content = await readFile(path.join(bundleDir, rel));
|
|
94
|
+
bundleMap[rel] = content.toString("base64");
|
|
95
|
+
}
|
|
96
|
+
const packageObj = {
|
|
97
|
+
format: "dropplugin-v2",
|
|
98
|
+
id,
|
|
99
|
+
version,
|
|
100
|
+
manifest,
|
|
101
|
+
files: bundleMap,
|
|
102
|
+
packedAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
const packagePath = path.join(outDir, `${id}-${version}.dropplugin`);
|
|
105
|
+
await writeFile(packagePath, JSON.stringify(packageObj, null, 2), "utf-8");
|
|
106
|
+
return { packagePath, id, version };
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@droposs/plugin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop Plugin build, test, signing, and packaging CLI for Drop OSS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"drop-plugin": "./bin/drop-plugin.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/signer.js",
|
|
10
|
+
"types": "dist/signer.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Heretek-Games/drop-plugin-sdk.git",
|
|
21
|
+
"directory": "packages/plugin-cli"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Heretek-Games/drop-plugin-sdk#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Heretek-Games/drop-plugin-sdk/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"drop",
|
|
29
|
+
"drop-oss",
|
|
30
|
+
"droposs",
|
|
31
|
+
"drop-plugin",
|
|
32
|
+
"plugin-cli",
|
|
33
|
+
"signer",
|
|
34
|
+
"packaging",
|
|
35
|
+
"playnite"
|
|
36
|
+
],
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.7.0",
|
|
39
|
+
"@types/node": "^22.0.0"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"test": "node --test"
|
|
45
|
+
}
|
|
46
|
+
}
|