@mittwald/cli 1.7.0 → 1.8.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.
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
export default class Cp extends ExecRenderBaseCommand<typeof Cp, void> {
|
|
4
|
+
static summary: string;
|
|
5
|
+
static description: string;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static args: {
|
|
8
|
+
source: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
+
dest: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
static flags: {
|
|
12
|
+
archive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
recursive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
15
|
+
"ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
};
|
|
19
|
+
private parseContainerPath;
|
|
20
|
+
private buildScpCommand;
|
|
21
|
+
protected exec(): Promise<void>;
|
|
22
|
+
protected render(): ReactNode;
|
|
23
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { Args, Flags } from "@oclif/core";
|
|
4
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
5
|
+
import { getSSHConnectionForContainer } from "../../lib/resources/ssh/container.js";
|
|
6
|
+
import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
|
|
7
|
+
import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
|
|
8
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
9
|
+
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
10
|
+
import { spawnInProcess } from "../../rendering/process/process_exec.js";
|
|
11
|
+
import { Success } from "../../rendering/react/components/Success.js";
|
|
12
|
+
export default class Cp extends ExecRenderBaseCommand {
|
|
13
|
+
static summary = "Copy files/folders between a container and the local filesystem";
|
|
14
|
+
static description = `The syntax is similar to docker cp:
|
|
15
|
+
- Copy from container to host: mw container cp CONTAINER:SRC_PATH DEST_PATH
|
|
16
|
+
- Copy from host to container: mw container cp SRC_PATH CONTAINER:DEST_PATH
|
|
17
|
+
|
|
18
|
+
Where CONTAINER can be a container ID, short ID, or service name.`;
|
|
19
|
+
static examples = [
|
|
20
|
+
"# Copy a file from container to current directory\n<%= config.bin %> <%= command.id %> mycontainer:/app/config.json .",
|
|
21
|
+
"# Copy a file from host to container\n<%= config.bin %> <%= command.id %> ./local-file.txt mycontainer:/app/",
|
|
22
|
+
"# Copy a directory recursively\n<%= config.bin %> <%= command.id %> mycontainer:/var/log ./logs",
|
|
23
|
+
"# Copy with archive mode (preserve permissions)\n<%= config.bin %> <%= command.id %> -a mycontainer:/app/data ./backup",
|
|
24
|
+
];
|
|
25
|
+
static args = {
|
|
26
|
+
source: Args.string({
|
|
27
|
+
description: "Source path (either local path or CONTAINER:PATH)",
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
dest: Args.string({
|
|
31
|
+
description: "Destination path (either local path or CONTAINER:PATH)",
|
|
32
|
+
required: true,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
static flags = {
|
|
36
|
+
...processFlags,
|
|
37
|
+
...sshConnectionFlags,
|
|
38
|
+
...projectFlags,
|
|
39
|
+
archive: Flags.boolean({
|
|
40
|
+
char: "a",
|
|
41
|
+
summary: "archive mode (copy all uid/gid information)",
|
|
42
|
+
description: "Preserve file permissions and ownership when copying",
|
|
43
|
+
}),
|
|
44
|
+
recursive: Flags.boolean({
|
|
45
|
+
char: "r",
|
|
46
|
+
summary: "copy directories recursively",
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
parseContainerPath(path) {
|
|
50
|
+
const colonIndex = path.indexOf(":");
|
|
51
|
+
if (colonIndex === -1) {
|
|
52
|
+
return { path };
|
|
53
|
+
}
|
|
54
|
+
const container = path.substring(0, colonIndex);
|
|
55
|
+
const containerPath = path.substring(colonIndex + 1);
|
|
56
|
+
// Ensure container path starts with / (absolute path)
|
|
57
|
+
const normalizedPath = containerPath.startsWith("/")
|
|
58
|
+
? containerPath
|
|
59
|
+
: `/${containerPath}`;
|
|
60
|
+
return { container, path: normalizedPath };
|
|
61
|
+
}
|
|
62
|
+
buildScpCommand(source, dest, flags) {
|
|
63
|
+
const scpArgs = ["-o", "PasswordAuthentication=no"];
|
|
64
|
+
if (flags.archive) {
|
|
65
|
+
scpArgs.push("-p");
|
|
66
|
+
}
|
|
67
|
+
if (flags.recursive) {
|
|
68
|
+
scpArgs.push("-r");
|
|
69
|
+
}
|
|
70
|
+
scpArgs.push(source, dest);
|
|
71
|
+
return scpArgs;
|
|
72
|
+
}
|
|
73
|
+
async exec() {
|
|
74
|
+
const { args, flags } = await this.parse(Cp);
|
|
75
|
+
const sourceParsed = this.parseContainerPath(args.source);
|
|
76
|
+
const destParsed = this.parseContainerPath(args.dest);
|
|
77
|
+
// Validate that exactly one of source or dest is a container path
|
|
78
|
+
if (sourceParsed.container && destParsed.container) {
|
|
79
|
+
this.error("Cannot copy from container to container. One path must be local.");
|
|
80
|
+
}
|
|
81
|
+
if (!sourceParsed.container && !destParsed.container) {
|
|
82
|
+
this.error("At least one path must specify a container (CONTAINER:PATH format).");
|
|
83
|
+
}
|
|
84
|
+
const containerName = sourceParsed.container || destParsed.container;
|
|
85
|
+
const isDownload = !!sourceParsed.container;
|
|
86
|
+
const p = makeProcessRenderer(flags, `Copying files ${isDownload ? "from" : "to"} container`);
|
|
87
|
+
// Get container connection info
|
|
88
|
+
const [containerId, stackId] = await p.runStep("getting container connection info", async () => {
|
|
89
|
+
return withContainerAndStackId(this.apiClient, Cp, flags, { "container-id": containerName }, this.config);
|
|
90
|
+
});
|
|
91
|
+
const { host, user } = await p.runStep("establishing SSH connection", async () => {
|
|
92
|
+
return getSSHConnectionForContainer(this.apiClient, containerId, stackId, flags["ssh-user"]);
|
|
93
|
+
});
|
|
94
|
+
// Construct source and destination for SCP
|
|
95
|
+
let scpSource;
|
|
96
|
+
let scpDest;
|
|
97
|
+
if (isDownload) {
|
|
98
|
+
scpSource = `${user}@${host}:${sourceParsed.path}`;
|
|
99
|
+
scpDest = destParsed.path;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
scpSource = sourceParsed.path;
|
|
103
|
+
scpDest = `${user}@${host}:${destParsed.path}`;
|
|
104
|
+
}
|
|
105
|
+
// Automatically enable recursive for directories
|
|
106
|
+
const effectiveFlags = { ...flags };
|
|
107
|
+
if (!flags.recursive && !isDownload) {
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(scpSource)) {
|
|
110
|
+
const stats = fs.statSync(scpSource);
|
|
111
|
+
if (stats.isDirectory()) {
|
|
112
|
+
effectiveFlags.recursive = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (ignored) {
|
|
117
|
+
// Ignore errors when checking if source is a directory
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const scpCommand = this.buildScpCommand(scpSource, scpDest, effectiveFlags);
|
|
121
|
+
await spawnInProcess(p, `copying files ${isDownload ? "from" : "to"} container`, "/usr/bin/scp", scpCommand);
|
|
122
|
+
await p.complete(_jsx(CopySuccess, { isDownload: isDownload }));
|
|
123
|
+
}
|
|
124
|
+
render() {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function CopySuccess({ isDownload }) {
|
|
129
|
+
return (_jsxs(Success, { children: ["Files successfully ", isDownload ? "downloaded from" : "uploaded to", " ", "container! \uD83D\uDE80"] }));
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Hand-crafted CLI for the mittwald API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"semver-parser": "^4.1.6",
|
|
69
69
|
"shell-escape": "^0.2.0",
|
|
70
70
|
"slice-ansi": "^7.1.0",
|
|
71
|
-
"string-width": "^
|
|
71
|
+
"string-width": "^8.0.0",
|
|
72
72
|
"tempfile": "^5.0.0",
|
|
73
73
|
"uuid": "^11.0.3"
|
|
74
74
|
},
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"oclif": "^4.14.31",
|
|
95
95
|
"prettier": "~3.6.2",
|
|
96
96
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
97
|
-
"prettier-plugin-package": "^
|
|
97
|
+
"prettier-plugin-package": "^2.0.0",
|
|
98
98
|
"prettier-plugin-sort-json": "^4.1.1",
|
|
99
99
|
"rimraf": "^5.0.1",
|
|
100
100
|
"ts-jest": "^29.2.5",
|