@saptools/cf-export 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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +454 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +413 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong Tran
|
|
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
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @saptools/cf-export
|
|
2
|
+
|
|
3
|
+
Export CAP / Cloud Foundry project configuration files from a **running** SAP BTP Cloud Foundry application.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
During development or incident response you often need the exact `package.json`, lockfiles, `.cdsrc.json`, `.npmrc` and a usable `default-env.json` that reflects the live binding environment of a deployed CAP/CF app.
|
|
8
|
+
|
|
9
|
+
This package provides both a CLI and a library to pull those artifacts over `cf ssh` + CF API (V3 env), with explicit support for custom container root paths.
|
|
10
|
+
|
|
11
|
+
## Supported artifacts
|
|
12
|
+
|
|
13
|
+
| File | Source | Notes |
|
|
14
|
+
|--------------------|----------------------------|------------------------------------|
|
|
15
|
+
| `package.json` | cf ssh cat | optional |
|
|
16
|
+
| `package-lock.json`| cf ssh cat | optional |
|
|
17
|
+
| `pnpm-lock.yaml` | cf ssh cat | optional |
|
|
18
|
+
| `.cdsrc.json` | cf ssh cat | optional |
|
|
19
|
+
| `default-env.json` | `cf curl /v3/apps/.../env` | synthesized (VCAP + env vars) |
|
|
20
|
+
| `.npmrc` | cf ssh cat | optional, written 0600 |
|
|
21
|
+
|
|
22
|
+
Missing optional files are skipped gracefully.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @saptools/cf-export
|
|
28
|
+
# or
|
|
29
|
+
pnpm add -g @saptools/cf-export
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js >= 20
|
|
35
|
+
- `cf` CLI installed and in PATH
|
|
36
|
+
- `SAP_EMAIL` and `SAP_PASSWORD` environment variables (for `cf auth`)
|
|
37
|
+
|
|
38
|
+
## CLI
|
|
39
|
+
|
|
40
|
+
Default command is `export`.
|
|
41
|
+
|
|
42
|
+
### Export everything (default)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
saptools-cf-export \
|
|
46
|
+
-r ap10 \
|
|
47
|
+
-o my-org \
|
|
48
|
+
-s dev \
|
|
49
|
+
-a my-cap-app \
|
|
50
|
+
--out ./exported-artifacts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Export with custom remote root (the "root url")
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
saptools-cf-export -r ap10 -o my-org -s dev -a my-cap-app \
|
|
57
|
+
--remote-root /home/vcap/app/srv \
|
|
58
|
+
--out ./out
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Export only specific files
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
saptools-cf-export ... --file package.json --file pnpm-lock.yaml --file default-env.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Valid names: `package.json`, `package-lock.json`, `pnpm-lock.yaml`, `.cdsrc.json`, `default-env.json`, `.npmrc`.
|
|
68
|
+
|
|
69
|
+
## Programmatic usage
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { exportArtifacts, formatExportCompletionMessage } from "@saptools/cf-export";
|
|
73
|
+
|
|
74
|
+
const result = await exportArtifacts({
|
|
75
|
+
target: {
|
|
76
|
+
region: "ap10",
|
|
77
|
+
org: "my-org",
|
|
78
|
+
space: "dev",
|
|
79
|
+
app: "my-cap-app",
|
|
80
|
+
},
|
|
81
|
+
outDir: "./export-dir",
|
|
82
|
+
remoteRoot: "/home/vcap/app", // optional
|
|
83
|
+
// artifacts: ["default-env.json", "pnpm-lock.yaml"], // optional subset
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(formatExportCompletionMessage("my-cap-app", result.writtenFiles, result.skipped));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Environment variables
|
|
90
|
+
|
|
91
|
+
- `SAP_EMAIL`, `SAP_PASSWORD` — required for authentication
|
|
92
|
+
- `CF_EXPORT_CF_HOME` — reuse an existing `CF_HOME` directory instead of creating a temporary one
|
|
93
|
+
- `CF_EXPORT_CF_BIN` — override the `cf` binary (mainly for testing)
|
|
94
|
+
|
|
95
|
+
## How remote root works
|
|
96
|
+
|
|
97
|
+
When `--remote-root` (or `remoteRoot`) is supplied, that prefix is tried first:
|
|
98
|
+
|
|
99
|
+
1. `${remoteRoot}/<file>`
|
|
100
|
+
2. `/home/vcap/app/<file>`
|
|
101
|
+
3. `<file>` (relative)
|
|
102
|
+
|
|
103
|
+
This matches the resolution strategy used by the SAP Tools VS Code "Export" button.
|
|
104
|
+
|
|
105
|
+
## Security
|
|
106
|
+
|
|
107
|
+
- `default-env.json` and `.npmrc` are written with mode `0600`.
|
|
108
|
+
- Temporary `CF_HOME` directories are cleaned up after use.
|
|
109
|
+
- Credentials are never logged.
|
|
110
|
+
|
|
111
|
+
## Development (inside monorepo)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm --filter @saptools/cf-export build
|
|
115
|
+
pnpm --filter @saptools/cf-export typecheck
|
|
116
|
+
pnpm --filter @saptools/cf-export lint
|
|
117
|
+
pnpm --filter @saptools/cf-export test:unit
|
|
118
|
+
pnpm --filter @saptools/cf-export test:e2e:fake
|
|
119
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var ARTIFACT_NAMES = [
|
|
9
|
+
"package.json",
|
|
10
|
+
"package-lock.json",
|
|
11
|
+
"pnpm-lock.yaml",
|
|
12
|
+
".cdsrc.json",
|
|
13
|
+
"default-env.json",
|
|
14
|
+
".npmrc"
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// src/exporter.ts
|
|
18
|
+
import { chmod, mkdir, writeFile } from "fs/promises";
|
|
19
|
+
import { dirname, join as join2, resolve } from "path";
|
|
20
|
+
|
|
21
|
+
// src/cf.ts
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
import { promisify } from "util";
|
|
24
|
+
var execFileAsync = promisify(execFile);
|
|
25
|
+
var MAX_BUFFER = 64 * 1024 * 1024;
|
|
26
|
+
var REMOTE_FILE_SENTINEL = "__SAPTOOLS_CF_EXPORT_FILE_CONTENT__";
|
|
27
|
+
function resolveCfCommand(context) {
|
|
28
|
+
return context?.command ?? process.env["CF_EXPORT_CF_BIN"] ?? "cf";
|
|
29
|
+
}
|
|
30
|
+
function resolveCfEnv(context) {
|
|
31
|
+
const env = context?.env ? { ...process.env, ...context.env } : { ...process.env };
|
|
32
|
+
delete env["SAP_EMAIL"];
|
|
33
|
+
delete env["SAP_PASSWORD"];
|
|
34
|
+
return env;
|
|
35
|
+
}
|
|
36
|
+
function describeCfCommand(args) {
|
|
37
|
+
const [command] = args;
|
|
38
|
+
if (command === void 0) {
|
|
39
|
+
return "cf";
|
|
40
|
+
}
|
|
41
|
+
if (command === "auth") {
|
|
42
|
+
return "cf auth";
|
|
43
|
+
}
|
|
44
|
+
return `cf ${args.join(" ")}`;
|
|
45
|
+
}
|
|
46
|
+
function redactSensitiveValue(detail, value) {
|
|
47
|
+
if (value.length === 0) {
|
|
48
|
+
return detail;
|
|
49
|
+
}
|
|
50
|
+
return detail.split(value).join("[REDACTED]");
|
|
51
|
+
}
|
|
52
|
+
function sanitizeCfErrorDetail(detail, args, sensitiveValues = []) {
|
|
53
|
+
const authArgs = args[0] === "auth" ? args.slice(1) : [];
|
|
54
|
+
const values = [...authArgs, ...sensitiveValues];
|
|
55
|
+
return values.reduce((current, value) => redactSensitiveValue(current, value), detail);
|
|
56
|
+
}
|
|
57
|
+
function errorDetailFrom(err) {
|
|
58
|
+
if (typeof err === "object" && err !== null) {
|
|
59
|
+
const e = err;
|
|
60
|
+
const detail = e.stderr ?? e.message;
|
|
61
|
+
return Buffer.isBuffer(detail) ? detail.toString("utf8") : detail ?? "";
|
|
62
|
+
}
|
|
63
|
+
return String(err);
|
|
64
|
+
}
|
|
65
|
+
function withSensitiveEnv(context, env, sensitiveValues) {
|
|
66
|
+
return {
|
|
67
|
+
...context,
|
|
68
|
+
env: { ...context?.env, ...env },
|
|
69
|
+
sensitiveValues: [...context?.sensitiveValues ?? [], ...sensitiveValues]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function runCf(args, context) {
|
|
73
|
+
const cmd = resolveCfCommand(context);
|
|
74
|
+
const isScript = cmd.endsWith(".mjs") || cmd.endsWith(".js");
|
|
75
|
+
const file = isScript ? "node" : cmd;
|
|
76
|
+
const allArgs = isScript ? [cmd, ...args] : [...args];
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execFileAsync(file, allArgs, {
|
|
79
|
+
env: resolveCfEnv(context),
|
|
80
|
+
maxBuffer: MAX_BUFFER
|
|
81
|
+
});
|
|
82
|
+
return stdout;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const command = describeCfCommand(args);
|
|
85
|
+
const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);
|
|
86
|
+
throw new Error(`${command} failed: ${detail}`, { cause: err });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function cfApi(apiEndpoint, context) {
|
|
90
|
+
await runCf(["api", apiEndpoint], context);
|
|
91
|
+
}
|
|
92
|
+
async function cfAuth(email, password, context) {
|
|
93
|
+
const authContext = withSensitiveEnv(
|
|
94
|
+
context,
|
|
95
|
+
{
|
|
96
|
+
CF_USERNAME: email,
|
|
97
|
+
CF_PASSWORD: password
|
|
98
|
+
},
|
|
99
|
+
[email, password]
|
|
100
|
+
);
|
|
101
|
+
await runCf(["auth"], authContext);
|
|
102
|
+
}
|
|
103
|
+
async function cfTargetSpace(org, space, context) {
|
|
104
|
+
await runCf(["target", "-o", org, "-s", space], context);
|
|
105
|
+
}
|
|
106
|
+
async function cfAppGuid(appName, context) {
|
|
107
|
+
const stdout = await runCf(["app", appName, "--guid"], context);
|
|
108
|
+
const guid = stdout.trim();
|
|
109
|
+
if (guid.length === 0) {
|
|
110
|
+
throw new Error(`CF returned an empty app GUID for "${appName}".`);
|
|
111
|
+
}
|
|
112
|
+
return guid;
|
|
113
|
+
}
|
|
114
|
+
async function cfCurl(path, context) {
|
|
115
|
+
return await runCf(["curl", path], context);
|
|
116
|
+
}
|
|
117
|
+
async function cfSsh(appName, command, context) {
|
|
118
|
+
return await runCf(["ssh", appName, "--disable-pseudo-tty", "-c", command], context);
|
|
119
|
+
}
|
|
120
|
+
function buildRemoteFilePaths(fileName, remoteRoot) {
|
|
121
|
+
const paths = [];
|
|
122
|
+
const normalized = remoteRoot?.trim().replace(/\/+$/, "");
|
|
123
|
+
if (normalized !== void 0 && normalized.length > 0) {
|
|
124
|
+
paths.push(`${normalized}/${fileName}`);
|
|
125
|
+
}
|
|
126
|
+
const fallbacks = [`/home/vcap/app/${fileName}`, fileName];
|
|
127
|
+
for (const fb of fallbacks) {
|
|
128
|
+
if (!paths.includes(fb)) {
|
|
129
|
+
paths.push(fb);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return paths;
|
|
133
|
+
}
|
|
134
|
+
function buildCatCommand(remotePath) {
|
|
135
|
+
const quoted = `'${remotePath.replaceAll("'", "'\\''")}'`;
|
|
136
|
+
return [
|
|
137
|
+
`if [ -f ${quoted} ]; then`,
|
|
138
|
+
`printf '%s\\n' ${quotedForSentinel()};`,
|
|
139
|
+
`cat ${quoted};`,
|
|
140
|
+
"else exit 66; fi"
|
|
141
|
+
].join(" ");
|
|
142
|
+
}
|
|
143
|
+
function quotedForSentinel() {
|
|
144
|
+
const s = REMOTE_FILE_SENTINEL;
|
|
145
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
146
|
+
}
|
|
147
|
+
function parseRemoteFileContent(stdout) {
|
|
148
|
+
const prefix = `${REMOTE_FILE_SENTINEL}
|
|
149
|
+
`;
|
|
150
|
+
if (stdout.startsWith(prefix)) {
|
|
151
|
+
return stdout.slice(prefix.length);
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/default-env.ts
|
|
157
|
+
function isRecord(value) {
|
|
158
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
|
160
|
+
function mergeRecordInto(target, source) {
|
|
161
|
+
if (!isRecord(source)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const [k, v] of Object.entries(source)) {
|
|
165
|
+
target[k] = v;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function buildDefaultEnvPayload(appEnv) {
|
|
169
|
+
const payload = {};
|
|
170
|
+
mergeRecordInto(payload, appEnv["system_env_json"]);
|
|
171
|
+
mergeRecordInto(payload, appEnv["environment_variables"]);
|
|
172
|
+
mergeRecordInto(payload, appEnv["running_env_json"]);
|
|
173
|
+
mergeRecordInto(payload, appEnv["staging_env_json"]);
|
|
174
|
+
if (Object.keys(payload).length === 0) {
|
|
175
|
+
throw new Error("No environment variables found to build default-env.json.");
|
|
176
|
+
}
|
|
177
|
+
return payload;
|
|
178
|
+
}
|
|
179
|
+
async function fetchDefaultEnvJson(options) {
|
|
180
|
+
const guid = await cfAppGuid(options.appName, options.context);
|
|
181
|
+
const encoded = encodeURIComponent(guid);
|
|
182
|
+
const stdout = await cfCurl(`/v3/apps/${encoded}/env`, options.context);
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(stdout);
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error("Unexpected JSON format for CF app environment payload.");
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(parsed)) {
|
|
190
|
+
throw new Error("Unexpected JSON object format for CF app environment payload.");
|
|
191
|
+
}
|
|
192
|
+
const payload = buildDefaultEnvPayload(parsed);
|
|
193
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/remote-paths.ts
|
|
198
|
+
async function fetchRemoteTextFile(options) {
|
|
199
|
+
const paths = buildRemoteFilePaths(options.fileName, options.remoteRoot);
|
|
200
|
+
for (const remotePath of paths) {
|
|
201
|
+
try {
|
|
202
|
+
const cmd = buildCatCommand(remotePath);
|
|
203
|
+
const stdout = await cfSsh(options.appName, cmd, options.context);
|
|
204
|
+
const content = parseRemoteFileContent(stdout);
|
|
205
|
+
if (content !== null) {
|
|
206
|
+
return content;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/session.ts
|
|
215
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
216
|
+
import { tmpdir } from "os";
|
|
217
|
+
import { join } from "path";
|
|
218
|
+
import { resolveApiEndpoint, resolveSessionEnv } from "@saptools/cf-files";
|
|
219
|
+
import { resolveApiEndpoint as resolveApiEndpoint2, resolveSessionEnv as resolveSessionEnv2 } from "@saptools/cf-files";
|
|
220
|
+
var CF_HOME_PREFIX = "saptools-cf-export-";
|
|
221
|
+
function explicitCfHome(context) {
|
|
222
|
+
const fromContext = context?.env?.["CF_HOME"] ?? context?.env?.["CF_EXPORT_CF_HOME"];
|
|
223
|
+
if (fromContext !== void 0 && fromContext !== "") {
|
|
224
|
+
return fromContext;
|
|
225
|
+
}
|
|
226
|
+
const fromProcess = process.env["CF_EXPORT_CF_HOME"];
|
|
227
|
+
return fromProcess === void 0 || fromProcess === "" ? void 0 : fromProcess;
|
|
228
|
+
}
|
|
229
|
+
function buildSessionEnv(context, cfHome) {
|
|
230
|
+
const env = {};
|
|
231
|
+
for (const [key, value] of Object.entries(context?.env ?? {})) {
|
|
232
|
+
if (key !== "SAP_EMAIL" && key !== "SAP_PASSWORD") {
|
|
233
|
+
env[key] = value;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
env["CF_HOME"] = cfHome;
|
|
237
|
+
return env;
|
|
238
|
+
}
|
|
239
|
+
async function createSessionContext(context) {
|
|
240
|
+
const configured = explicitCfHome(context);
|
|
241
|
+
if (configured !== void 0) {
|
|
242
|
+
return {
|
|
243
|
+
context: {
|
|
244
|
+
...context,
|
|
245
|
+
env: buildSessionEnv(context, configured)
|
|
246
|
+
},
|
|
247
|
+
dispose: () => Promise.resolve()
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const cfHome = await mkdtemp(join(tmpdir(), CF_HOME_PREFIX));
|
|
251
|
+
return {
|
|
252
|
+
context: {
|
|
253
|
+
...context,
|
|
254
|
+
env: buildSessionEnv(context, cfHome)
|
|
255
|
+
},
|
|
256
|
+
dispose: async () => {
|
|
257
|
+
await rm(cfHome, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function openCfSession(target, context) {
|
|
262
|
+
const { email, password } = resolveSessionEnv(context?.env);
|
|
263
|
+
const apiEndpoint = resolveApiEndpoint(target.region);
|
|
264
|
+
const session = await createSessionContext(context);
|
|
265
|
+
try {
|
|
266
|
+
await cfApi(apiEndpoint, session.context);
|
|
267
|
+
await cfAuth(email, password, session.context);
|
|
268
|
+
await cfTargetSpace(target.org, target.space, session.context);
|
|
269
|
+
return session;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
await session.dispose();
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/exporter.ts
|
|
277
|
+
function resolveAllArtifacts() {
|
|
278
|
+
return [...ARTIFACT_NAMES];
|
|
279
|
+
}
|
|
280
|
+
function normalizeRequested(requested) {
|
|
281
|
+
if (!requested || requested.length === 0) {
|
|
282
|
+
return resolveAllArtifacts();
|
|
283
|
+
}
|
|
284
|
+
const seen = /* @__PURE__ */ new Set();
|
|
285
|
+
const out = [];
|
|
286
|
+
for (const name of requested) {
|
|
287
|
+
if (ARTIFACT_NAMES.includes(name) && !seen.has(name)) {
|
|
288
|
+
seen.add(name);
|
|
289
|
+
out.push(name);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
async function writeArtifact(outDir, fileName, content) {
|
|
295
|
+
const outPath = resolve(join2(outDir, fileName));
|
|
296
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
297
|
+
const isSensitive = fileName === "default-env.json" || fileName === ".npmrc";
|
|
298
|
+
await writeFile(outPath, content, { encoding: "utf8" });
|
|
299
|
+
if (isSensitive) {
|
|
300
|
+
await chmod(outPath, 384);
|
|
301
|
+
}
|
|
302
|
+
return outPath;
|
|
303
|
+
}
|
|
304
|
+
async function exportArtifacts(options) {
|
|
305
|
+
const requested = options.artifacts;
|
|
306
|
+
if (requested?.length === 0) {
|
|
307
|
+
throw new Error("At least one artifact must be selected for export.");
|
|
308
|
+
}
|
|
309
|
+
const artifacts = normalizeRequested(requested);
|
|
310
|
+
const session = await openCfSession(options.target);
|
|
311
|
+
const written = [];
|
|
312
|
+
const skipped = [];
|
|
313
|
+
try {
|
|
314
|
+
for (const name of artifacts) {
|
|
315
|
+
if (name === "default-env.json") {
|
|
316
|
+
try {
|
|
317
|
+
const json = await fetchDefaultEnvJson({
|
|
318
|
+
appName: options.target.app,
|
|
319
|
+
context: session.context
|
|
320
|
+
});
|
|
321
|
+
const path2 = await writeArtifact(options.outDir, name, json);
|
|
322
|
+
written.push(path2);
|
|
323
|
+
} catch {
|
|
324
|
+
skipped.push(name);
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const remoteRoot = options.remoteRoot;
|
|
329
|
+
const fetchOpts = {
|
|
330
|
+
appName: options.target.app,
|
|
331
|
+
fileName: name,
|
|
332
|
+
context: session.context,
|
|
333
|
+
...typeof remoteRoot === "string" ? { remoteRoot } : {}
|
|
334
|
+
};
|
|
335
|
+
const content = await fetchRemoteTextFile(fetchOpts);
|
|
336
|
+
if (content === null) {
|
|
337
|
+
skipped.push(name);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const path = await writeArtifact(options.outDir, name, content);
|
|
341
|
+
written.push(path);
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
await session.dispose();
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
writtenFiles: written,
|
|
348
|
+
skipped
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/format.ts
|
|
353
|
+
function formatExportCompletionMessage(appName, writtenFiles, skipped) {
|
|
354
|
+
const names = writtenFiles.map((p) => {
|
|
355
|
+
const parts = p.split(/[\\/]/);
|
|
356
|
+
return parts[parts.length - 1] ?? p;
|
|
357
|
+
});
|
|
358
|
+
const base = `Export completed for "${appName}".`;
|
|
359
|
+
if (names.length === 0) {
|
|
360
|
+
return `${base} No files written.`;
|
|
361
|
+
}
|
|
362
|
+
const filesPart = `${String(names.length)} file${names.length === 1 ? "" : "s"}: ${names.join(", ")}`;
|
|
363
|
+
if (skipped.length === 0) {
|
|
364
|
+
return `${base} ${filesPart}.`;
|
|
365
|
+
}
|
|
366
|
+
const skipPart = `Skipped: ${skipped.join(", ")}`;
|
|
367
|
+
return `${base} ${filesPart}. ${skipPart}.`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/cli.ts
|
|
371
|
+
function requireFlag(value, name) {
|
|
372
|
+
if (value === void 0 || value === "") {
|
|
373
|
+
process2.stderr.write(`Error: --${name} is required
|
|
374
|
+
`);
|
|
375
|
+
process2.exit(1);
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
function buildTarget(flags) {
|
|
380
|
+
return {
|
|
381
|
+
region: requireFlag(flags.region, "region"),
|
|
382
|
+
org: requireFlag(flags.org, "org"),
|
|
383
|
+
space: requireFlag(flags.space, "space"),
|
|
384
|
+
app: requireFlag(flags.app, "app")
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function addTargetOptions(cmd) {
|
|
388
|
+
return cmd.requiredOption("-r, --region <key>", "CF region key (e.g. ap10)").requiredOption("-o, --org <name>", "CF org name").requiredOption("-s, --space <name>", "CF space name").requiredOption("-a, --app <name>", "CF app name");
|
|
389
|
+
}
|
|
390
|
+
function parseArtifactList(files) {
|
|
391
|
+
if (!files || files.length === 0) {
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
const result = [];
|
|
395
|
+
for (const chunk of files) {
|
|
396
|
+
const parts = chunk.split(/[,\s]+/).filter((p) => p.length > 0);
|
|
397
|
+
for (const p of parts) {
|
|
398
|
+
if (ARTIFACT_NAMES.includes(p)) {
|
|
399
|
+
result.push(p);
|
|
400
|
+
} else {
|
|
401
|
+
process2.stderr.write(`Error: unknown artifact "${p}". Valid: ${ARTIFACT_NAMES.join(", ")}
|
|
402
|
+
`);
|
|
403
|
+
process2.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return result.length > 0 ? result : void 0;
|
|
408
|
+
}
|
|
409
|
+
async function main(argv) {
|
|
410
|
+
const program = new Command();
|
|
411
|
+
program.name("saptools-cf-export").description(
|
|
412
|
+
"Export project artifacts (package.json, lockfiles, .cdsrc.json, default-env.json, .npmrc) from a running CF app"
|
|
413
|
+
);
|
|
414
|
+
addTargetOptions(
|
|
415
|
+
program.command("export", { isDefault: true }).description("Export artifacts from the target CF app (default command)")
|
|
416
|
+
).option("--out <dir>", "Output directory (default: current working directory)").option("--remote-root <path>", "Hint for the base directory inside the container containing the files").option(
|
|
417
|
+
"--file <name>",
|
|
418
|
+
"Artifact to export (repeatable). Omit to export all. Example: --file package.json --file pnpm-lock.yaml",
|
|
419
|
+
(val, prev) => [...prev ?? [], val],
|
|
420
|
+
[]
|
|
421
|
+
).option("--all", "Export all supported artifacts (default behavior)", false).action(async (opts) => {
|
|
422
|
+
const target = buildTarget(opts);
|
|
423
|
+
const outDir = opts.out && opts.out.length > 0 ? opts.out : process2.cwd();
|
|
424
|
+
const remoteRoot = opts.remoteRoot && opts.remoteRoot.trim().length > 0 ? opts.remoteRoot.trim() : void 0;
|
|
425
|
+
const explicitFiles = parseArtifactList(opts.file);
|
|
426
|
+
const artifacts = opts.all || !explicitFiles ? void 0 : explicitFiles;
|
|
427
|
+
const result = await exportArtifacts({
|
|
428
|
+
target,
|
|
429
|
+
outDir,
|
|
430
|
+
...remoteRoot ? { remoteRoot } : {},
|
|
431
|
+
...artifacts ? { artifacts } : {}
|
|
432
|
+
});
|
|
433
|
+
const msg = formatExportCompletionMessage(target.app, result.writtenFiles, result.skipped);
|
|
434
|
+
process2.stdout.write(`${msg}
|
|
435
|
+
`);
|
|
436
|
+
if (result.skipped.length > 0) {
|
|
437
|
+
process2.stdout.write(`Skipped (not found): ${result.skipped.join(", ")}
|
|
438
|
+
`);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
await program.parseAsync([...argv]);
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await main(process2.argv);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
447
|
+
process2.stderr.write(`Error: ${msg}
|
|
448
|
+
`);
|
|
449
|
+
process2.exit(1);
|
|
450
|
+
}
|
|
451
|
+
export {
|
|
452
|
+
main
|
|
453
|
+
};
|
|
454
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/types.ts","../src/exporter.ts","../src/cf.ts","../src/default-env.ts","../src/remote-paths.ts","../src/session.ts","../src/format.ts"],"sourcesContent":["import process from \"node:process\";\n\nimport { Command } from \"commander\";\n\nimport { ARTIFACT_NAMES, exportArtifacts, formatExportCompletionMessage, type ArtifactName, type CfTarget } from \"./index.js\";\n\ninterface TargetFlags {\n readonly region?: string;\n readonly org?: string;\n readonly space?: string;\n readonly app?: string;\n}\n\ninterface ExportFlags extends TargetFlags {\n readonly out?: string;\n readonly remoteRoot?: string;\n readonly file?: string[];\n readonly all?: boolean;\n}\n\nfunction requireFlag(value: string | undefined, name: string): string {\n if (value === undefined || value === \"\") {\n process.stderr.write(`Error: --${name} is required\\n`);\n process.exit(1);\n }\n return value;\n}\n\nfunction buildTarget(flags: TargetFlags): CfTarget {\n return {\n region: requireFlag(flags.region, \"region\"),\n org: requireFlag(flags.org, \"org\"),\n space: requireFlag(flags.space, \"space\"),\n app: requireFlag(flags.app, \"app\"),\n };\n}\n\nfunction addTargetOptions(cmd: Command): Command {\n return cmd\n .requiredOption(\"-r, --region <key>\", \"CF region key (e.g. ap10)\")\n .requiredOption(\"-o, --org <name>\", \"CF org name\")\n .requiredOption(\"-s, --space <name>\", \"CF space name\")\n .requiredOption(\"-a, --app <name>\", \"CF app name\");\n}\n\nfunction parseArtifactList(files: string[] | undefined): readonly ArtifactName[] | undefined {\n if (!files || files.length === 0) {\n return undefined;\n }\n const result: ArtifactName[] = [];\n for (const chunk of files) {\n const parts = chunk.split(/[,\\s]+/).filter((p) => p.length > 0);\n for (const p of parts) {\n if ((ARTIFACT_NAMES as readonly string[]).includes(p)) {\n result.push(p as ArtifactName);\n } else {\n process.stderr.write(`Error: unknown artifact \"${p}\". Valid: ${ARTIFACT_NAMES.join(\", \")}\\n`);\n process.exit(1);\n }\n }\n }\n return result.length > 0 ? result : undefined;\n}\n\nexport async function main(argv: readonly string[]): Promise<void> {\n const program = new Command();\n\n program\n .name(\"saptools-cf-export\")\n .description(\n \"Export project artifacts (package.json, lockfiles, .cdsrc.json, default-env.json, .npmrc) from a running CF app\",\n );\n\n addTargetOptions(\n program\n .command(\"export\", { isDefault: true })\n .description(\"Export artifacts from the target CF app (default command)\"),\n )\n .option(\"--out <dir>\", \"Output directory (default: current working directory)\")\n .option(\"--remote-root <path>\", \"Hint for the base directory inside the container containing the files\")\n .option(\n \"--file <name>\",\n \"Artifact to export (repeatable). Omit to export all. Example: --file package.json --file pnpm-lock.yaml\",\n (val: string, prev: string[] | undefined) => [...(prev ?? []), val],\n [] as string[],\n )\n .option(\"--all\", \"Export all supported artifacts (default behavior)\", false)\n .action(async (opts: ExportFlags): Promise<void> => {\n const target = buildTarget(opts);\n const outDir = opts.out && opts.out.length > 0 ? opts.out : process.cwd();\n const remoteRoot = opts.remoteRoot && opts.remoteRoot.trim().length > 0 ? opts.remoteRoot.trim() : undefined;\n\n const explicitFiles = parseArtifactList(opts.file);\n const artifacts = opts.all || !explicitFiles ? undefined : explicitFiles;\n\n const result = await exportArtifacts({\n target,\n outDir,\n ...(remoteRoot ? { remoteRoot } : {}),\n ...(artifacts ? { artifacts } : {}),\n });\n\n const msg = formatExportCompletionMessage(target.app, result.writtenFiles, result.skipped);\n process.stdout.write(`${msg}\\n`);\n if (result.skipped.length > 0) {\n process.stdout.write(`Skipped (not found): ${result.skipped.join(\", \")}\\n`);\n }\n });\n\n await program.parseAsync([...argv]);\n}\n\ntry {\n await main(process.argv);\n} catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n process.stderr.write(`Error: ${msg}\\n`);\n process.exit(1);\n}\n","export interface CfTarget {\n readonly region: string;\n readonly org: string;\n readonly space: string;\n readonly app: string;\n}\n\nexport const ARTIFACT_NAMES = [\n \"package.json\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \".cdsrc.json\",\n \"default-env.json\",\n \".npmrc\",\n] as const;\n\nexport type ArtifactName = (typeof ARTIFACT_NAMES)[number];\n\nexport interface ExportArtifactsOptions {\n readonly target: CfTarget;\n readonly outDir: string;\n readonly remoteRoot?: string;\n /**\n * Subset of artifacts to export. When omitted or empty, all supported artifacts are attempted.\n */\n readonly artifacts?: readonly ArtifactName[];\n}\n\nexport interface ExportArtifactsResult {\n readonly writtenFiles: readonly string[];\n readonly skipped: readonly string[];\n}\n\nexport interface CfExecContext {\n readonly command?: string;\n readonly env?: NodeJS.ProcessEnv;\n readonly sensitiveValues?: readonly string[];\n}\n","import { chmod, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\n\nimport { fetchDefaultEnvJson } from \"./default-env.js\";\nimport { fetchRemoteTextFile } from \"./remote-paths.js\";\nimport { openCfSession } from \"./session.js\";\nimport type { OpenCfSession } from \"./session.js\";\nimport { ARTIFACT_NAMES, type ArtifactName, type CfExecContext, type ExportArtifactsOptions, type ExportArtifactsResult } from \"./types.js\";\n\nfunction resolveAllArtifacts(): readonly ArtifactName[] {\n return [...ARTIFACT_NAMES];\n}\n\nfunction normalizeRequested(requested: readonly ArtifactName[] | undefined): readonly ArtifactName[] {\n if (!requested || requested.length === 0) {\n return resolveAllArtifacts();\n }\n // Deduplicate while preserving order of first occurrence\n const seen = new Set<string>();\n const out: ArtifactName[] = [];\n for (const name of requested) {\n if (ARTIFACT_NAMES.includes(name) && !seen.has(name)) {\n seen.add(name);\n out.push(name);\n }\n }\n return out;\n}\n\nasync function writeArtifact(outDir: string, fileName: string, content: string): Promise<string> {\n const outPath = resolve(join(outDir, fileName));\n await mkdir(dirname(outPath), { recursive: true });\n const isSensitive = fileName === \"default-env.json\" || fileName === \".npmrc\";\n await writeFile(outPath, content, { encoding: \"utf8\" });\n if (isSensitive) {\n await chmod(outPath, 0o600);\n }\n return outPath;\n}\n\nexport async function exportArtifacts(\n options: ExportArtifactsOptions,\n): Promise<ExportArtifactsResult> {\n const requested = options.artifacts;\n if (requested?.length === 0) {\n throw new Error(\"At least one artifact must be selected for export.\");\n }\n const artifacts = normalizeRequested(requested);\n\n const session: OpenCfSession = await openCfSession(options.target);\n const written: string[] = [];\n const skipped: string[] = [];\n\n try {\n for (const name of artifacts) {\n if (name === \"default-env.json\") {\n try {\n const json = await fetchDefaultEnvJson({\n appName: options.target.app,\n context: session.context,\n });\n const path = await writeArtifact(options.outDir, name, json);\n written.push(path);\n } catch {\n // Treat default-env as best-effort too (consistent with \"nếu có\" for all artifacts in default \"all\" mode).\n // Only the presence of the file/VCAP in the remote app determines if it can be exported.\n skipped.push(name);\n }\n continue;\n }\n\n // regular file via ssh\n const remoteRoot = options.remoteRoot;\n const fetchOpts: {\n readonly appName: string;\n readonly fileName: string;\n readonly remoteRoot?: string | undefined;\n readonly context?: CfExecContext;\n } = {\n appName: options.target.app,\n fileName: name,\n context: session.context,\n ...(typeof remoteRoot === \"string\" ? { remoteRoot } : {}),\n };\n const content = await fetchRemoteTextFile(fetchOpts);\n\n if (content === null) {\n skipped.push(name);\n continue;\n }\n\n const path = await writeArtifact(options.outDir, name, content);\n written.push(path);\n }\n } finally {\n await session.dispose();\n }\n\n return {\n writtenFiles: written,\n skipped,\n };\n}\n\nexport function getAllArtifactNames(): readonly ArtifactName[] {\n return resolveAllArtifacts();\n}\n","import { execFile, type ExecFileOptionsWithBufferEncoding } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nimport type { CfExecContext } from \"./types.js\";\n\nconst execFileAsync = promisify(execFile);\n\nconst MAX_BUFFER = 64 * 1024 * 1024;\n\nconst REMOTE_FILE_SENTINEL = \"__SAPTOOLS_CF_EXPORT_FILE_CONTENT__\";\n\nfunction resolveCfCommand(context?: CfExecContext): string {\n return context?.command ?? process.env[\"CF_EXPORT_CF_BIN\"] ?? \"cf\";\n}\n\nfunction resolveCfEnv(context?: CfExecContext): NodeJS.ProcessEnv {\n const env = context?.env ? { ...process.env, ...context.env } : { ...process.env };\n delete env[\"SAP_EMAIL\"];\n delete env[\"SAP_PASSWORD\"];\n return env;\n}\n\nfunction describeCfCommand(args: readonly string[]): string {\n const [command] = args;\n if (command === undefined) {\n return \"cf\";\n }\n if (command === \"auth\") {\n return \"cf auth\";\n }\n return `cf ${args.join(\" \")}`;\n}\n\nfunction redactSensitiveValue(detail: string, value: string): string {\n if (value.length === 0) {\n return detail;\n }\n return detail.split(value).join(\"[REDACTED]\");\n}\n\nfunction sanitizeCfErrorDetail(\n detail: string,\n args: readonly string[],\n sensitiveValues: readonly string[] = [],\n): string {\n const authArgs = args[0] === \"auth\" ? args.slice(1) : [];\n const values = [...authArgs, ...sensitiveValues];\n return values.reduce((current, value) => redactSensitiveValue(current, value), detail);\n}\n\nfunction errorDetailFrom(err: unknown): string {\n if (typeof err === \"object\" && err !== null) {\n const e = err as { stderr?: Buffer | string; message?: string };\n const detail = e.stderr ?? e.message;\n return Buffer.isBuffer(detail) ? detail.toString(\"utf8\") : (detail ?? \"\");\n }\n return String(err);\n}\n\nfunction withSensitiveEnv(\n context: CfExecContext | undefined,\n env: NodeJS.ProcessEnv,\n sensitiveValues: readonly string[],\n): CfExecContext {\n return {\n ...context,\n env: { ...context?.env, ...env },\n sensitiveValues: [...(context?.sensitiveValues ?? []), ...sensitiveValues],\n };\n}\n\nasync function runCf(args: readonly string[], context?: CfExecContext): Promise<string> {\n const cmd = resolveCfCommand(context);\n const isScript = cmd.endsWith(\".mjs\") || cmd.endsWith(\".js\");\n const file = isScript ? \"node\" : cmd;\n const allArgs = isScript ? [cmd, ...args] : [...args];\n try {\n const { stdout } = await execFileAsync(file, allArgs, {\n env: resolveCfEnv(context),\n maxBuffer: MAX_BUFFER,\n });\n return stdout;\n } catch (err) {\n const command = describeCfCommand(args);\n const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);\n throw new Error(`${command} failed: ${detail}`, { cause: err });\n }\n}\n\nasync function runCfBuffer(\n args: readonly string[],\n context?: CfExecContext,\n): Promise<Buffer> {\n const cmd = resolveCfCommand(context);\n const isScript = cmd.endsWith(\".mjs\") || cmd.endsWith(\".js\");\n const file = isScript ? \"node\" : cmd;\n const allArgs = isScript ? [cmd, ...args] : [...args];\n const options: ExecFileOptionsWithBufferEncoding = {\n env: resolveCfEnv(context),\n maxBuffer: MAX_BUFFER,\n encoding: \"buffer\",\n };\n try {\n const { stdout } = await execFileAsync(file, allArgs, options);\n return Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);\n } catch (err) {\n const command = describeCfCommand(args);\n const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);\n throw new Error(`${command} failed: ${detail}`, { cause: err });\n }\n}\n\nexport async function cfApi(apiEndpoint: string, context?: CfExecContext): Promise<void> {\n await runCf([\"api\", apiEndpoint], context);\n}\n\nexport async function cfAuth(\n email: string,\n password: string,\n context?: CfExecContext,\n): Promise<void> {\n const authContext = withSensitiveEnv(\n context,\n {\n CF_USERNAME: email,\n CF_PASSWORD: password,\n },\n [email, password],\n );\n await runCf([\"auth\"], authContext);\n}\n\nexport async function cfTargetSpace(\n org: string,\n space: string,\n context?: CfExecContext,\n): Promise<void> {\n await runCf([\"target\", \"-o\", org, \"-s\", space], context);\n}\n\nexport async function cfAppGuid(appName: string, context?: CfExecContext): Promise<string> {\n const stdout = await runCf([\"app\", appName, \"--guid\"], context);\n const guid = stdout.trim();\n if (guid.length === 0) {\n throw new Error(`CF returned an empty app GUID for \"${appName}\".`);\n }\n return guid;\n}\n\nexport async function cfCurl(path: string, context?: CfExecContext): Promise<string> {\n return await runCf([\"curl\", path], context);\n}\n\nexport async function cfSsh(\n appName: string,\n command: string,\n context?: CfExecContext,\n): Promise<string> {\n return await runCf([\"ssh\", appName, \"--disable-pseudo-tty\", \"-c\", command], context);\n}\n\nexport async function cfSshBuffer(\n appName: string,\n command: string,\n context?: CfExecContext,\n): Promise<Buffer> {\n return await runCfBuffer([\"ssh\", appName, \"--disable-pseudo-tty\", \"-c\", command], context);\n}\n\nexport function buildRemoteFilePaths(fileName: string, remoteRoot: string | undefined): readonly string[] {\n const paths: string[] = [];\n const normalized = remoteRoot?.trim().replace(/\\/+$/, \"\");\n if (normalized !== undefined && normalized.length > 0) {\n paths.push(`${normalized}/${fileName}`);\n }\n const fallbacks = [`/home/vcap/app/${fileName}`, fileName];\n for (const fb of fallbacks) {\n if (!paths.includes(fb)) {\n paths.push(fb);\n }\n }\n return paths;\n}\n\nexport function buildCatCommand(remotePath: string): string {\n const quoted = `'${remotePath.replaceAll(\"'\", \"'\\\\''\")}'`;\n return [\n `if [ -f ${quoted} ]; then`,\n `printf '%s\\\\n' ${quotedForSentinel()};`,\n `cat ${quoted};`,\n \"else exit 66; fi\",\n ].join(\" \");\n}\n\nfunction quotedForSentinel(): string {\n // Sentinel is a constant known only to us; no user content can start with it after trim.\n const s = REMOTE_FILE_SENTINEL;\n return `'${s.replaceAll(\"'\", \"'\\\\''\")}'`;\n}\n\nexport const REMOTE_CONTENT_SENTINEL = REMOTE_FILE_SENTINEL;\n\nexport function parseRemoteFileContent(stdout: string): string | null {\n const prefix = `${REMOTE_FILE_SENTINEL}\\n`;\n if (stdout.startsWith(prefix)) {\n return stdout.slice(prefix.length);\n }\n return null;\n}\n\nexport const internals = {\n runCf,\n describeCfCommand,\n sanitizeCfErrorDetail,\n};\n","import { cfAppGuid, cfCurl } from \"./cf.js\";\nimport type { CfExecContext } from \"./types.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction mergeRecordInto(target: Record<string, unknown>, source: unknown): void {\n if (!isRecord(source)) {\n return;\n }\n for (const [k, v] of Object.entries(source)) {\n target[k] = v;\n }\n}\n\nfunction buildDefaultEnvPayload(appEnv: Record<string, unknown>): Record<string, unknown> {\n const payload: Record<string, unknown> = {};\n mergeRecordInto(payload, appEnv[\"system_env_json\"]);\n mergeRecordInto(payload, appEnv[\"environment_variables\"]);\n mergeRecordInto(payload, appEnv[\"running_env_json\"]);\n mergeRecordInto(payload, appEnv[\"staging_env_json\"]);\n\n if (Object.keys(payload).length === 0) {\n throw new Error(\"No environment variables found to build default-env.json.\");\n }\n return payload;\n}\n\nexport interface FetchDefaultEnvOptions {\n readonly appName: string;\n readonly context?: CfExecContext;\n}\n\nexport async function fetchDefaultEnvJson(options: FetchDefaultEnvOptions): Promise<string> {\n const guid = await cfAppGuid(options.appName, options.context);\n const encoded = encodeURIComponent(guid);\n const stdout = await cfCurl(`/v3/apps/${encoded}/env`, options.context);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n throw new Error(\"Unexpected JSON format for CF app environment payload.\");\n }\n\n if (!isRecord(parsed)) {\n throw new Error(\"Unexpected JSON object format for CF app environment payload.\");\n }\n\n const payload = buildDefaultEnvPayload(parsed);\n return `${JSON.stringify(payload, null, 2)}\\n`;\n}\n","import { cfSsh, buildRemoteFilePaths, parseRemoteFileContent, REMOTE_CONTENT_SENTINEL, buildCatCommand } from \"./cf.js\";\nimport type { CfExecContext } from \"./types.js\";\n\nexport interface FetchRemoteTextOptions {\n readonly appName: string;\n readonly fileName: string;\n readonly remoteRoot?: string | undefined;\n readonly context?: CfExecContext;\n}\n\n/**\n * Fetch a text file from the CF app container using cf ssh.\n * Tries remoteRoot (if provided) first, then standard fallbacks.\n * Returns null when the file does not exist in any candidate location.\n */\nexport async function fetchRemoteTextFile(options: FetchRemoteTextOptions): Promise<string | null> {\n const paths = buildRemoteFilePaths(options.fileName, options.remoteRoot);\n\n for (const remotePath of paths) {\n try {\n const cmd = buildCatCommand(remotePath);\n const stdout = await cfSsh(options.appName, cmd, options.context);\n const content = parseRemoteFileContent(stdout);\n if (content !== null) {\n return content;\n }\n } catch {\n // 66 exit or ssh failure for this path → try next\n }\n }\n\n return null;\n}\n\nexport { buildRemoteFilePaths, buildCatCommand, parseRemoteFileContent, REMOTE_CONTENT_SENTINEL };\n","import { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { resolveApiEndpoint, resolveSessionEnv } from \"@saptools/cf-files\";\nimport { cfApi, cfAuth, cfTargetSpace } from \"./cf.js\";\nimport type { CfExecContext, CfTarget } from \"./types.js\";\n\n// Delegate pure resolve helpers to @saptools/cf-files (shared, less duplication for non-exec parts)\nexport { resolveApiEndpoint, resolveSessionEnv } from \"@saptools/cf-files\";\n\n// Local openCfSession still uses our custom CF_EXPORT_* envs and prefix while calling the shared cf* functions.\n\nexport interface SessionEnv {\n readonly email: string;\n readonly password: string;\n}\n\nexport interface OpenCfSession {\n readonly context: CfExecContext;\n readonly dispose: () => Promise<void>;\n}\n\nconst CF_HOME_PREFIX = \"saptools-cf-export-\";\n\n// resolveSessionEnv and resolveApiEndpoint are re-exported from @saptools/cf-files above\n// (keeps implementation in one place, reduces duplication).\n\nfunction explicitCfHome(context?: CfExecContext): string | undefined {\n const fromContext =\n context?.env?.[\"CF_HOME\"] ?? context?.env?.[\"CF_EXPORT_CF_HOME\"];\n if (fromContext !== undefined && fromContext !== \"\") {\n return fromContext;\n }\n const fromProcess = process.env[\"CF_EXPORT_CF_HOME\"];\n return fromProcess === undefined || fromProcess === \"\" ? undefined : fromProcess;\n}\n\nfunction buildSessionEnv(context: CfExecContext | undefined, cfHome: string): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const [key, value] of Object.entries(context?.env ?? {})) {\n if (key !== \"SAP_EMAIL\" && key !== \"SAP_PASSWORD\") {\n env[key] = value;\n }\n }\n env[\"CF_HOME\"] = cfHome;\n return env;\n}\n\nasync function createSessionContext(context?: CfExecContext): Promise<OpenCfSession> {\n const configured = explicitCfHome(context);\n if (configured !== undefined) {\n return {\n context: {\n ...context,\n env: buildSessionEnv(context, configured),\n },\n dispose: (): Promise<void> => Promise.resolve(),\n };\n }\n\n const cfHome = await mkdtemp(join(tmpdir(), CF_HOME_PREFIX));\n return {\n context: {\n ...context,\n env: buildSessionEnv(context, cfHome),\n },\n dispose: async (): Promise<void> => {\n await rm(cfHome, { recursive: true, force: true });\n },\n };\n}\n\nexport async function openCfSession(\n target: CfTarget,\n context?: CfExecContext,\n): Promise<OpenCfSession> {\n const { email, password } = resolveSessionEnv(context?.env);\n const apiEndpoint = resolveApiEndpoint(target.region);\n const session = await createSessionContext(context);\n\n try {\n await cfApi(apiEndpoint, session.context);\n await cfAuth(email, password, session.context);\n await cfTargetSpace(target.org, target.space, session.context);\n return session;\n } catch (err) {\n await session.dispose();\n throw err;\n }\n}\n","\n\nexport function formatExportCompletionMessage(\n appName: string,\n writtenFiles: readonly string[],\n skipped: readonly string[],\n): string {\n const names = writtenFiles.map((p) => {\n const parts = p.split(/[\\\\/]/);\n return parts[parts.length - 1] ?? p;\n });\n\n const base = `Export completed for \"${appName}\".`;\n if (names.length === 0) {\n return `${base} No files written.`;\n }\n\n const filesPart = `${String(names.length)} file${names.length === 1 ? \"\" : \"s\"}: ${names.join(\", \")}`;\n if (skipped.length === 0) {\n return `${base} ${filesPart}.`;\n }\n const skipPart = `Skipped: ${skipped.join(\", \")}`;\n return `${base} ${filesPart}. ${skipPart}.`;\n}\n"],"mappings":"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","names":["process","join","resolveApiEndpoint","resolveSessionEnv","join","path","process"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export { resolveApiEndpoint, resolveSessionEnv } from '@saptools/cf-files';
|
|
2
|
+
|
|
3
|
+
interface CfTarget {
|
|
4
|
+
readonly region: string;
|
|
5
|
+
readonly org: string;
|
|
6
|
+
readonly space: string;
|
|
7
|
+
readonly app: string;
|
|
8
|
+
}
|
|
9
|
+
declare const ARTIFACT_NAMES: readonly ["package.json", "package-lock.json", "pnpm-lock.yaml", ".cdsrc.json", "default-env.json", ".npmrc"];
|
|
10
|
+
type ArtifactName = (typeof ARTIFACT_NAMES)[number];
|
|
11
|
+
interface ExportArtifactsOptions {
|
|
12
|
+
readonly target: CfTarget;
|
|
13
|
+
readonly outDir: string;
|
|
14
|
+
readonly remoteRoot?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Subset of artifacts to export. When omitted or empty, all supported artifacts are attempted.
|
|
17
|
+
*/
|
|
18
|
+
readonly artifacts?: readonly ArtifactName[];
|
|
19
|
+
}
|
|
20
|
+
interface ExportArtifactsResult {
|
|
21
|
+
readonly writtenFiles: readonly string[];
|
|
22
|
+
readonly skipped: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
interface CfExecContext {
|
|
25
|
+
readonly command?: string;
|
|
26
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
27
|
+
readonly sensitiveValues?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare function exportArtifacts(options: ExportArtifactsOptions): Promise<ExportArtifactsResult>;
|
|
31
|
+
declare function getAllArtifactNames(): readonly ArtifactName[];
|
|
32
|
+
|
|
33
|
+
declare function formatExportCompletionMessage(appName: string, writtenFiles: readonly string[], skipped: readonly string[]): string;
|
|
34
|
+
|
|
35
|
+
interface FetchDefaultEnvOptions {
|
|
36
|
+
readonly appName: string;
|
|
37
|
+
readonly context?: CfExecContext;
|
|
38
|
+
}
|
|
39
|
+
declare function fetchDefaultEnvJson(options: FetchDefaultEnvOptions): Promise<string>;
|
|
40
|
+
|
|
41
|
+
declare function cfApi(apiEndpoint: string, context?: CfExecContext): Promise<void>;
|
|
42
|
+
declare function cfAuth(email: string, password: string, context?: CfExecContext): Promise<void>;
|
|
43
|
+
declare function cfTargetSpace(org: string, space: string, context?: CfExecContext): Promise<void>;
|
|
44
|
+
declare function cfAppGuid(appName: string, context?: CfExecContext): Promise<string>;
|
|
45
|
+
declare function cfCurl(path: string, context?: CfExecContext): Promise<string>;
|
|
46
|
+
declare function cfSsh(appName: string, command: string, context?: CfExecContext): Promise<string>;
|
|
47
|
+
declare function cfSshBuffer(appName: string, command: string, context?: CfExecContext): Promise<Buffer>;
|
|
48
|
+
declare function buildRemoteFilePaths(fileName: string, remoteRoot: string | undefined): readonly string[];
|
|
49
|
+
declare function buildCatCommand(remotePath: string): string;
|
|
50
|
+
declare const REMOTE_CONTENT_SENTINEL = "__SAPTOOLS_CF_EXPORT_FILE_CONTENT__";
|
|
51
|
+
declare function parseRemoteFileContent(stdout: string): string | null;
|
|
52
|
+
|
|
53
|
+
interface FetchRemoteTextOptions {
|
|
54
|
+
readonly appName: string;
|
|
55
|
+
readonly fileName: string;
|
|
56
|
+
readonly remoteRoot?: string | undefined;
|
|
57
|
+
readonly context?: CfExecContext;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fetch a text file from the CF app container using cf ssh.
|
|
61
|
+
* Tries remoteRoot (if provided) first, then standard fallbacks.
|
|
62
|
+
* Returns null when the file does not exist in any candidate location.
|
|
63
|
+
*/
|
|
64
|
+
declare function fetchRemoteTextFile(options: FetchRemoteTextOptions): Promise<string | null>;
|
|
65
|
+
|
|
66
|
+
interface SessionEnv {
|
|
67
|
+
readonly email: string;
|
|
68
|
+
readonly password: string;
|
|
69
|
+
}
|
|
70
|
+
interface OpenCfSession {
|
|
71
|
+
readonly context: CfExecContext;
|
|
72
|
+
readonly dispose: () => Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
declare function openCfSession(target: CfTarget, context?: CfExecContext): Promise<OpenCfSession>;
|
|
75
|
+
|
|
76
|
+
export { ARTIFACT_NAMES, type ArtifactName, type CfExecContext, type CfTarget, type ExportArtifactsOptions, type ExportArtifactsResult, type OpenCfSession, REMOTE_CONTENT_SENTINEL, type SessionEnv, buildCatCommand, buildRemoteFilePaths, cfApi, cfAppGuid, cfAuth, cfCurl, cfSsh, cfSshBuffer, cfTargetSpace, exportArtifacts, fetchDefaultEnvJson, fetchRemoteTextFile, formatExportCompletionMessage, getAllArtifactNames, openCfSession, parseRemoteFileContent };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var ARTIFACT_NAMES = [
|
|
5
|
+
"package.json",
|
|
6
|
+
"package-lock.json",
|
|
7
|
+
"pnpm-lock.yaml",
|
|
8
|
+
".cdsrc.json",
|
|
9
|
+
"default-env.json",
|
|
10
|
+
".npmrc"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// src/exporter.ts
|
|
14
|
+
import { chmod, mkdir, writeFile } from "fs/promises";
|
|
15
|
+
import { dirname, join as join2, resolve } from "path";
|
|
16
|
+
|
|
17
|
+
// src/cf.ts
|
|
18
|
+
import { execFile } from "child_process";
|
|
19
|
+
import { promisify } from "util";
|
|
20
|
+
var execFileAsync = promisify(execFile);
|
|
21
|
+
var MAX_BUFFER = 64 * 1024 * 1024;
|
|
22
|
+
var REMOTE_FILE_SENTINEL = "__SAPTOOLS_CF_EXPORT_FILE_CONTENT__";
|
|
23
|
+
function resolveCfCommand(context) {
|
|
24
|
+
return context?.command ?? process.env["CF_EXPORT_CF_BIN"] ?? "cf";
|
|
25
|
+
}
|
|
26
|
+
function resolveCfEnv(context) {
|
|
27
|
+
const env = context?.env ? { ...process.env, ...context.env } : { ...process.env };
|
|
28
|
+
delete env["SAP_EMAIL"];
|
|
29
|
+
delete env["SAP_PASSWORD"];
|
|
30
|
+
return env;
|
|
31
|
+
}
|
|
32
|
+
function describeCfCommand(args) {
|
|
33
|
+
const [command] = args;
|
|
34
|
+
if (command === void 0) {
|
|
35
|
+
return "cf";
|
|
36
|
+
}
|
|
37
|
+
if (command === "auth") {
|
|
38
|
+
return "cf auth";
|
|
39
|
+
}
|
|
40
|
+
return `cf ${args.join(" ")}`;
|
|
41
|
+
}
|
|
42
|
+
function redactSensitiveValue(detail, value) {
|
|
43
|
+
if (value.length === 0) {
|
|
44
|
+
return detail;
|
|
45
|
+
}
|
|
46
|
+
return detail.split(value).join("[REDACTED]");
|
|
47
|
+
}
|
|
48
|
+
function sanitizeCfErrorDetail(detail, args, sensitiveValues = []) {
|
|
49
|
+
const authArgs = args[0] === "auth" ? args.slice(1) : [];
|
|
50
|
+
const values = [...authArgs, ...sensitiveValues];
|
|
51
|
+
return values.reduce((current, value) => redactSensitiveValue(current, value), detail);
|
|
52
|
+
}
|
|
53
|
+
function errorDetailFrom(err) {
|
|
54
|
+
if (typeof err === "object" && err !== null) {
|
|
55
|
+
const e = err;
|
|
56
|
+
const detail = e.stderr ?? e.message;
|
|
57
|
+
return Buffer.isBuffer(detail) ? detail.toString("utf8") : detail ?? "";
|
|
58
|
+
}
|
|
59
|
+
return String(err);
|
|
60
|
+
}
|
|
61
|
+
function withSensitiveEnv(context, env, sensitiveValues) {
|
|
62
|
+
return {
|
|
63
|
+
...context,
|
|
64
|
+
env: { ...context?.env, ...env },
|
|
65
|
+
sensitiveValues: [...context?.sensitiveValues ?? [], ...sensitiveValues]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function runCf(args, context) {
|
|
69
|
+
const cmd = resolveCfCommand(context);
|
|
70
|
+
const isScript = cmd.endsWith(".mjs") || cmd.endsWith(".js");
|
|
71
|
+
const file = isScript ? "node" : cmd;
|
|
72
|
+
const allArgs = isScript ? [cmd, ...args] : [...args];
|
|
73
|
+
try {
|
|
74
|
+
const { stdout } = await execFileAsync(file, allArgs, {
|
|
75
|
+
env: resolveCfEnv(context),
|
|
76
|
+
maxBuffer: MAX_BUFFER
|
|
77
|
+
});
|
|
78
|
+
return stdout;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const command = describeCfCommand(args);
|
|
81
|
+
const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);
|
|
82
|
+
throw new Error(`${command} failed: ${detail}`, { cause: err });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function runCfBuffer(args, context) {
|
|
86
|
+
const cmd = resolveCfCommand(context);
|
|
87
|
+
const isScript = cmd.endsWith(".mjs") || cmd.endsWith(".js");
|
|
88
|
+
const file = isScript ? "node" : cmd;
|
|
89
|
+
const allArgs = isScript ? [cmd, ...args] : [...args];
|
|
90
|
+
const options = {
|
|
91
|
+
env: resolveCfEnv(context),
|
|
92
|
+
maxBuffer: MAX_BUFFER,
|
|
93
|
+
encoding: "buffer"
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await execFileAsync(file, allArgs, options);
|
|
97
|
+
return Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const command = describeCfCommand(args);
|
|
100
|
+
const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);
|
|
101
|
+
throw new Error(`${command} failed: ${detail}`, { cause: err });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function cfApi(apiEndpoint, context) {
|
|
105
|
+
await runCf(["api", apiEndpoint], context);
|
|
106
|
+
}
|
|
107
|
+
async function cfAuth(email, password, context) {
|
|
108
|
+
const authContext = withSensitiveEnv(
|
|
109
|
+
context,
|
|
110
|
+
{
|
|
111
|
+
CF_USERNAME: email,
|
|
112
|
+
CF_PASSWORD: password
|
|
113
|
+
},
|
|
114
|
+
[email, password]
|
|
115
|
+
);
|
|
116
|
+
await runCf(["auth"], authContext);
|
|
117
|
+
}
|
|
118
|
+
async function cfTargetSpace(org, space, context) {
|
|
119
|
+
await runCf(["target", "-o", org, "-s", space], context);
|
|
120
|
+
}
|
|
121
|
+
async function cfAppGuid(appName, context) {
|
|
122
|
+
const stdout = await runCf(["app", appName, "--guid"], context);
|
|
123
|
+
const guid = stdout.trim();
|
|
124
|
+
if (guid.length === 0) {
|
|
125
|
+
throw new Error(`CF returned an empty app GUID for "${appName}".`);
|
|
126
|
+
}
|
|
127
|
+
return guid;
|
|
128
|
+
}
|
|
129
|
+
async function cfCurl(path, context) {
|
|
130
|
+
return await runCf(["curl", path], context);
|
|
131
|
+
}
|
|
132
|
+
async function cfSsh(appName, command, context) {
|
|
133
|
+
return await runCf(["ssh", appName, "--disable-pseudo-tty", "-c", command], context);
|
|
134
|
+
}
|
|
135
|
+
async function cfSshBuffer(appName, command, context) {
|
|
136
|
+
return await runCfBuffer(["ssh", appName, "--disable-pseudo-tty", "-c", command], context);
|
|
137
|
+
}
|
|
138
|
+
function buildRemoteFilePaths(fileName, remoteRoot) {
|
|
139
|
+
const paths = [];
|
|
140
|
+
const normalized = remoteRoot?.trim().replace(/\/+$/, "");
|
|
141
|
+
if (normalized !== void 0 && normalized.length > 0) {
|
|
142
|
+
paths.push(`${normalized}/${fileName}`);
|
|
143
|
+
}
|
|
144
|
+
const fallbacks = [`/home/vcap/app/${fileName}`, fileName];
|
|
145
|
+
for (const fb of fallbacks) {
|
|
146
|
+
if (!paths.includes(fb)) {
|
|
147
|
+
paths.push(fb);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return paths;
|
|
151
|
+
}
|
|
152
|
+
function buildCatCommand(remotePath) {
|
|
153
|
+
const quoted = `'${remotePath.replaceAll("'", "'\\''")}'`;
|
|
154
|
+
return [
|
|
155
|
+
`if [ -f ${quoted} ]; then`,
|
|
156
|
+
`printf '%s\\n' ${quotedForSentinel()};`,
|
|
157
|
+
`cat ${quoted};`,
|
|
158
|
+
"else exit 66; fi"
|
|
159
|
+
].join(" ");
|
|
160
|
+
}
|
|
161
|
+
function quotedForSentinel() {
|
|
162
|
+
const s = REMOTE_FILE_SENTINEL;
|
|
163
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
164
|
+
}
|
|
165
|
+
var REMOTE_CONTENT_SENTINEL = REMOTE_FILE_SENTINEL;
|
|
166
|
+
function parseRemoteFileContent(stdout) {
|
|
167
|
+
const prefix = `${REMOTE_FILE_SENTINEL}
|
|
168
|
+
`;
|
|
169
|
+
if (stdout.startsWith(prefix)) {
|
|
170
|
+
return stdout.slice(prefix.length);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/default-env.ts
|
|
176
|
+
function isRecord(value) {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function mergeRecordInto(target, source) {
|
|
180
|
+
if (!isRecord(source)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const [k, v] of Object.entries(source)) {
|
|
184
|
+
target[k] = v;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function buildDefaultEnvPayload(appEnv) {
|
|
188
|
+
const payload = {};
|
|
189
|
+
mergeRecordInto(payload, appEnv["system_env_json"]);
|
|
190
|
+
mergeRecordInto(payload, appEnv["environment_variables"]);
|
|
191
|
+
mergeRecordInto(payload, appEnv["running_env_json"]);
|
|
192
|
+
mergeRecordInto(payload, appEnv["staging_env_json"]);
|
|
193
|
+
if (Object.keys(payload).length === 0) {
|
|
194
|
+
throw new Error("No environment variables found to build default-env.json.");
|
|
195
|
+
}
|
|
196
|
+
return payload;
|
|
197
|
+
}
|
|
198
|
+
async function fetchDefaultEnvJson(options) {
|
|
199
|
+
const guid = await cfAppGuid(options.appName, options.context);
|
|
200
|
+
const encoded = encodeURIComponent(guid);
|
|
201
|
+
const stdout = await cfCurl(`/v3/apps/${encoded}/env`, options.context);
|
|
202
|
+
let parsed;
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(stdout);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error("Unexpected JSON format for CF app environment payload.");
|
|
207
|
+
}
|
|
208
|
+
if (!isRecord(parsed)) {
|
|
209
|
+
throw new Error("Unexpected JSON object format for CF app environment payload.");
|
|
210
|
+
}
|
|
211
|
+
const payload = buildDefaultEnvPayload(parsed);
|
|
212
|
+
return `${JSON.stringify(payload, null, 2)}
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/remote-paths.ts
|
|
217
|
+
async function fetchRemoteTextFile(options) {
|
|
218
|
+
const paths = buildRemoteFilePaths(options.fileName, options.remoteRoot);
|
|
219
|
+
for (const remotePath of paths) {
|
|
220
|
+
try {
|
|
221
|
+
const cmd = buildCatCommand(remotePath);
|
|
222
|
+
const stdout = await cfSsh(options.appName, cmd, options.context);
|
|
223
|
+
const content = parseRemoteFileContent(stdout);
|
|
224
|
+
if (content !== null) {
|
|
225
|
+
return content;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/session.ts
|
|
234
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
235
|
+
import { tmpdir } from "os";
|
|
236
|
+
import { join } from "path";
|
|
237
|
+
import { resolveApiEndpoint, resolveSessionEnv } from "@saptools/cf-files";
|
|
238
|
+
import { resolveApiEndpoint as resolveApiEndpoint2, resolveSessionEnv as resolveSessionEnv2 } from "@saptools/cf-files";
|
|
239
|
+
var CF_HOME_PREFIX = "saptools-cf-export-";
|
|
240
|
+
function explicitCfHome(context) {
|
|
241
|
+
const fromContext = context?.env?.["CF_HOME"] ?? context?.env?.["CF_EXPORT_CF_HOME"];
|
|
242
|
+
if (fromContext !== void 0 && fromContext !== "") {
|
|
243
|
+
return fromContext;
|
|
244
|
+
}
|
|
245
|
+
const fromProcess = process.env["CF_EXPORT_CF_HOME"];
|
|
246
|
+
return fromProcess === void 0 || fromProcess === "" ? void 0 : fromProcess;
|
|
247
|
+
}
|
|
248
|
+
function buildSessionEnv(context, cfHome) {
|
|
249
|
+
const env = {};
|
|
250
|
+
for (const [key, value] of Object.entries(context?.env ?? {})) {
|
|
251
|
+
if (key !== "SAP_EMAIL" && key !== "SAP_PASSWORD") {
|
|
252
|
+
env[key] = value;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
env["CF_HOME"] = cfHome;
|
|
256
|
+
return env;
|
|
257
|
+
}
|
|
258
|
+
async function createSessionContext(context) {
|
|
259
|
+
const configured = explicitCfHome(context);
|
|
260
|
+
if (configured !== void 0) {
|
|
261
|
+
return {
|
|
262
|
+
context: {
|
|
263
|
+
...context,
|
|
264
|
+
env: buildSessionEnv(context, configured)
|
|
265
|
+
},
|
|
266
|
+
dispose: () => Promise.resolve()
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const cfHome = await mkdtemp(join(tmpdir(), CF_HOME_PREFIX));
|
|
270
|
+
return {
|
|
271
|
+
context: {
|
|
272
|
+
...context,
|
|
273
|
+
env: buildSessionEnv(context, cfHome)
|
|
274
|
+
},
|
|
275
|
+
dispose: async () => {
|
|
276
|
+
await rm(cfHome, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function openCfSession(target, context) {
|
|
281
|
+
const { email, password } = resolveSessionEnv(context?.env);
|
|
282
|
+
const apiEndpoint = resolveApiEndpoint(target.region);
|
|
283
|
+
const session = await createSessionContext(context);
|
|
284
|
+
try {
|
|
285
|
+
await cfApi(apiEndpoint, session.context);
|
|
286
|
+
await cfAuth(email, password, session.context);
|
|
287
|
+
await cfTargetSpace(target.org, target.space, session.context);
|
|
288
|
+
return session;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
await session.dispose();
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/exporter.ts
|
|
296
|
+
function resolveAllArtifacts() {
|
|
297
|
+
return [...ARTIFACT_NAMES];
|
|
298
|
+
}
|
|
299
|
+
function normalizeRequested(requested) {
|
|
300
|
+
if (!requested || requested.length === 0) {
|
|
301
|
+
return resolveAllArtifacts();
|
|
302
|
+
}
|
|
303
|
+
const seen = /* @__PURE__ */ new Set();
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const name of requested) {
|
|
306
|
+
if (ARTIFACT_NAMES.includes(name) && !seen.has(name)) {
|
|
307
|
+
seen.add(name);
|
|
308
|
+
out.push(name);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
async function writeArtifact(outDir, fileName, content) {
|
|
314
|
+
const outPath = resolve(join2(outDir, fileName));
|
|
315
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
316
|
+
const isSensitive = fileName === "default-env.json" || fileName === ".npmrc";
|
|
317
|
+
await writeFile(outPath, content, { encoding: "utf8" });
|
|
318
|
+
if (isSensitive) {
|
|
319
|
+
await chmod(outPath, 384);
|
|
320
|
+
}
|
|
321
|
+
return outPath;
|
|
322
|
+
}
|
|
323
|
+
async function exportArtifacts(options) {
|
|
324
|
+
const requested = options.artifacts;
|
|
325
|
+
if (requested?.length === 0) {
|
|
326
|
+
throw new Error("At least one artifact must be selected for export.");
|
|
327
|
+
}
|
|
328
|
+
const artifacts = normalizeRequested(requested);
|
|
329
|
+
const session = await openCfSession(options.target);
|
|
330
|
+
const written = [];
|
|
331
|
+
const skipped = [];
|
|
332
|
+
try {
|
|
333
|
+
for (const name of artifacts) {
|
|
334
|
+
if (name === "default-env.json") {
|
|
335
|
+
try {
|
|
336
|
+
const json = await fetchDefaultEnvJson({
|
|
337
|
+
appName: options.target.app,
|
|
338
|
+
context: session.context
|
|
339
|
+
});
|
|
340
|
+
const path2 = await writeArtifact(options.outDir, name, json);
|
|
341
|
+
written.push(path2);
|
|
342
|
+
} catch {
|
|
343
|
+
skipped.push(name);
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const remoteRoot = options.remoteRoot;
|
|
348
|
+
const fetchOpts = {
|
|
349
|
+
appName: options.target.app,
|
|
350
|
+
fileName: name,
|
|
351
|
+
context: session.context,
|
|
352
|
+
...typeof remoteRoot === "string" ? { remoteRoot } : {}
|
|
353
|
+
};
|
|
354
|
+
const content = await fetchRemoteTextFile(fetchOpts);
|
|
355
|
+
if (content === null) {
|
|
356
|
+
skipped.push(name);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const path = await writeArtifact(options.outDir, name, content);
|
|
360
|
+
written.push(path);
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
await session.dispose();
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
writtenFiles: written,
|
|
367
|
+
skipped
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function getAllArtifactNames() {
|
|
371
|
+
return resolveAllArtifacts();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/format.ts
|
|
375
|
+
function formatExportCompletionMessage(appName, writtenFiles, skipped) {
|
|
376
|
+
const names = writtenFiles.map((p) => {
|
|
377
|
+
const parts = p.split(/[\\/]/);
|
|
378
|
+
return parts[parts.length - 1] ?? p;
|
|
379
|
+
});
|
|
380
|
+
const base = `Export completed for "${appName}".`;
|
|
381
|
+
if (names.length === 0) {
|
|
382
|
+
return `${base} No files written.`;
|
|
383
|
+
}
|
|
384
|
+
const filesPart = `${String(names.length)} file${names.length === 1 ? "" : "s"}: ${names.join(", ")}`;
|
|
385
|
+
if (skipped.length === 0) {
|
|
386
|
+
return `${base} ${filesPart}.`;
|
|
387
|
+
}
|
|
388
|
+
const skipPart = `Skipped: ${skipped.join(", ")}`;
|
|
389
|
+
return `${base} ${filesPart}. ${skipPart}.`;
|
|
390
|
+
}
|
|
391
|
+
export {
|
|
392
|
+
ARTIFACT_NAMES,
|
|
393
|
+
REMOTE_CONTENT_SENTINEL,
|
|
394
|
+
buildCatCommand,
|
|
395
|
+
buildRemoteFilePaths,
|
|
396
|
+
cfApi,
|
|
397
|
+
cfAppGuid,
|
|
398
|
+
cfAuth,
|
|
399
|
+
cfCurl,
|
|
400
|
+
cfSsh,
|
|
401
|
+
cfSshBuffer,
|
|
402
|
+
cfTargetSpace,
|
|
403
|
+
exportArtifacts,
|
|
404
|
+
fetchDefaultEnvJson,
|
|
405
|
+
fetchRemoteTextFile,
|
|
406
|
+
formatExportCompletionMessage,
|
|
407
|
+
getAllArtifactNames,
|
|
408
|
+
openCfSession,
|
|
409
|
+
parseRemoteFileContent,
|
|
410
|
+
resolveApiEndpoint2 as resolveApiEndpoint,
|
|
411
|
+
resolveSessionEnv2 as resolveSessionEnv
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/exporter.ts","../src/cf.ts","../src/default-env.ts","../src/remote-paths.ts","../src/session.ts","../src/format.ts"],"sourcesContent":["export interface CfTarget {\n readonly region: string;\n readonly org: string;\n readonly space: string;\n readonly app: string;\n}\n\nexport const ARTIFACT_NAMES = [\n \"package.json\",\n \"package-lock.json\",\n \"pnpm-lock.yaml\",\n \".cdsrc.json\",\n \"default-env.json\",\n \".npmrc\",\n] as const;\n\nexport type ArtifactName = (typeof ARTIFACT_NAMES)[number];\n\nexport interface ExportArtifactsOptions {\n readonly target: CfTarget;\n readonly outDir: string;\n readonly remoteRoot?: string;\n /**\n * Subset of artifacts to export. When omitted or empty, all supported artifacts are attempted.\n */\n readonly artifacts?: readonly ArtifactName[];\n}\n\nexport interface ExportArtifactsResult {\n readonly writtenFiles: readonly string[];\n readonly skipped: readonly string[];\n}\n\nexport interface CfExecContext {\n readonly command?: string;\n readonly env?: NodeJS.ProcessEnv;\n readonly sensitiveValues?: readonly string[];\n}\n","import { chmod, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\n\nimport { fetchDefaultEnvJson } from \"./default-env.js\";\nimport { fetchRemoteTextFile } from \"./remote-paths.js\";\nimport { openCfSession } from \"./session.js\";\nimport type { OpenCfSession } from \"./session.js\";\nimport { ARTIFACT_NAMES, type ArtifactName, type CfExecContext, type ExportArtifactsOptions, type ExportArtifactsResult } from \"./types.js\";\n\nfunction resolveAllArtifacts(): readonly ArtifactName[] {\n return [...ARTIFACT_NAMES];\n}\n\nfunction normalizeRequested(requested: readonly ArtifactName[] | undefined): readonly ArtifactName[] {\n if (!requested || requested.length === 0) {\n return resolveAllArtifacts();\n }\n // Deduplicate while preserving order of first occurrence\n const seen = new Set<string>();\n const out: ArtifactName[] = [];\n for (const name of requested) {\n if (ARTIFACT_NAMES.includes(name) && !seen.has(name)) {\n seen.add(name);\n out.push(name);\n }\n }\n return out;\n}\n\nasync function writeArtifact(outDir: string, fileName: string, content: string): Promise<string> {\n const outPath = resolve(join(outDir, fileName));\n await mkdir(dirname(outPath), { recursive: true });\n const isSensitive = fileName === \"default-env.json\" || fileName === \".npmrc\";\n await writeFile(outPath, content, { encoding: \"utf8\" });\n if (isSensitive) {\n await chmod(outPath, 0o600);\n }\n return outPath;\n}\n\nexport async function exportArtifacts(\n options: ExportArtifactsOptions,\n): Promise<ExportArtifactsResult> {\n const requested = options.artifacts;\n if (requested?.length === 0) {\n throw new Error(\"At least one artifact must be selected for export.\");\n }\n const artifacts = normalizeRequested(requested);\n\n const session: OpenCfSession = await openCfSession(options.target);\n const written: string[] = [];\n const skipped: string[] = [];\n\n try {\n for (const name of artifacts) {\n if (name === \"default-env.json\") {\n try {\n const json = await fetchDefaultEnvJson({\n appName: options.target.app,\n context: session.context,\n });\n const path = await writeArtifact(options.outDir, name, json);\n written.push(path);\n } catch {\n // Treat default-env as best-effort too (consistent with \"nếu có\" for all artifacts in default \"all\" mode).\n // Only the presence of the file/VCAP in the remote app determines if it can be exported.\n skipped.push(name);\n }\n continue;\n }\n\n // regular file via ssh\n const remoteRoot = options.remoteRoot;\n const fetchOpts: {\n readonly appName: string;\n readonly fileName: string;\n readonly remoteRoot?: string | undefined;\n readonly context?: CfExecContext;\n } = {\n appName: options.target.app,\n fileName: name,\n context: session.context,\n ...(typeof remoteRoot === \"string\" ? { remoteRoot } : {}),\n };\n const content = await fetchRemoteTextFile(fetchOpts);\n\n if (content === null) {\n skipped.push(name);\n continue;\n }\n\n const path = await writeArtifact(options.outDir, name, content);\n written.push(path);\n }\n } finally {\n await session.dispose();\n }\n\n return {\n writtenFiles: written,\n skipped,\n };\n}\n\nexport function getAllArtifactNames(): readonly ArtifactName[] {\n return resolveAllArtifacts();\n}\n","import { execFile, type ExecFileOptionsWithBufferEncoding } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nimport type { CfExecContext } from \"./types.js\";\n\nconst execFileAsync = promisify(execFile);\n\nconst MAX_BUFFER = 64 * 1024 * 1024;\n\nconst REMOTE_FILE_SENTINEL = \"__SAPTOOLS_CF_EXPORT_FILE_CONTENT__\";\n\nfunction resolveCfCommand(context?: CfExecContext): string {\n return context?.command ?? process.env[\"CF_EXPORT_CF_BIN\"] ?? \"cf\";\n}\n\nfunction resolveCfEnv(context?: CfExecContext): NodeJS.ProcessEnv {\n const env = context?.env ? { ...process.env, ...context.env } : { ...process.env };\n delete env[\"SAP_EMAIL\"];\n delete env[\"SAP_PASSWORD\"];\n return env;\n}\n\nfunction describeCfCommand(args: readonly string[]): string {\n const [command] = args;\n if (command === undefined) {\n return \"cf\";\n }\n if (command === \"auth\") {\n return \"cf auth\";\n }\n return `cf ${args.join(\" \")}`;\n}\n\nfunction redactSensitiveValue(detail: string, value: string): string {\n if (value.length === 0) {\n return detail;\n }\n return detail.split(value).join(\"[REDACTED]\");\n}\n\nfunction sanitizeCfErrorDetail(\n detail: string,\n args: readonly string[],\n sensitiveValues: readonly string[] = [],\n): string {\n const authArgs = args[0] === \"auth\" ? args.slice(1) : [];\n const values = [...authArgs, ...sensitiveValues];\n return values.reduce((current, value) => redactSensitiveValue(current, value), detail);\n}\n\nfunction errorDetailFrom(err: unknown): string {\n if (typeof err === \"object\" && err !== null) {\n const e = err as { stderr?: Buffer | string; message?: string };\n const detail = e.stderr ?? e.message;\n return Buffer.isBuffer(detail) ? detail.toString(\"utf8\") : (detail ?? \"\");\n }\n return String(err);\n}\n\nfunction withSensitiveEnv(\n context: CfExecContext | undefined,\n env: NodeJS.ProcessEnv,\n sensitiveValues: readonly string[],\n): CfExecContext {\n return {\n ...context,\n env: { ...context?.env, ...env },\n sensitiveValues: [...(context?.sensitiveValues ?? []), ...sensitiveValues],\n };\n}\n\nasync function runCf(args: readonly string[], context?: CfExecContext): Promise<string> {\n const cmd = resolveCfCommand(context);\n const isScript = cmd.endsWith(\".mjs\") || cmd.endsWith(\".js\");\n const file = isScript ? \"node\" : cmd;\n const allArgs = isScript ? [cmd, ...args] : [...args];\n try {\n const { stdout } = await execFileAsync(file, allArgs, {\n env: resolveCfEnv(context),\n maxBuffer: MAX_BUFFER,\n });\n return stdout;\n } catch (err) {\n const command = describeCfCommand(args);\n const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);\n throw new Error(`${command} failed: ${detail}`, { cause: err });\n }\n}\n\nasync function runCfBuffer(\n args: readonly string[],\n context?: CfExecContext,\n): Promise<Buffer> {\n const cmd = resolveCfCommand(context);\n const isScript = cmd.endsWith(\".mjs\") || cmd.endsWith(\".js\");\n const file = isScript ? \"node\" : cmd;\n const allArgs = isScript ? [cmd, ...args] : [...args];\n const options: ExecFileOptionsWithBufferEncoding = {\n env: resolveCfEnv(context),\n maxBuffer: MAX_BUFFER,\n encoding: \"buffer\",\n };\n try {\n const { stdout } = await execFileAsync(file, allArgs, options);\n return Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout);\n } catch (err) {\n const command = describeCfCommand(args);\n const detail = sanitizeCfErrorDetail(errorDetailFrom(err), args, context?.sensitiveValues);\n throw new Error(`${command} failed: ${detail}`, { cause: err });\n }\n}\n\nexport async function cfApi(apiEndpoint: string, context?: CfExecContext): Promise<void> {\n await runCf([\"api\", apiEndpoint], context);\n}\n\nexport async function cfAuth(\n email: string,\n password: string,\n context?: CfExecContext,\n): Promise<void> {\n const authContext = withSensitiveEnv(\n context,\n {\n CF_USERNAME: email,\n CF_PASSWORD: password,\n },\n [email, password],\n );\n await runCf([\"auth\"], authContext);\n}\n\nexport async function cfTargetSpace(\n org: string,\n space: string,\n context?: CfExecContext,\n): Promise<void> {\n await runCf([\"target\", \"-o\", org, \"-s\", space], context);\n}\n\nexport async function cfAppGuid(appName: string, context?: CfExecContext): Promise<string> {\n const stdout = await runCf([\"app\", appName, \"--guid\"], context);\n const guid = stdout.trim();\n if (guid.length === 0) {\n throw new Error(`CF returned an empty app GUID for \"${appName}\".`);\n }\n return guid;\n}\n\nexport async function cfCurl(path: string, context?: CfExecContext): Promise<string> {\n return await runCf([\"curl\", path], context);\n}\n\nexport async function cfSsh(\n appName: string,\n command: string,\n context?: CfExecContext,\n): Promise<string> {\n return await runCf([\"ssh\", appName, \"--disable-pseudo-tty\", \"-c\", command], context);\n}\n\nexport async function cfSshBuffer(\n appName: string,\n command: string,\n context?: CfExecContext,\n): Promise<Buffer> {\n return await runCfBuffer([\"ssh\", appName, \"--disable-pseudo-tty\", \"-c\", command], context);\n}\n\nexport function buildRemoteFilePaths(fileName: string, remoteRoot: string | undefined): readonly string[] {\n const paths: string[] = [];\n const normalized = remoteRoot?.trim().replace(/\\/+$/, \"\");\n if (normalized !== undefined && normalized.length > 0) {\n paths.push(`${normalized}/${fileName}`);\n }\n const fallbacks = [`/home/vcap/app/${fileName}`, fileName];\n for (const fb of fallbacks) {\n if (!paths.includes(fb)) {\n paths.push(fb);\n }\n }\n return paths;\n}\n\nexport function buildCatCommand(remotePath: string): string {\n const quoted = `'${remotePath.replaceAll(\"'\", \"'\\\\''\")}'`;\n return [\n `if [ -f ${quoted} ]; then`,\n `printf '%s\\\\n' ${quotedForSentinel()};`,\n `cat ${quoted};`,\n \"else exit 66; fi\",\n ].join(\" \");\n}\n\nfunction quotedForSentinel(): string {\n // Sentinel is a constant known only to us; no user content can start with it after trim.\n const s = REMOTE_FILE_SENTINEL;\n return `'${s.replaceAll(\"'\", \"'\\\\''\")}'`;\n}\n\nexport const REMOTE_CONTENT_SENTINEL = REMOTE_FILE_SENTINEL;\n\nexport function parseRemoteFileContent(stdout: string): string | null {\n const prefix = `${REMOTE_FILE_SENTINEL}\\n`;\n if (stdout.startsWith(prefix)) {\n return stdout.slice(prefix.length);\n }\n return null;\n}\n\nexport const internals = {\n runCf,\n describeCfCommand,\n sanitizeCfErrorDetail,\n};\n","import { cfAppGuid, cfCurl } from \"./cf.js\";\nimport type { CfExecContext } from \"./types.js\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction mergeRecordInto(target: Record<string, unknown>, source: unknown): void {\n if (!isRecord(source)) {\n return;\n }\n for (const [k, v] of Object.entries(source)) {\n target[k] = v;\n }\n}\n\nfunction buildDefaultEnvPayload(appEnv: Record<string, unknown>): Record<string, unknown> {\n const payload: Record<string, unknown> = {};\n mergeRecordInto(payload, appEnv[\"system_env_json\"]);\n mergeRecordInto(payload, appEnv[\"environment_variables\"]);\n mergeRecordInto(payload, appEnv[\"running_env_json\"]);\n mergeRecordInto(payload, appEnv[\"staging_env_json\"]);\n\n if (Object.keys(payload).length === 0) {\n throw new Error(\"No environment variables found to build default-env.json.\");\n }\n return payload;\n}\n\nexport interface FetchDefaultEnvOptions {\n readonly appName: string;\n readonly context?: CfExecContext;\n}\n\nexport async function fetchDefaultEnvJson(options: FetchDefaultEnvOptions): Promise<string> {\n const guid = await cfAppGuid(options.appName, options.context);\n const encoded = encodeURIComponent(guid);\n const stdout = await cfCurl(`/v3/apps/${encoded}/env`, options.context);\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(stdout);\n } catch {\n throw new Error(\"Unexpected JSON format for CF app environment payload.\");\n }\n\n if (!isRecord(parsed)) {\n throw new Error(\"Unexpected JSON object format for CF app environment payload.\");\n }\n\n const payload = buildDefaultEnvPayload(parsed);\n return `${JSON.stringify(payload, null, 2)}\\n`;\n}\n","import { cfSsh, buildRemoteFilePaths, parseRemoteFileContent, REMOTE_CONTENT_SENTINEL, buildCatCommand } from \"./cf.js\";\nimport type { CfExecContext } from \"./types.js\";\n\nexport interface FetchRemoteTextOptions {\n readonly appName: string;\n readonly fileName: string;\n readonly remoteRoot?: string | undefined;\n readonly context?: CfExecContext;\n}\n\n/**\n * Fetch a text file from the CF app container using cf ssh.\n * Tries remoteRoot (if provided) first, then standard fallbacks.\n * Returns null when the file does not exist in any candidate location.\n */\nexport async function fetchRemoteTextFile(options: FetchRemoteTextOptions): Promise<string | null> {\n const paths = buildRemoteFilePaths(options.fileName, options.remoteRoot);\n\n for (const remotePath of paths) {\n try {\n const cmd = buildCatCommand(remotePath);\n const stdout = await cfSsh(options.appName, cmd, options.context);\n const content = parseRemoteFileContent(stdout);\n if (content !== null) {\n return content;\n }\n } catch {\n // 66 exit or ssh failure for this path → try next\n }\n }\n\n return null;\n}\n\nexport { buildRemoteFilePaths, buildCatCommand, parseRemoteFileContent, REMOTE_CONTENT_SENTINEL };\n","import { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { resolveApiEndpoint, resolveSessionEnv } from \"@saptools/cf-files\";\nimport { cfApi, cfAuth, cfTargetSpace } from \"./cf.js\";\nimport type { CfExecContext, CfTarget } from \"./types.js\";\n\n// Delegate pure resolve helpers to @saptools/cf-files (shared, less duplication for non-exec parts)\nexport { resolveApiEndpoint, resolveSessionEnv } from \"@saptools/cf-files\";\n\n// Local openCfSession still uses our custom CF_EXPORT_* envs and prefix while calling the shared cf* functions.\n\nexport interface SessionEnv {\n readonly email: string;\n readonly password: string;\n}\n\nexport interface OpenCfSession {\n readonly context: CfExecContext;\n readonly dispose: () => Promise<void>;\n}\n\nconst CF_HOME_PREFIX = \"saptools-cf-export-\";\n\n// resolveSessionEnv and resolveApiEndpoint are re-exported from @saptools/cf-files above\n// (keeps implementation in one place, reduces duplication).\n\nfunction explicitCfHome(context?: CfExecContext): string | undefined {\n const fromContext =\n context?.env?.[\"CF_HOME\"] ?? context?.env?.[\"CF_EXPORT_CF_HOME\"];\n if (fromContext !== undefined && fromContext !== \"\") {\n return fromContext;\n }\n const fromProcess = process.env[\"CF_EXPORT_CF_HOME\"];\n return fromProcess === undefined || fromProcess === \"\" ? undefined : fromProcess;\n}\n\nfunction buildSessionEnv(context: CfExecContext | undefined, cfHome: string): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const [key, value] of Object.entries(context?.env ?? {})) {\n if (key !== \"SAP_EMAIL\" && key !== \"SAP_PASSWORD\") {\n env[key] = value;\n }\n }\n env[\"CF_HOME\"] = cfHome;\n return env;\n}\n\nasync function createSessionContext(context?: CfExecContext): Promise<OpenCfSession> {\n const configured = explicitCfHome(context);\n if (configured !== undefined) {\n return {\n context: {\n ...context,\n env: buildSessionEnv(context, configured),\n },\n dispose: (): Promise<void> => Promise.resolve(),\n };\n }\n\n const cfHome = await mkdtemp(join(tmpdir(), CF_HOME_PREFIX));\n return {\n context: {\n ...context,\n env: buildSessionEnv(context, cfHome),\n },\n dispose: async (): Promise<void> => {\n await rm(cfHome, { recursive: true, force: true });\n },\n };\n}\n\nexport async function openCfSession(\n target: CfTarget,\n context?: CfExecContext,\n): Promise<OpenCfSession> {\n const { email, password } = resolveSessionEnv(context?.env);\n const apiEndpoint = resolveApiEndpoint(target.region);\n const session = await createSessionContext(context);\n\n try {\n await cfApi(apiEndpoint, session.context);\n await cfAuth(email, password, session.context);\n await cfTargetSpace(target.org, target.space, session.context);\n return session;\n } catch (err) {\n await session.dispose();\n throw err;\n }\n}\n","\n\nexport function formatExportCompletionMessage(\n appName: string,\n writtenFiles: readonly string[],\n skipped: readonly string[],\n): string {\n const names = writtenFiles.map((p) => {\n const parts = p.split(/[\\\\/]/);\n return parts[parts.length - 1] ?? p;\n });\n\n const base = `Export completed for \"${appName}\".`;\n if (names.length === 0) {\n return `${base} No files written.`;\n }\n\n const filesPart = `${String(names.length)} file${names.length === 1 ? \"\" : \"s\"}: ${names.join(\", \")}`;\n if (skipped.length === 0) {\n return `${base} ${filesPart}.`;\n }\n const skipPart = `Skipped: ${skipped.join(\", \")}`;\n return `${base} ${filesPart}. ${skipPart}.`;\n}\n"],"mappings":";;;AAOO,IAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;ACdA,SAAS,OAAO,OAAO,iBAAiB;AACxC,SAAS,SAAS,QAAAA,OAAM,eAAe;;;ACDvC,SAAS,gBAAwD;AACjE,SAAS,iBAAiB;AAI1B,IAAM,gBAAgB,UAAU,QAAQ;AAExC,IAAM,aAAa,KAAK,OAAO;AAE/B,IAAM,uBAAuB;AAE7B,SAAS,iBAAiB,SAAiC;AACzD,SAAO,SAAS,WAAW,QAAQ,IAAI,kBAAkB,KAAK;AAChE;AAEA,SAAS,aAAa,SAA4C;AAChE,QAAM,MAAM,SAAS,MAAM,EAAE,GAAG,QAAQ,KAAK,GAAG,QAAQ,IAAI,IAAI,EAAE,GAAG,QAAQ,IAAI;AACjF,SAAO,IAAI,WAAW;AACtB,SAAO,IAAI,cAAc;AACzB,SAAO;AACT;AAEA,SAAS,kBAAkB,MAAiC;AAC1D,QAAM,CAAC,OAAO,IAAI;AAClB,MAAI,YAAY,QAAW;AACzB,WAAO;AAAA,EACT;AACA,MAAI,YAAY,QAAQ;AACtB,WAAO;AAAA,EACT;AACA,SAAO,MAAM,KAAK,KAAK,GAAG,CAAC;AAC7B;AAEA,SAAS,qBAAqB,QAAgB,OAAuB;AACnE,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,MAAM,KAAK,EAAE,KAAK,YAAY;AAC9C;AAEA,SAAS,sBACP,QACA,MACA,kBAAqC,CAAC,GAC9B;AACR,QAAM,WAAW,KAAK,CAAC,MAAM,SAAS,KAAK,MAAM,CAAC,IAAI,CAAC;AACvD,QAAM,SAAS,CAAC,GAAG,UAAU,GAAG,eAAe;AAC/C,SAAO,OAAO,OAAO,CAAC,SAAS,UAAU,qBAAqB,SAAS,KAAK,GAAG,MAAM;AACvF;AAEA,SAAS,gBAAgB,KAAsB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,UAAM,IAAI;AACV,UAAM,SAAS,EAAE,UAAU,EAAE;AAC7B,WAAO,OAAO,SAAS,MAAM,IAAI,OAAO,SAAS,MAAM,IAAK,UAAU;AAAA,EACxE;AACA,SAAO,OAAO,GAAG;AACnB;AAEA,SAAS,iBACP,SACA,KACA,iBACe;AACf,SAAO;AAAA,IACL,GAAG;AAAA,IACH,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG,IAAI;AAAA,IAC/B,iBAAiB,CAAC,GAAI,SAAS,mBAAmB,CAAC,GAAI,GAAG,eAAe;AAAA,EAC3E;AACF;AAEA,eAAe,MAAM,MAAyB,SAA0C;AACtF,QAAM,MAAM,iBAAiB,OAAO;AACpC,QAAM,WAAW,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,KAAK;AAC3D,QAAM,OAAO,WAAW,SAAS;AACjC,QAAM,UAAU,WAAW,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI;AACpD,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,MAAM,SAAS;AAAA,MACpD,KAAK,aAAa,OAAO;AAAA,MACzB,WAAW;AAAA,IACb,CAAC;AACD,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,UAAU,kBAAkB,IAAI;AACtC,UAAM,SAAS,sBAAsB,gBAAgB,GAAG,GAAG,MAAM,SAAS,eAAe;AACzF,UAAM,IAAI,MAAM,GAAG,OAAO,YAAY,MAAM,IAAI,EAAE,OAAO,IAAI,CAAC;AAAA,EAChE;AACF;AAEA,eAAe,YACb,MACA,SACiB;AACjB,QAAM,MAAM,iBAAiB,OAAO;AACpC,QAAM,WAAW,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,KAAK;AAC3D,QAAM,OAAO,WAAW,SAAS;AACjC,QAAM,UAAU,WAAW,CAAC,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI;AACpD,QAAM,UAA6C;AAAA,IACjD,KAAK,aAAa,OAAO;AAAA,IACzB,WAAW;AAAA,IACX,UAAU;AAAA,EACZ;AACA,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,MAAM,SAAS,OAAO;AAC7D,WAAO,OAAO,SAAS,MAAM,IAAI,SAAS,OAAO,KAAK,MAAM;AAAA,EAC9D,SAAS,KAAK;AACZ,UAAM,UAAU,kBAAkB,IAAI;AACtC,UAAM,SAAS,sBAAsB,gBAAgB,GAAG,GAAG,MAAM,SAAS,eAAe;AACzF,UAAM,IAAI,MAAM,GAAG,OAAO,YAAY,MAAM,IAAI,EAAE,OAAO,IAAI,CAAC;AAAA,EAChE;AACF;AAEA,eAAsB,MAAM,aAAqB,SAAwC;AACvF,QAAM,MAAM,CAAC,OAAO,WAAW,GAAG,OAAO;AAC3C;AAEA,eAAsB,OACpB,OACA,UACA,SACe;AACf,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,aAAa;AAAA,IACf;AAAA,IACA,CAAC,OAAO,QAAQ;AAAA,EAClB;AACA,QAAM,MAAM,CAAC,MAAM,GAAG,WAAW;AACnC;AAEA,eAAsB,cACpB,KACA,OACA,SACe;AACf,QAAM,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,KAAK,GAAG,OAAO;AACzD;AAEA,eAAsB,UAAU,SAAiB,SAA0C;AACzF,QAAM,SAAS,MAAM,MAAM,CAAC,OAAO,SAAS,QAAQ,GAAG,OAAO;AAC9D,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,IAAI,MAAM,sCAAsC,OAAO,IAAI;AAAA,EACnE;AACA,SAAO;AACT;AAEA,eAAsB,OAAO,MAAc,SAA0C;AACnF,SAAO,MAAM,MAAM,CAAC,QAAQ,IAAI,GAAG,OAAO;AAC5C;AAEA,eAAsB,MACpB,SACA,SACA,SACiB;AACjB,SAAO,MAAM,MAAM,CAAC,OAAO,SAAS,wBAAwB,MAAM,OAAO,GAAG,OAAO;AACrF;AAEA,eAAsB,YACpB,SACA,SACA,SACiB;AACjB,SAAO,MAAM,YAAY,CAAC,OAAO,SAAS,wBAAwB,MAAM,OAAO,GAAG,OAAO;AAC3F;AAEO,SAAS,qBAAqB,UAAkB,YAAmD;AACxG,QAAM,QAAkB,CAAC;AACzB,QAAM,aAAa,YAAY,KAAK,EAAE,QAAQ,QAAQ,EAAE;AACxD,MAAI,eAAe,UAAa,WAAW,SAAS,GAAG;AACrD,UAAM,KAAK,GAAG,UAAU,IAAI,QAAQ,EAAE;AAAA,EACxC;AACA,QAAM,YAAY,CAAC,kBAAkB,QAAQ,IAAI,QAAQ;AACzD,aAAW,MAAM,WAAW;AAC1B,QAAI,CAAC,MAAM,SAAS,EAAE,GAAG;AACvB,YAAM,KAAK,EAAE;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,YAA4B;AAC1D,QAAM,SAAS,IAAI,WAAW,WAAW,KAAK,OAAO,CAAC;AACtD,SAAO;AAAA,IACL,WAAW,MAAM;AAAA,IACjB,kBAAkB,kBAAkB,CAAC;AAAA,IACrC,OAAO,MAAM;AAAA,IACb;AAAA,EACF,EAAE,KAAK,GAAG;AACZ;AAEA,SAAS,oBAA4B;AAEnC,QAAM,IAAI;AACV,SAAO,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC;AACvC;AAEO,IAAM,0BAA0B;AAEhC,SAAS,uBAAuB,QAA+B;AACpE,QAAM,SAAS,GAAG,oBAAoB;AAAA;AACtC,MAAI,OAAO,WAAW,MAAM,GAAG;AAC7B,WAAO,OAAO,MAAM,OAAO,MAAM;AAAA,EACnC;AACA,SAAO;AACT;;;AC7MA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,gBAAgB,QAAiC,QAAuB;AAC/E,MAAI,CAAC,SAAS,MAAM,GAAG;AACrB;AAAA,EACF;AACA,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,WAAO,CAAC,IAAI;AAAA,EACd;AACF;AAEA,SAAS,uBAAuB,QAA0D;AACxF,QAAM,UAAmC,CAAC;AAC1C,kBAAgB,SAAS,OAAO,iBAAiB,CAAC;AAClD,kBAAgB,SAAS,OAAO,uBAAuB,CAAC;AACxD,kBAAgB,SAAS,OAAO,kBAAkB,CAAC;AACnD,kBAAgB,SAAS,OAAO,kBAAkB,CAAC;AAEnD,MAAI,OAAO,KAAK,OAAO,EAAE,WAAW,GAAG;AACrC,UAAM,IAAI,MAAM,2DAA2D;AAAA,EAC7E;AACA,SAAO;AACT;AAOA,eAAsB,oBAAoB,SAAkD;AAC1F,QAAM,OAAO,MAAM,UAAU,QAAQ,SAAS,QAAQ,OAAO;AAC7D,QAAM,UAAU,mBAAmB,IAAI;AACvC,QAAM,SAAS,MAAM,OAAO,YAAY,OAAO,QAAQ,QAAQ,OAAO;AAEtE,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,MAAM;AAAA,EAC5B,QAAQ;AACN,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AAEA,MAAI,CAAC,SAAS,MAAM,GAAG;AACrB,UAAM,IAAI,MAAM,+DAA+D;AAAA,EACjF;AAEA,QAAM,UAAU,uBAAuB,MAAM;AAC7C,SAAO,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA;AAC5C;;;ACrCA,eAAsB,oBAAoB,SAAyD;AACjG,QAAM,QAAQ,qBAAqB,QAAQ,UAAU,QAAQ,UAAU;AAEvE,aAAW,cAAc,OAAO;AAC9B,QAAI;AACF,YAAM,MAAM,gBAAgB,UAAU;AACtC,YAAM,SAAS,MAAM,MAAM,QAAQ,SAAS,KAAK,QAAQ,OAAO;AAChE,YAAM,UAAU,uBAAuB,MAAM;AAC7C,UAAI,YAAY,MAAM;AACpB,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;;;AChCA,SAAS,SAAS,UAAU;AAC5B,SAAS,cAAc;AACvB,SAAS,YAAY;AAErB,SAAS,oBAAoB,yBAAyB;AAKtD,SAAS,sBAAAC,qBAAoB,qBAAAC,0BAAyB;AActD,IAAM,iBAAiB;AAKvB,SAAS,eAAe,SAA6C;AACnE,QAAM,cACJ,SAAS,MAAM,SAAS,KAAK,SAAS,MAAM,mBAAmB;AACjE,MAAI,gBAAgB,UAAa,gBAAgB,IAAI;AACnD,WAAO;AAAA,EACT;AACA,QAAM,cAAc,QAAQ,IAAI,mBAAmB;AACnD,SAAO,gBAAgB,UAAa,gBAAgB,KAAK,SAAY;AACvE;AAEA,SAAS,gBAAgB,SAAoC,QAAmC;AAC9F,QAAM,MAAyB,CAAC;AAChC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,OAAO,CAAC,CAAC,GAAG;AAC7D,QAAI,QAAQ,eAAe,QAAQ,gBAAgB;AACjD,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,MAAI,SAAS,IAAI;AACjB,SAAO;AACT;AAEA,eAAe,qBAAqB,SAAiD;AACnF,QAAM,aAAa,eAAe,OAAO;AACzC,MAAI,eAAe,QAAW;AAC5B,WAAO;AAAA,MACL,SAAS;AAAA,QACP,GAAG;AAAA,QACH,KAAK,gBAAgB,SAAS,UAAU;AAAA,MAC1C;AAAA,MACA,SAAS,MAAqB,QAAQ,QAAQ;AAAA,IAChD;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,QAAQ,KAAK,OAAO,GAAG,cAAc,CAAC;AAC3D,SAAO;AAAA,IACL,SAAS;AAAA,MACP,GAAG;AAAA,MACH,KAAK,gBAAgB,SAAS,MAAM;AAAA,IACtC;AAAA,IACA,SAAS,YAA2B;AAClC,YAAM,GAAG,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,IACnD;AAAA,EACF;AACF;AAEA,eAAsB,cACpB,QACA,SACwB;AACxB,QAAM,EAAE,OAAO,SAAS,IAAI,kBAAkB,SAAS,GAAG;AAC1D,QAAM,cAAc,mBAAmB,OAAO,MAAM;AACpD,QAAM,UAAU,MAAM,qBAAqB,OAAO;AAElD,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,OAAO;AACxC,UAAM,OAAO,OAAO,UAAU,QAAQ,OAAO;AAC7C,UAAM,cAAc,OAAO,KAAK,OAAO,OAAO,QAAQ,OAAO;AAC7D,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,QAAQ,QAAQ;AACtB,UAAM;AAAA,EACR;AACF;;;AJjFA,SAAS,sBAA+C;AACtD,SAAO,CAAC,GAAG,cAAc;AAC3B;AAEA,SAAS,mBAAmB,WAAyE;AACnG,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG;AACxC,WAAO,oBAAoB;AAAA,EAC7B;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAsB,CAAC;AAC7B,aAAW,QAAQ,WAAW;AAC5B,QAAI,eAAe,SAAS,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,GAAG;AACpD,WAAK,IAAI,IAAI;AACb,UAAI,KAAK,IAAI;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,cAAc,QAAgB,UAAkB,SAAkC;AAC/F,QAAM,UAAU,QAAQC,MAAK,QAAQ,QAAQ,CAAC;AAC9C,QAAM,MAAM,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,QAAM,cAAc,aAAa,sBAAsB,aAAa;AACpE,QAAM,UAAU,SAAS,SAAS,EAAE,UAAU,OAAO,CAAC;AACtD,MAAI,aAAa;AACf,UAAM,MAAM,SAAS,GAAK;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,eAAsB,gBACpB,SACgC;AAChC,QAAM,YAAY,QAAQ;AAC1B,MAAI,WAAW,WAAW,GAAG;AAC3B,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,QAAM,YAAY,mBAAmB,SAAS;AAE9C,QAAM,UAAyB,MAAM,cAAc,QAAQ,MAAM;AACjE,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAE3B,MAAI;AACF,eAAW,QAAQ,WAAW;AAC5B,UAAI,SAAS,oBAAoB;AAC/B,YAAI;AACF,gBAAM,OAAO,MAAM,oBAAoB;AAAA,YACrC,SAAS,QAAQ,OAAO;AAAA,YACxB,SAAS,QAAQ;AAAA,UACnB,CAAC;AACD,gBAAMC,QAAO,MAAM,cAAc,QAAQ,QAAQ,MAAM,IAAI;AAC3D,kBAAQ,KAAKA,KAAI;AAAA,QACnB,QAAQ;AAGN,kBAAQ,KAAK,IAAI;AAAA,QACnB;AACA;AAAA,MACF;AAGA,YAAM,aAAa,QAAQ;AAC3B,YAAM,YAKF;AAAA,QACF,SAAS,QAAQ,OAAO;AAAA,QACxB,UAAU;AAAA,QACV,SAAS,QAAQ;AAAA,QACjB,GAAI,OAAO,eAAe,WAAW,EAAE,WAAW,IAAI,CAAC;AAAA,MACzD;AACA,YAAM,UAAU,MAAM,oBAAoB,SAAS;AAEnD,UAAI,YAAY,MAAM;AACpB,gBAAQ,KAAK,IAAI;AACjB;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,cAAc,QAAQ,QAAQ,MAAM,OAAO;AAC9D,cAAQ,KAAK,IAAI;AAAA,IACnB;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,QAAQ;AAAA,EACxB;AAEA,SAAO;AAAA,IACL,cAAc;AAAA,IACd;AAAA,EACF;AACF;AAEO,SAAS,sBAA+C;AAC7D,SAAO,oBAAoB;AAC7B;;;AKxGO,SAAS,8BACd,SACA,cACA,SACQ;AACR,QAAM,QAAQ,aAAa,IAAI,CAAC,MAAM;AACpC,UAAM,QAAQ,EAAE,MAAM,OAAO;AAC7B,WAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AAAA,EACpC,CAAC;AAED,QAAM,OAAO,yBAAyB,OAAO;AAC7C,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,GAAG,IAAI;AAAA,EAChB;AAEA,QAAM,YAAY,GAAG,OAAO,MAAM,MAAM,CAAC,QAAQ,MAAM,WAAW,IAAI,KAAK,GAAG,KAAK,MAAM,KAAK,IAAI,CAAC;AACnG,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,GAAG,IAAI,IAAI,SAAS;AAAA,EAC7B;AACA,QAAM,WAAW,YAAY,QAAQ,KAAK,IAAI,CAAC;AAC/C,SAAO,GAAG,IAAI,IAAI,SAAS,KAAK,QAAQ;AAC1C;","names":["join","resolveApiEndpoint","resolveSessionEnv","join","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saptools/cf-export",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Export CAP/CF project artifacts (package.json, lockfiles, .cdsrc.json, default-env.json, .npmrc) from running SAP BTP Cloud Foundry apps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"saptools-cf-export": "dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"lint": "eslint src tests --ignore-pattern tests/e2e/fixtures/fake-cf.mjs",
|
|
31
|
+
"test:unit": "vitest run --coverage",
|
|
32
|
+
"test:e2e": "playwright test",
|
|
33
|
+
"test:e2e:fake": "playwright test tests/e2e/cf-export.e2e.ts"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"sap",
|
|
37
|
+
"cloud-foundry",
|
|
38
|
+
"btp",
|
|
39
|
+
"export",
|
|
40
|
+
"default-env",
|
|
41
|
+
"pnpm-lock",
|
|
42
|
+
"cli",
|
|
43
|
+
"saptools"
|
|
44
|
+
],
|
|
45
|
+
"author": "Dong Tran",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/dongitran/saptools.git",
|
|
50
|
+
"directory": "packages/cf-export"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/dongitran/saptools/tree/main/packages/cf-export#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/dongitran/saptools/issues"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@saptools/cf-files": "^0.3.3",
|
|
58
|
+
"commander": "^13.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@playwright/test": "^1.50.0",
|
|
62
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
63
|
+
"tsup": "^8.3.0",
|
|
64
|
+
"vitest": "^3.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|