@abapify/p2-cli 0.0.0 → 0.4.1

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 CHANGED
@@ -1,3 +1,170 @@
1
1
  # @abapify/p2-cli
2
2
 
3
- Placeholder. The real release is published via CI/CD.
3
+ CLI for Eclipse P2 repositories - download, extract, and decompile plugins.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install -g @abapify/p2-cli
10
+
11
+ # or run without installing globally
12
+ npx @abapify/p2-cli --help
13
+ ```
14
+
15
+ ## Development
16
+
17
+ ```bash
18
+ # From monorepo root
19
+ bun install
20
+ npx nx build p2-cli
21
+
22
+ # Or run directly with tsx
23
+ npx tsx tools/p2-cli/src/cli.ts
24
+ ```
25
+
26
+ ## Publishing Bootstrap (maintainers)
27
+
28
+ `p2-cli` is wired into workspace release publishing via `nx release`.
29
+
30
+ ```bash
31
+ # Validate npm publish/trusted-publisher readiness
32
+ bunx nx run p2-cli:npm-trust-check
33
+
34
+ # One-time bootstrap for new npm package/trusted publishing
35
+ bunx nx run p2-cli:npm-trust-check --prepare
36
+ ```
37
+
38
+ Then publish from CI using the repository release workflow (which triggers `.github/workflows/publish.yml`).
39
+
40
+ ## Commands
41
+
42
+ ### `p2 download <url>`
43
+
44
+ Download plugins from a P2 repository.
45
+
46
+ ```bash
47
+ # Download ADT plugins from SAP tools
48
+ p2 download https://tools.hana.ondemand.com/latest -o ./sdk
49
+
50
+ # Filter to specific plugins
51
+ p2 download https://tools.hana.ondemand.com/latest -o ./sdk -f "com.sap.adt.*"
52
+
53
+ # Download and extract in one step
54
+ p2 download https://tools.hana.ondemand.com/latest -o ./sdk --extract
55
+ ```
56
+
57
+ **Options:**
58
+
59
+ - `-o, --output <dir>` - Output directory (default: `./p2-download`)
60
+ - `-f, --filter <patterns>` - Filter plugins by ID pattern (comma-separated)
61
+ - `-e, --extract` - Also extract files after download
62
+ - `--extract-output <dir>` - Output directory for extraction
63
+ - `--extract-patterns <patterns>` - File patterns to extract (default: `*.xsd,*.ecore,*.genmodel,*.xml`)
64
+
65
+ ### `p2 extract <input>`
66
+
67
+ Extract files from JAR archives. Works as a general-purpose JAR extractor.
68
+
69
+ ```bash
70
+ # Extract schemas from downloaded plugins
71
+ p2 extract ./sdk/plugins -o ./extracted
72
+
73
+ # Extract specific file types
74
+ p2 extract ./sdk/plugins -o ./extracted -p "*.xsd,*.xml"
75
+
76
+ # Extract everything (no organization)
77
+ p2 extract ./sdk/plugins -o ./extracted -p "*" --no-organize
78
+
79
+ # Single JAR file
80
+ p2 extract ./my-plugin.jar -o ./extracted
81
+ ```
82
+
83
+ **Options:**
84
+
85
+ - `-o, --output <dir>` - Output directory (default: `./extracted`)
86
+ - `-p, --patterns <patterns>` - File patterns to extract (default: `*.xsd,*.ecore,*.genmodel,*.xml`)
87
+ - `--no-organize` - Do not organize files by type (flat output)
88
+ - `-v, --verbose` - Verbose output
89
+
90
+ **Organized output structure:**
91
+
92
+ ```
93
+ extracted/
94
+ ├── schemas/
95
+ │ ├── xsd/ # XML Schema definitions
96
+ │ ├── ecore/ # Eclipse EMF models
97
+ │ └── genmodel/ # Generator models
98
+ ├── templates/ # Code templates
99
+ ├── xml/ # Other XML files
100
+ ├── classes/ # Java class files (by JAR)
101
+ └── sources/ # Java source files (by JAR)
102
+ ```
103
+
104
+ ### `p2 decompile <input>`
105
+
106
+ Decompile Java class files to readable source code.
107
+
108
+ ```bash
109
+ # Decompile extracted classes
110
+ p2 decompile ./extracted/classes -o ./decompiled
111
+
112
+ # Use specific decompiler
113
+ p2 decompile ./extracted/classes -o ./decompiled -d cfr
114
+ ```
115
+
116
+ **Options:**
117
+
118
+ - `-o, --output <dir>` - Output directory (default: `./decompiled`)
119
+ - `-d, --decompiler <name>` - Decompiler to use (`cfr`, `procyon`, `fernflower`)
120
+ - `-v, --verbose` - Verbose output
121
+
122
+ **Supported decompilers:**
123
+
124
+ - **CFR** - `brew install cfr-decompiler` (macOS/Linux)
125
+ - **Procyon** - Download from GitHub
126
+ - **Fernflower** - Bundled with IntelliJ IDEA
127
+
128
+ ## Examples
129
+
130
+ ### Download SAP ADT SDK
131
+
132
+ ```bash
133
+ # Full workflow: download, extract, and organize
134
+ p2 download https://tools.hana.ondemand.com/latest \
135
+ -o ./adt-sdk \
136
+ -f "com.sap.adt.*,com.sap.conn.jco.*" \
137
+ --extract \
138
+ --extract-output ./adt-sdk/extracted
139
+
140
+ # Result:
141
+ # ./adt-sdk/
142
+ # ├── artifacts.jar
143
+ # ├── content.jar
144
+ # ├── plugins/
145
+ # │ ├── com.sap.adt.tools.core_3.54.1.jar
146
+ # │ └── ...
147
+ # └── extracted/
148
+ # ├── schemas/xsd/
149
+ # ├── schemas/ecore/
150
+ # └── ...
151
+ ```
152
+
153
+ ### Extract from existing Eclipse installation
154
+
155
+ ```bash
156
+ # macOS
157
+ p2 extract /Applications/Eclipse.app/Contents/Eclipse/plugins \
158
+ -o ./eclipse-schemas \
159
+ -p "*.xsd"
160
+
161
+ # Linux
162
+ p2 extract ~/.eclipse/*/plugins -o ./eclipse-schemas
163
+ ```
164
+
165
+ ## Requirements
166
+
167
+ - Node.js 18+
168
+ - `wget` command (for downloads)
169
+ - `unzip` command (for extraction)
170
+ - Java decompiler (optional, for decompilation)
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command } from "commander";
4
+ import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
5
+ import { basename, join } from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/lib/utils.ts
20
+ /**
21
+ * Execute shell command and return output
22
+ */
23
+ function execOutput(cmd) {
24
+ return execSync(cmd, {
25
+ encoding: "utf-8",
26
+ maxBuffer: 50 * 1024 * 1024
27
+ });
28
+ }
29
+ /**
30
+ * Find files matching glob pattern using native find/ls
31
+ */
32
+ function findFiles(dir, pattern) {
33
+ if (!existsSync(dir)) return [];
34
+ const files = [];
35
+ const entries = readdirSync(dir, {
36
+ withFileTypes: true,
37
+ recursive: true
38
+ });
39
+ const escaped = pattern.replaceAll(/[.+^${}()|[\]\\/]/g, "\\$&");
40
+ const regex = new RegExp("^" + escaped.replaceAll(/\*/g, ".*").replaceAll(/\?/g, ".") + "$");
41
+ for (const entry of entries) if (entry.isFile() && regex.test(entry.name)) {
42
+ const parentPath = entry.parentPath ?? dir;
43
+ files.push(join(parentPath, entry.name));
44
+ }
45
+ return files;
46
+ }
47
+ /**
48
+ * Ensure directory exists
49
+ */
50
+ function ensureDir(dir) {
51
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
52
+ }
53
+ //#endregion
54
+ //#region src/commands/download.ts
55
+ /**
56
+ * Parse artifacts.xml from P2 repository
57
+ */
58
+ function parseArtifacts(xml) {
59
+ const artifacts = [];
60
+ const regex = /<artifact classifier='([^']+)' id='([^']+)' version='([^']+)'>/g;
61
+ let match;
62
+ while ((match = regex.exec(xml)) !== null) artifacts.push({
63
+ classifier: match[1],
64
+ id: match[2],
65
+ version: match[3]
66
+ });
67
+ return artifacts;
68
+ }
69
+ /**
70
+ * Download P2 repository metadata and optionally plugins
71
+ */
72
+ async function download(repoUrl, options) {
73
+ const { output, filter, extract, extractOutput, extractPatterns } = options;
74
+ console.log(`🔧 P2 Repository Download`);
75
+ console.log(` Repository: ${repoUrl}`);
76
+ console.log(` Output: ${output}`);
77
+ console.log("");
78
+ ensureDir(output);
79
+ const artifactsJar = join(output, "artifacts.jar");
80
+ console.log("📥 Downloading artifacts.jar...");
81
+ execOutput(`wget -q "${repoUrl}/artifacts.jar" -O "${artifactsJar}"`);
82
+ const contentJar = join(output, "content.jar");
83
+ console.log("📥 Downloading content.jar...");
84
+ execOutput(`wget -q "${repoUrl}/content.jar" -O "${contentJar}"`);
85
+ const bundles = parseArtifacts(execOutput(`unzip -p "${artifactsJar}" artifacts.xml`)).filter((a) => a.classifier === "osgi.bundle");
86
+ console.log(`📋 Found ${bundles.length} plugins`);
87
+ let toDownload = bundles;
88
+ if (filter) {
89
+ const patterns = filter.split(",").map((p) => p.trim());
90
+ toDownload = bundles.filter((a) => patterns.some((p) => {
91
+ if (p.includes("*")) {
92
+ const escaped = p.replaceAll(/[.+?^${}()|[\]\\/]/g, "\\$&");
93
+ return new RegExp("^" + escaped.replaceAll(/\*/g, ".*") + "$").test(a.id);
94
+ }
95
+ return a.id.startsWith(p);
96
+ }));
97
+ console.log(`🎯 Filtered to ${toDownload.length} plugins matching: ${filter}`);
98
+ }
99
+ if (toDownload.length === 0) {
100
+ console.log("⚠️ No plugins to download");
101
+ return;
102
+ }
103
+ const pluginsDir = join(output, "plugins");
104
+ ensureDir(pluginsDir);
105
+ let downloaded = 0;
106
+ let skipped = 0;
107
+ for (let i = 0; i < toDownload.length; i++) {
108
+ const artifact = toDownload[i];
109
+ const fileName = `${artifact.id}_${artifact.version}.jar`;
110
+ const targetPath = join(pluginsDir, fileName);
111
+ process.stdout.write(`\r Downloading ${i + 1}/${toDownload.length}: ${artifact.id.slice(0, 50).padEnd(50)}`);
112
+ if (existsSync(targetPath)) {
113
+ skipped++;
114
+ continue;
115
+ }
116
+ const url = `${repoUrl}/plugins/${fileName}`;
117
+ try {
118
+ execOutput(`wget -q "${url}" -O "${targetPath}"`);
119
+ downloaded++;
120
+ } catch {
121
+ console.error(`\n ❌ Failed: ${artifact.id}`);
122
+ }
123
+ }
124
+ console.log("\n");
125
+ console.log("✅ Download complete!");
126
+ console.log(` Downloaded: ${downloaded}`);
127
+ console.log(` Skipped: ${skipped}`);
128
+ console.log(` Location: ${pluginsDir}`);
129
+ if (extract) {
130
+ const { extractJars } = await Promise.resolve().then(() => extract_exports);
131
+ console.log("");
132
+ await extractJars(pluginsDir, {
133
+ output: extractOutput || join(output, "extracted"),
134
+ patterns: extractPatterns
135
+ });
136
+ }
137
+ }
138
+ //#endregion
139
+ //#region src/commands/extract.ts
140
+ var extract_exports = /* @__PURE__ */ __exportAll({ extractJars: () => extractJars });
141
+ /**
142
+ * Extract files from JAR archives - just unzip preserving package structure
143
+ */
144
+ async function extractJars(input, options) {
145
+ const { output, patterns, verbose = false } = options;
146
+ console.log(`🔧 JAR Extraction`);
147
+ console.log(` Input: ${input}`);
148
+ console.log(` Output: ${output}`);
149
+ console.log(` Patterns: ${patterns ? patterns.join(", ") : "all files"}`);
150
+ console.log("");
151
+ const jars = findFiles(input, "*.jar");
152
+ if (jars.length === 0) if (input.endsWith(".jar") && existsSync(input)) jars.push(input);
153
+ else {
154
+ console.log("❌ No JAR files found");
155
+ return;
156
+ }
157
+ console.log(`📦 Found ${jars.length} JAR files`);
158
+ console.log("");
159
+ if (existsSync(output)) rmSync(output, { recursive: true });
160
+ ensureDir(output);
161
+ const patternArgs = patterns ? patterns.map((p) => `"${p}"`).join(" ") : "";
162
+ for (const jar of jars) {
163
+ const jarName = basename(jar);
164
+ if (verbose) console.log(` 📦 ${jarName}`);
165
+ try {
166
+ execOutput(`unzip -o -q "${jar}" ${patternArgs} -d "${output}" 2>/dev/null || true`);
167
+ } catch {}
168
+ }
169
+ console.log("");
170
+ console.log("✅ Extraction complete!");
171
+ console.log(` Output: ${output}`);
172
+ }
173
+ //#endregion
174
+ //#region src/commands/decompile.ts
175
+ const DECOMPILERS = {
176
+ cfr: {
177
+ name: "CFR",
178
+ check: "which cfr-decompiler",
179
+ command: (input, output) => `cfr-decompiler "${input}" --outputdir "${output}"`,
180
+ install: "brew install cfr-decompiler (macOS/Linux) or download from https://www.benf.org/other/cfr/"
181
+ },
182
+ procyon: {
183
+ name: "Procyon",
184
+ check: "which procyon",
185
+ command: (input, output) => `procyon -o "${output}" "${input}"`,
186
+ install: "Download from https://github.com/mstrobel/procyon"
187
+ },
188
+ fernflower: {
189
+ name: "Fernflower",
190
+ check: "test -f fernflower.jar",
191
+ command: (input, output) => `java -jar fernflower.jar "${input}" "${output}"`,
192
+ install: "Find in IntelliJ IDEA: plugins/java-decompiler/lib/java-decompiler.jar"
193
+ }
194
+ };
195
+ /**
196
+ * Check if a decompiler is available
197
+ */
198
+ function isDecompilerAvailable(decompiler) {
199
+ try {
200
+ execSync(DECOMPILERS[decompiler].check, { stdio: "ignore" });
201
+ return true;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+ /**
207
+ * Find available decompiler
208
+ */
209
+ function findDecompiler() {
210
+ for (const key of Object.keys(DECOMPILERS)) if (isDecompilerAvailable(key)) return key;
211
+ return null;
212
+ }
213
+ /**
214
+ * Decompile Java class files or JAR files
215
+ */
216
+ async function decompile(input, options) {
217
+ const { output, decompiler: requestedDecompiler, filter, verbose = false } = options;
218
+ console.log(`🔧 Java Decompilation`);
219
+ console.log(` Input: ${input}`);
220
+ console.log(` Output: ${output}`);
221
+ console.log("");
222
+ const decompiler = requestedDecompiler || findDecompiler();
223
+ if (!decompiler) {
224
+ console.log("❌ No Java decompiler found!");
225
+ console.log("");
226
+ console.log("Install one of these decompilers:");
227
+ for (const [_key, config] of Object.entries(DECOMPILERS)) console.log(` ${config.name}: ${config.install}`);
228
+ process.exit(1);
229
+ }
230
+ if (requestedDecompiler && !isDecompilerAvailable(requestedDecompiler)) {
231
+ console.log(`❌ Requested decompiler '${requestedDecompiler}' is not available.`);
232
+ console.log(` ${DECOMPILERS[requestedDecompiler].install}`);
233
+ process.exit(1);
234
+ }
235
+ const config = DECOMPILERS[decompiler];
236
+ console.log(`📦 Using decompiler: ${config.name}`);
237
+ console.log("");
238
+ ensureDir(output);
239
+ let jarFiles = findFiles(input, "*.jar");
240
+ if (filter && jarFiles.length > 0) {
241
+ const patterns = filter.split(",").map((p) => p.trim());
242
+ jarFiles = jarFiles.filter((jar) => {
243
+ const jarName = basename(jar);
244
+ return patterns.some((pattern) => {
245
+ const escaped = pattern.replaceAll(/[.+?^${}()|[\]\\/]/g, "\\$&");
246
+ return new RegExp(escaped.replaceAll(/\*/g, ".*")).test(jarName);
247
+ });
248
+ });
249
+ console.log(`🎯 Filtered to ${jarFiles.length} JARs matching: ${filter}`);
250
+ }
251
+ if (jarFiles.length > 0) {
252
+ console.log(`🔍 Found ${jarFiles.length} JAR files`);
253
+ console.log("");
254
+ let success = 0;
255
+ let failed = 0;
256
+ for (let i = 0; i < jarFiles.length; i++) {
257
+ const jar = jarFiles[i];
258
+ const jarName = basename(jar, ".jar");
259
+ process.stdout.write(`\r Decompiling ${i + 1}/${jarFiles.length}: ${jarName.slice(0, 50).padEnd(50)}`);
260
+ try {
261
+ execSync(config.command(jar, output), { stdio: "ignore" });
262
+ success++;
263
+ } catch {
264
+ if (verbose) console.error(`\n ❌ Failed: ${jarName}`);
265
+ failed++;
266
+ }
267
+ }
268
+ console.log("\n");
269
+ console.log("✅ Decompilation complete!");
270
+ console.log(` Success: ${success}`);
271
+ console.log(` Failed: ${failed}`);
272
+ console.log(` Output: ${output}`);
273
+ } else {
274
+ const classFiles = findFiles(input, "*.class");
275
+ if (classFiles.length === 0) {
276
+ console.log("❌ No JAR or class files found");
277
+ return;
278
+ }
279
+ console.log(`🔍 Found ${classFiles.length} class files`);
280
+ console.log("");
281
+ let success = 0;
282
+ let failed = 0;
283
+ for (let i = 0; i < classFiles.length; i++) {
284
+ const classFile = classFiles[i];
285
+ if (verbose) process.stdout.write(`\r Decompiling ${i + 1}/${classFiles.length}: ${basename(classFile).slice(0, 50).padEnd(50)}`);
286
+ try {
287
+ execSync(config.command(classFile, output), { stdio: "ignore" });
288
+ success++;
289
+ } catch {
290
+ failed++;
291
+ }
292
+ }
293
+ console.log("\n");
294
+ console.log("✅ Decompilation complete!");
295
+ console.log(` Success: ${success}`);
296
+ console.log(` Failed: ${failed}`);
297
+ console.log(` Output: ${output}`);
298
+ }
299
+ }
300
+ //#endregion
301
+ //#region src/cli.ts
302
+ /**
303
+ * P2 CLI - Eclipse P2 Repository Tools
304
+ *
305
+ * Commands:
306
+ * p2 download <url> Download plugins from P2 repository
307
+ * p2 extract <input> Extract files from JAR archives
308
+ * p2 decompile <input> Decompile Java class files
309
+ */
310
+ const program = new Command();
311
+ const pkg = createRequire(import.meta.url)("../package.json");
312
+ program.name("p2").description("CLI for Eclipse P2 repositories - download, extract, and decompile plugins").version(pkg.version ?? "0.0.0");
313
+ program.command("download <url>").description("Download plugins from a P2 repository").option("-o, --output <dir>", "Output directory", "./p2-download").option("-f, --filter <patterns>", "Filter plugins by ID pattern (comma-separated)", "").option("-e, --extract", "Also extract files after download").option("--extract-output <dir>", "Output directory for extraction").option("--extract-patterns <patterns>", "File patterns to extract (comma-separated, default: all)").action(async (url, opts) => {
314
+ await download(url, {
315
+ output: opts.output,
316
+ filter: opts.filter || void 0,
317
+ extract: opts.extract,
318
+ extractOutput: opts.extractOutput,
319
+ extractPatterns: opts.extractPatterns?.split(",").map((p) => p.trim())
320
+ });
321
+ });
322
+ program.command("extract <input>").description("Extract files from JAR archives (preserves package structure)").option("-o, --output <dir>", "Output directory", "./extracted").option("-p, --patterns <patterns>", "File patterns to extract (comma-separated, default: all)").option("-v, --verbose", "Verbose output").action(async (input, opts) => {
323
+ await extractJars(input, {
324
+ output: opts.output,
325
+ patterns: opts.patterns ? opts.patterns.split(",").map((p) => p.trim()) : void 0,
326
+ verbose: opts.verbose
327
+ });
328
+ });
329
+ program.command("decompile <input>").description("Decompile Java class files or JAR files").option("-o, --output <dir>", "Output directory", "./decompiled").option("-f, --filter <patterns>", "Filter JARs by name pattern (comma-separated)").option("-d, --decompiler <name>", "Decompiler to use (cfr, procyon, fernflower)").option("-v, --verbose", "Verbose output").action(async (input, opts) => {
330
+ await decompile(input, {
331
+ output: opts.output,
332
+ filter: opts.filter,
333
+ decompiler: opts.decompiler,
334
+ verbose: opts.verbose
335
+ });
336
+ });
337
+ program.parse();
338
+ //#endregion
339
+ export {};
package/package.json CHANGED
@@ -1,5 +1,39 @@
1
1
  {
2
2
  "name": "@abapify/p2-cli",
3
- "version": "0.0.0",
4
- "description": "Placeholder — real package is published via CI/CD (trusted publishing)."
3
+ "version": "0.4.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/abapify/adt-cli.git",
7
+ "directory": "tools/p2-cli"
8
+ },
9
+ "homepage": "https://github.com/abapify/adt-cli/tree/main/tools/p2-cli#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/abapify/adt-cli/issues"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "description": "CLI for Eclipse P2 repositories - download, extract, and decompile plugins",
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "bin": {
23
+ "p2": "./dist/cli.mjs"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "keywords": [
30
+ "sap",
31
+ "eclipse",
32
+ "p2",
33
+ "cli",
34
+ "decompiler"
35
+ ],
36
+ "dependencies": {
37
+ "commander": "*"
38
+ }
5
39
  }