sap-adt-cli 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +61 -2
- package/package.json +22 -4
- package/runtime-manifest.json +17 -0
- package/scripts/install.js +226 -0
- package/scripts/run.js +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sap-engineering-skill contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sap-adt-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
npm/npx channel for the self-contained **sap-adt** CLI (SAP ADT REST API:
|
|
4
|
+
read/write ABAP source and metadata, transport request collection).
|
|
5
|
+
|
|
6
|
+
The runtime is a native executable per platform — **no Python, pip or venv
|
|
7
|
+
required**. This npm package itself needs **Node.js ≥ 18** (npx + installer);
|
|
8
|
+
if Node.js is not an option, use the desktop installer / portable archive
|
|
9
|
+
from the [repository releases](https://github.com/shrek-abaper/sap-engineering-skill)
|
|
10
|
+
instead.
|
|
11
|
+
|
|
12
|
+
## Install the runtime (once)
|
|
13
|
+
|
|
14
|
+
The package is named `sap-adt-cli`; its bin is **`sap-adt`**.
|
|
15
|
+
|
|
16
|
+
**Global command (recommended):**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g sap-adt-cli
|
|
20
|
+
sap-adt install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**npx (no global install) — keep the prefix on every call** (npx does not add
|
|
24
|
+
`sap-adt` to PATH):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx sap-adt-cli@1.4.0 install
|
|
28
|
+
npx sap-adt-cli@1.4.0 --version
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`install` is this package's own wizard (not an npm/npx built-in): it matches
|
|
32
|
+
your OS/arch against the version-bound manifest, downloads the matching
|
|
33
|
+
artifact over HTTPS, verifies its SHA-256 (from the manifest bundled with
|
|
34
|
+
the package — never from the download endpoint), extracts safely and swaps
|
|
35
|
+
it in atomically, then self-tests with the absolute install path.
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sap-adt --version
|
|
41
|
+
sap-adt status
|
|
42
|
+
sap-adt configure # interactive; password goes to the OS keystore
|
|
43
|
+
sap-adt credentials doctor # backend diagnostics (no secrets shown)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
With npx, prefix each command: `npx sap-adt-cli@1.4.0 <command>`.
|
|
47
|
+
|
|
48
|
+
Configuration and credentials live in `~/.sap-adt-cli/` and the OS keystore —
|
|
49
|
+
never inside this package. Upgrading the npm package requires re-running
|
|
50
|
+
`install` (the runtime slot lives next to the launcher).
|
|
51
|
+
|
|
52
|
+
## Security model notes
|
|
53
|
+
|
|
54
|
+
- The CLI is read-only by default; write/activate/transport commands need a
|
|
55
|
+
per-profile capability flag, are hard-refused on `environment=prd`, and ask
|
|
56
|
+
for a fresh `[y/N]` confirmation per operation.
|
|
57
|
+
- The installer refuses digest mismatches, unsupported platforms, non-HTTPS
|
|
58
|
+
or unlisted redirect hosts, and archives with path traversal or symlinks.
|
|
59
|
+
- A normal `sap-adt` invocation never downloads or upgrades anything.
|
|
60
|
+
|
|
61
|
+
Package versions track the CLI version exactly; installing version X always
|
|
62
|
+
installs runtime X (no `latest` surprises).
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-adt-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Installer and launcher for the self-contained sap-adt CLI (SAP ADT REST API). No Python required; Node.js >=18 needed for this npm channel only.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "shrek-abaper",
|
|
8
|
+
"url": "https://github.com/shrek-abaper"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/shrek-abaper/sap-engineering-skill",
|
|
11
|
+
"repository": "https://github.com/shrek-abaper/sap-engineering-skill",
|
|
12
|
+
"bin": {
|
|
13
|
+
"sap-adt": "scripts/run.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"scripts/",
|
|
17
|
+
"runtime-manifest.json",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli_version": "1.4.1",
|
|
3
|
+
"release_state": "published",
|
|
4
|
+
"artifacts": {
|
|
5
|
+
"linux-x86_64": {
|
|
6
|
+
"url": "https://github.com/shrek-abaper/sap-engineering-skill/releases/download/sap-adt-v1.4.1/sap-adt-1.4.1-linux-x86_64.tar.gz",
|
|
7
|
+
"sha256": "10e6436369185305ccc6325d0154259cb910eba9275fa9eacd1c150d3009087a",
|
|
8
|
+
"format": "tar.gz",
|
|
9
|
+
"size_bytes": 16558461,
|
|
10
|
+
"allowed_redirect_hosts": [
|
|
11
|
+
"github.com",
|
|
12
|
+
"release-assets.githubusercontent.com",
|
|
13
|
+
"objects.githubusercontent.com"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* sap-adt runtime install wizard (invoked via `sap-adt install`).
|
|
3
|
+
*
|
|
4
|
+
* Flow: platform match -> download over HTTPS -> sha256 verify -> safe
|
|
5
|
+
* extract -> atomic swap -> self-test from the absolute path. The expected
|
|
6
|
+
* digest comes from the version-bound runtime-manifest.json bundled with
|
|
7
|
+
* this package — never from the same endpoint as the payload.
|
|
8
|
+
*
|
|
9
|
+
* Hard rules:
|
|
10
|
+
* - digest mismatch, unsupported platform, download failure, or unsafe
|
|
11
|
+
* archive entries abort WITHOUT touching any existing runtime
|
|
12
|
+
* - redirects stay on HTTPS and must stay on the artifact host or an
|
|
13
|
+
* explicitly allow-listed redirect host in the manifest
|
|
14
|
+
* - extraction rejects absolute paths, ".." traversal, and symlinks
|
|
15
|
+
* - no curl|sh, no shell:true, no string-concatenated commands
|
|
16
|
+
*
|
|
17
|
+
* Test seam: file:// artifact URLs are accepted ONLY when the environment
|
|
18
|
+
* variable SAP_ADT_INSTALL_ALLOW_FILE=1 is set (local packaging tests).
|
|
19
|
+
*/
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
const { spawnSync } = require("node:child_process");
|
|
23
|
+
const crypto = require("node:crypto");
|
|
24
|
+
const fs = require("node:fs");
|
|
25
|
+
const https = require("node:https");
|
|
26
|
+
const os = require("node:os");
|
|
27
|
+
const path = require("node:path");
|
|
28
|
+
const { pipeline } = require("node:stream/promises");
|
|
29
|
+
|
|
30
|
+
const PKG_DIR = path.resolve(__dirname, "..");
|
|
31
|
+
const MAX_REDIRECTS = 3;
|
|
32
|
+
|
|
33
|
+
function fail(message, code = 1) {
|
|
34
|
+
process.stderr.write(`[sap-adt install] ERROR: ${message}\n`);
|
|
35
|
+
process.exit(code);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function info(message) {
|
|
39
|
+
process.stdout.write(`[sap-adt install] ${message}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function platformKey() {
|
|
43
|
+
const osMap = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
44
|
+
const archMap = { x64: "x86_64", arm64: "aarch64" };
|
|
45
|
+
const o = osMap[process.platform];
|
|
46
|
+
const a = archMap[process.arch];
|
|
47
|
+
return o && a ? `${o}-${a}` : `${process.platform}-${process.arch}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadManifest() {
|
|
51
|
+
const file = path.join(PKG_DIR, "runtime-manifest.json");
|
|
52
|
+
if (!fs.existsSync(file)) fail(`runtime-manifest.json missing in ${PKG_DIR}`, 2);
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
fail(`runtime-manifest.json is not valid JSON: ${e.message}`, 2);
|
|
58
|
+
}
|
|
59
|
+
if (!manifest || typeof manifest !== "object" || !manifest.artifacts) {
|
|
60
|
+
fail("runtime-manifest.json has no artifacts section", 2);
|
|
61
|
+
}
|
|
62
|
+
return manifest;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pickArtifact(manifest, key) {
|
|
66
|
+
const entry = manifest.artifacts[key];
|
|
67
|
+
if (!entry) {
|
|
68
|
+
const supported = Object.keys(manifest.artifacts).join(", ") || "(none)";
|
|
69
|
+
fail(`unsupported platform "${key}". Supported: ${supported}`, 1);
|
|
70
|
+
}
|
|
71
|
+
for (const field of ["url", "sha256", "format"]) {
|
|
72
|
+
if (!entry[field]) fail(`manifest entry for ${key} lacks "${field}"`, 2);
|
|
73
|
+
}
|
|
74
|
+
if (!/^[0-9a-f]{64}$/i.test(entry.sha256)) {
|
|
75
|
+
fail(`manifest sha256 for ${key} is malformed`, 2);
|
|
76
|
+
}
|
|
77
|
+
return entry;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function download(url, allowedHosts, dest, redirectsLeft) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const parsed = new URL(url);
|
|
83
|
+
if (parsed.protocol === "file:") {
|
|
84
|
+
if (process.env.SAP_ADT_INSTALL_ALLOW_FILE !== "1") {
|
|
85
|
+
return reject(new Error(
|
|
86
|
+
"file:// artifacts are refused (test-only; set SAP_ADT_INSTALL_ALLOW_FILE=1 to allow)"));
|
|
87
|
+
}
|
|
88
|
+
return fs.promises.copyFile(decodeURIComponent(parsed.pathname), dest)
|
|
89
|
+
.then(resolve, reject);
|
|
90
|
+
}
|
|
91
|
+
if (parsed.protocol !== "https:") {
|
|
92
|
+
return reject(new Error(`refusing non-HTTPS artifact URL: ${url}`));
|
|
93
|
+
}
|
|
94
|
+
if (!allowedHosts.includes(parsed.host)) {
|
|
95
|
+
return reject(new Error(`artifact host not allow-listed: ${parsed.host}`));
|
|
96
|
+
}
|
|
97
|
+
https.get(url, (res) => {
|
|
98
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
99
|
+
res.resume();
|
|
100
|
+
if (redirectsLeft <= 0) return reject(new Error("too many redirects"));
|
|
101
|
+
let next;
|
|
102
|
+
try {
|
|
103
|
+
next = new URL(res.headers.location, url);
|
|
104
|
+
} catch {
|
|
105
|
+
return reject(new Error("invalid redirect location"));
|
|
106
|
+
}
|
|
107
|
+
if (next.protocol !== "https:") {
|
|
108
|
+
return reject(new Error(`refusing redirect to non-HTTPS: ${next}`));
|
|
109
|
+
}
|
|
110
|
+
if (!allowedHosts.includes(next.host)) {
|
|
111
|
+
return reject(new Error(`refusing redirect to non-allow-listed host: ${next.host}`));
|
|
112
|
+
}
|
|
113
|
+
return resolve(download(next.toString(), allowedHosts, dest, redirectsLeft - 1));
|
|
114
|
+
}
|
|
115
|
+
if (res.statusCode !== 200) {
|
|
116
|
+
res.resume();
|
|
117
|
+
return reject(new Error(`download failed: HTTP ${res.statusCode}`));
|
|
118
|
+
}
|
|
119
|
+
pipeline(res, fs.createWriteStream(dest)).then(resolve, reject);
|
|
120
|
+
}).on("error", reject);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sha256File(file) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const hash = crypto.createHash("sha256");
|
|
127
|
+
fs.createReadStream(file)
|
|
128
|
+
.on("data", (d) => hash.update(d))
|
|
129
|
+
.on("end", () => resolve(hash.digest("hex")))
|
|
130
|
+
.on("error", reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Extract with tar (bsdtar on Windows handles .zip too) via an argument
|
|
135
|
+
* array — never a shell. Then walk entries rejecting traversal/symlinks. */
|
|
136
|
+
function extract(archive, format, destDir) {
|
|
137
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
138
|
+
const args = format === "zip"
|
|
139
|
+
? ["-xf", archive, "-C", destDir]
|
|
140
|
+
: ["-xzf", archive, "-C", destDir];
|
|
141
|
+
const result = spawnSync("tar", args, { encoding: "utf8" });
|
|
142
|
+
if (result.error) throw new Error(`tar unavailable: ${result.error.message}`);
|
|
143
|
+
if (result.status !== 0) throw new Error(`tar failed: ${result.stderr.trim()}`);
|
|
144
|
+
|
|
145
|
+
for (const entry of fs.readdirSync(destDir, { recursive: true })) {
|
|
146
|
+
const full = path.join(destDir, entry);
|
|
147
|
+
const stat = fs.lstatSync(full);
|
|
148
|
+
if (stat.isSymbolicLink()) {
|
|
149
|
+
throw new Error(`archive contains a symlink (refused): ${entry}`);
|
|
150
|
+
}
|
|
151
|
+
const rel = path.relative(destDir, full);
|
|
152
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
153
|
+
throw new Error(`archive entry escapes target dir (refused): ${entry}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Swap runtime atomically-ish: stage -> rename old aside -> rename new in ->
|
|
159
|
+
* on any failure, restore the old one and leave it working. */
|
|
160
|
+
function installTree(staging, runtimeDir) {
|
|
161
|
+
const backup = `${runtimeDir}.old-${process.pid}`;
|
|
162
|
+
let movedOld = false;
|
|
163
|
+
try {
|
|
164
|
+
if (fs.existsSync(runtimeDir)) {
|
|
165
|
+
fs.renameSync(runtimeDir, backup);
|
|
166
|
+
movedOld = true;
|
|
167
|
+
}
|
|
168
|
+
fs.renameSync(staging, runtimeDir);
|
|
169
|
+
if (movedOld) fs.rmSync(backup, { recursive: true, force: true });
|
|
170
|
+
} catch (e) {
|
|
171
|
+
fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
172
|
+
if (movedOld) fs.renameSync(backup, runtimeDir);
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function main() {
|
|
178
|
+
const manifest = loadManifest();
|
|
179
|
+
if (manifest.release_state && manifest.release_state !== "published") {
|
|
180
|
+
info(`NOTE: manifest is marked "${manifest.release_state}" — this package is a pre-release candidate.`);
|
|
181
|
+
}
|
|
182
|
+
const key = platformKey();
|
|
183
|
+
const entry = pickArtifact(manifest, key);
|
|
184
|
+
const exeName = process.platform === "win32" ? "sap-adt.exe" : "sap-adt";
|
|
185
|
+
|
|
186
|
+
info(`platform ${key}; fetching ${entry.url}`);
|
|
187
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sap-adt-install-"));
|
|
188
|
+
try {
|
|
189
|
+
const archive = path.join(tmp, `artifact.${entry.format === "zip" ? "zip" : "tar.gz"}`);
|
|
190
|
+
const allowedHosts = [new URL(entry.url).host, ...(entry.allowed_redirect_hosts || [])];
|
|
191
|
+
await download(entry.url, allowedHosts, archive, MAX_REDIRECTS);
|
|
192
|
+
|
|
193
|
+
const actual = await sha256File(archive);
|
|
194
|
+
if (actual.toLowerCase() !== entry.sha256.toLowerCase()) {
|
|
195
|
+
fail(`sha256 mismatch: expected ${entry.sha256}, got ${actual}. Nothing was installed.`, 1);
|
|
196
|
+
}
|
|
197
|
+
info("sha256 verified");
|
|
198
|
+
|
|
199
|
+
const staging = path.join(tmp, "staging");
|
|
200
|
+
extract(archive, entry.format, staging);
|
|
201
|
+
// The archive root is the onedir tree (sap-adt/); normalize to the
|
|
202
|
+
// runtime slot: runtime/<platform>/sap-adt/... containing the exe.
|
|
203
|
+
const treeRoot = fs.existsSync(path.join(staging, "sap-adt", exeName))
|
|
204
|
+
? path.join(staging, "sap-adt")
|
|
205
|
+
: staging;
|
|
206
|
+
if (!fs.existsSync(path.join(treeRoot, exeName))) {
|
|
207
|
+
fail(`archive has no ${exeName} at its root`, 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const runtimeDir = path.join(PKG_DIR, "runtime", key, "sap-adt");
|
|
211
|
+
fs.mkdirSync(path.dirname(runtimeDir), { recursive: true });
|
|
212
|
+
installTree(treeRoot, runtimeDir);
|
|
213
|
+
|
|
214
|
+
const bin = path.join(runtimeDir, exeName);
|
|
215
|
+
const selftest = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
216
|
+
if (selftest.status !== 0) {
|
|
217
|
+
fail(`self-test failed (${bin} --version exited ${selftest.status}). Restored previous runtime if any.`, 1);
|
|
218
|
+
}
|
|
219
|
+
info(`installed ${selftest.stdout.trim() || bin}`);
|
|
220
|
+
info("next: sap-adt configure (interactive; password goes to the OS keystore)");
|
|
221
|
+
} finally {
|
|
222
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main().catch((e) => fail(e.message));
|
package/scripts/run.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* sap-adt launcher (npm bin entry).
|
|
3
|
+
*
|
|
4
|
+
* Dispatches to the installed self-contained runtime. The runtime must have
|
|
5
|
+
* been fetched once by `<bin> install` — a normal invocation NEVER downloads
|
|
6
|
+
* or upgrades anything. Node.js is required only for this launcher path;
|
|
7
|
+
* the runtime itself is a native, Python-free executable.
|
|
8
|
+
*
|
|
9
|
+
* `sap-adt install` is our custom install wizard, not an npx/npm feature.
|
|
10
|
+
*/
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require("node:child_process");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
|
|
17
|
+
const PKG_DIR = path.resolve(__dirname, "..");
|
|
18
|
+
|
|
19
|
+
function platformKey() {
|
|
20
|
+
const osMap = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
21
|
+
const archMap = { x64: "x86_64", arm64: "aarch64" };
|
|
22
|
+
const os = osMap[process.platform];
|
|
23
|
+
const arch = archMap[process.arch];
|
|
24
|
+
return os && arch ? `${os}-${arch}` : `${process.platform}-${process.arch}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runtimeBin() {
|
|
28
|
+
const exe = process.platform === "win32" ? "sap-adt.exe" : "sap-adt";
|
|
29
|
+
return path.join(PKG_DIR, "runtime", platformKey(), "sap-adt", exe);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fail(message, code = 1) {
|
|
33
|
+
process.stderr.write(`${message}\n`);
|
|
34
|
+
process.exit(code);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function main(argv) {
|
|
38
|
+
if (argv[0] === "install") {
|
|
39
|
+
// Custom install wizard (fetch + verify + atomic install of the runtime).
|
|
40
|
+
const result = spawnSync(process.execPath,
|
|
41
|
+
[path.join(__dirname, "install.js"), ...argv.slice(1)],
|
|
42
|
+
{ stdio: "inherit" });
|
|
43
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bin = runtimeBin();
|
|
47
|
+
if (!fs.existsSync(bin)) {
|
|
48
|
+
fail(
|
|
49
|
+
`sap-adt runtime is not installed for ${platformKey()}.\n` +
|
|
50
|
+
`Run the install wizard once: npx sap-adt-cli install\n` +
|
|
51
|
+
`(or: <this-bin> install). Nothing was downloaded or modified.`,
|
|
52
|
+
2,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Argument array, no shell: exit code and both streams pass through.
|
|
57
|
+
const result = spawnSync(bin, argv, { stdio: "inherit" });
|
|
58
|
+
if (result.error) {
|
|
59
|
+
fail(`Failed to launch runtime at ${bin}: ${result.error.message}`, 2);
|
|
60
|
+
}
|
|
61
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main(process.argv.slice(2));
|