@mittwald/cli 1.8.1 → 1.9.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/README.md +1 -0
- package/dist/commands/container/run.d.ts +8 -0
- package/dist/commands/container/run.js +52 -7
- package/dist/commands/volume/create.d.ts +20 -0
- package/dist/commands/volume/create.js +57 -0
- package/dist/commands/volume/delete.d.ts +22 -0
- package/dist/commands/volume/delete.js +89 -0
- package/dist/commands/volume/list.d.ts +24 -0
- package/dist/commands/volume/list.js +51 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -127,5 +127,6 @@ USAGE
|
|
|
127
127
|
* [`mw stack`](docs/stack.md) - Manage container stacks
|
|
128
128
|
* [`mw update`](docs/update.md) - update the mw CLI
|
|
129
129
|
* [`mw user`](docs/user.md) - Manage your own user account
|
|
130
|
+
* [`mw volume`](docs/volume.md) - Manage volumes
|
|
130
131
|
|
|
131
132
|
<!-- commandsstop -->
|
|
@@ -16,6 +16,7 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
|
|
|
16
16
|
publish: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
17
|
"publish-all": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
18
|
volume: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
"create-volumes": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
20
|
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
20
21
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
22
|
};
|
|
@@ -26,6 +27,13 @@ export declare class Run extends ExecRenderBaseCommand<typeof Run, Result> {
|
|
|
26
27
|
};
|
|
27
28
|
protected exec(): Promise<Result>;
|
|
28
29
|
private addServiceToStack;
|
|
30
|
+
/**
|
|
31
|
+
* Gets the list of named volumes that need to be created.
|
|
32
|
+
*
|
|
33
|
+
* @param stackId The stack ID to check existing volumes against
|
|
34
|
+
* @returns Array of volume names that need to be created
|
|
35
|
+
*/
|
|
36
|
+
private getVolumesToCreate;
|
|
29
37
|
/**
|
|
30
38
|
* Builds and returns the container command based on the provided image
|
|
31
39
|
* metadata and arguments.
|
|
@@ -21,12 +21,14 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
21
21
|
description: "Format: KEY=VALUE. Multiple environment variables can be specified with multiple --env flags.",
|
|
22
22
|
required: false,
|
|
23
23
|
multiple: true,
|
|
24
|
+
multipleNonGreedy: true,
|
|
24
25
|
char: "e",
|
|
25
26
|
}),
|
|
26
27
|
"env-file": Flags.string({
|
|
27
28
|
summary: "read environment variables from a file",
|
|
28
29
|
description: "The file should contain lines in the format KEY=VALUE. Multiple files can be specified with multiple --env-file flags.",
|
|
29
30
|
multiple: true,
|
|
31
|
+
multipleNonGreedy: true,
|
|
30
32
|
required: false,
|
|
31
33
|
}),
|
|
32
34
|
description: Flags.string({
|
|
@@ -53,6 +55,7 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
53
55
|
"NOTE: Please note that the usual shorthand -p is not supported for this flag, as it would conflict with the --project flag.",
|
|
54
56
|
required: false,
|
|
55
57
|
multiple: true,
|
|
58
|
+
multipleNonGreedy: true,
|
|
56
59
|
}),
|
|
57
60
|
"publish-all": Flags.boolean({
|
|
58
61
|
summary: "publish all ports that are defined in the image",
|
|
@@ -71,6 +74,13 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
71
74
|
required: false,
|
|
72
75
|
char: "v",
|
|
73
76
|
multiple: true,
|
|
77
|
+
multipleNonGreedy: true,
|
|
78
|
+
}),
|
|
79
|
+
"create-volumes": Flags.boolean({
|
|
80
|
+
summary: "automatically create named volumes that do not exist",
|
|
81
|
+
description: "When enabled, any named volumes referenced in --volume flags that do not already exist will be automatically created before starting the container.",
|
|
82
|
+
required: false,
|
|
83
|
+
default: false,
|
|
74
84
|
}),
|
|
75
85
|
};
|
|
76
86
|
static args = {
|
|
@@ -107,17 +117,52 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
107
117
|
return { serviceId };
|
|
108
118
|
}
|
|
109
119
|
async addServiceToStack(stackId, serviceName, serviceRequest) {
|
|
120
|
+
const updateData = {
|
|
121
|
+
services: {
|
|
122
|
+
[serviceName]: serviceRequest,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
// If create-volumes flag is enabled, add missing volumes to the same call
|
|
126
|
+
if (this.flags["create-volumes"] && this.flags.volume) {
|
|
127
|
+
const volumesToCreate = await this.getVolumesToCreate(stackId);
|
|
128
|
+
if (volumesToCreate.length > 0) {
|
|
129
|
+
updateData.volumes = Object.fromEntries(volumesToCreate.map((name) => [name, { name }]));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
110
132
|
const resp = await this.apiClient.container.updateStack({
|
|
111
133
|
stackId,
|
|
112
|
-
data:
|
|
113
|
-
services: {
|
|
114
|
-
[serviceName]: serviceRequest,
|
|
115
|
-
},
|
|
116
|
-
},
|
|
134
|
+
data: updateData,
|
|
117
135
|
});
|
|
118
136
|
assertStatus(resp, 200);
|
|
119
137
|
return resp.data;
|
|
120
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Gets the list of named volumes that need to be created.
|
|
141
|
+
*
|
|
142
|
+
* @param stackId The stack ID to check existing volumes against
|
|
143
|
+
* @returns Array of volume names that need to be created
|
|
144
|
+
*/
|
|
145
|
+
async getVolumesToCreate(stackId) {
|
|
146
|
+
if (!this.flags.volume) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
// Get current stack state to check existing volumes
|
|
150
|
+
const currentStackResp = await this.apiClient.container.getStack({
|
|
151
|
+
stackId,
|
|
152
|
+
});
|
|
153
|
+
assertStatus(currentStackResp, 200);
|
|
154
|
+
const existingVolumes = (currentStackResp.data.volumes || []).map((v) => v.name);
|
|
155
|
+
// Parse volume flags to extract named volumes (not file paths)
|
|
156
|
+
const namedVolumes = new Set(this.flags.volume
|
|
157
|
+
.filter((volume) => {
|
|
158
|
+
const [source] = volume.split(":");
|
|
159
|
+
// Named volumes typically don't start with /
|
|
160
|
+
return source && !source.startsWith("/");
|
|
161
|
+
})
|
|
162
|
+
.map((volume) => volume.split(":")[0]));
|
|
163
|
+
// Find volumes that need to be created
|
|
164
|
+
return Array.from(namedVolumes).filter((volumeName) => !existingVolumes.includes(volumeName));
|
|
165
|
+
}
|
|
121
166
|
/**
|
|
122
167
|
* Builds and returns the container command based on the provided image
|
|
123
168
|
* metadata and arguments.
|
|
@@ -150,7 +195,7 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
150
195
|
? [this.flags.entrypoint]
|
|
151
196
|
: imageMeta.entrypoint;
|
|
152
197
|
const description = this.flags.description ?? serviceName;
|
|
153
|
-
const
|
|
198
|
+
const environment = await parseEnvironmentVariables(this.flags.env, this.flags["env-file"]);
|
|
154
199
|
const ports = getPortMappings(imageMeta, this.flags["publish-all"], this.flags.publish);
|
|
155
200
|
const volumes = this.flags.volume;
|
|
156
201
|
return {
|
|
@@ -158,7 +203,7 @@ export class Run extends ExecRenderBaseCommand {
|
|
|
158
203
|
command,
|
|
159
204
|
entrypoint,
|
|
160
205
|
description,
|
|
161
|
-
|
|
206
|
+
environment,
|
|
162
207
|
ports,
|
|
163
208
|
volumes,
|
|
164
209
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
type Result = {
|
|
4
|
+
volumeName: string;
|
|
5
|
+
stackId: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class Create extends ExecRenderBaseCommand<typeof Create, Result> {
|
|
8
|
+
static summary: string;
|
|
9
|
+
static description: string;
|
|
10
|
+
static flags: {
|
|
11
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
13
|
+
};
|
|
14
|
+
static args: {
|
|
15
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
protected exec(): Promise<Result>;
|
|
18
|
+
protected render({ volumeName }: Result): ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
4
|
+
import { Args } from "@oclif/core";
|
|
5
|
+
import { assertStatus } from "@mittwald/api-client-commons";
|
|
6
|
+
import { Success } from "../../rendering/react/components/Success.js";
|
|
7
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
8
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
9
|
+
export class Create extends ExecRenderBaseCommand {
|
|
10
|
+
static summary = "Create a new volume";
|
|
11
|
+
static description = "Creates a new named volume in the project stack. The volume will be available for use by containers.";
|
|
12
|
+
static flags = {
|
|
13
|
+
...projectFlags,
|
|
14
|
+
...processFlags,
|
|
15
|
+
};
|
|
16
|
+
static args = {
|
|
17
|
+
name: Args.string({
|
|
18
|
+
description: "name of the volume to create",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async exec() {
|
|
23
|
+
const process = makeProcessRenderer(this.flags, "Creating a new volume");
|
|
24
|
+
const projectId = await this.withProjectId(Create);
|
|
25
|
+
const stackId = projectId; // In mStudio, project and stack are the same for volumes
|
|
26
|
+
const { name: volumeName } = this.args;
|
|
27
|
+
// Get current stack state
|
|
28
|
+
const currentStack = await process.runStep("getting current stack state", async () => {
|
|
29
|
+
const r = await this.apiClient.container.getStack({ stackId });
|
|
30
|
+
assertStatus(r, 200);
|
|
31
|
+
return r.data;
|
|
32
|
+
});
|
|
33
|
+
// Check if volume already exists
|
|
34
|
+
if ((currentStack.volumes || []).some((v) => v.name === volumeName)) {
|
|
35
|
+
throw new Error(`Volume "${volumeName}" already exists`);
|
|
36
|
+
}
|
|
37
|
+
// Update stack to include the new volume
|
|
38
|
+
await process.runStep("creating volume", async () => {
|
|
39
|
+
const r = await this.apiClient.container.updateStack({
|
|
40
|
+
stackId,
|
|
41
|
+
data: {
|
|
42
|
+
volumes: {
|
|
43
|
+
[volumeName]: { name: volumeName },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
assertStatus(r, 200);
|
|
48
|
+
});
|
|
49
|
+
await process.complete(_jsxs(Success, { children: ["Volume ", _jsx(Value, { children: volumeName }), " was successfully created."] }));
|
|
50
|
+
return { volumeName, stackId };
|
|
51
|
+
}
|
|
52
|
+
render({ volumeName }) {
|
|
53
|
+
if (this.flags.quiet) {
|
|
54
|
+
return volumeName;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
type Result = {
|
|
4
|
+
volumeName: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class Delete extends ExecRenderBaseCommand<typeof Delete, Result> {
|
|
7
|
+
static summary: string;
|
|
8
|
+
static description: string;
|
|
9
|
+
static aliases: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
14
|
+
};
|
|
15
|
+
static args: {
|
|
16
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
17
|
+
};
|
|
18
|
+
protected exec(): Promise<Result>;
|
|
19
|
+
private checkVolumeInUse;
|
|
20
|
+
protected render({ volumeName }: Result): ReactNode;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExecRenderBaseCommand } from "../../lib/basecommands/ExecRenderBaseCommand.js";
|
|
3
|
+
import { makeProcessRenderer, processFlags, } from "../../rendering/process/process_flags.js";
|
|
4
|
+
import { Args, Flags } from "@oclif/core";
|
|
5
|
+
import { assertStatus } from "@mittwald/api-client-commons";
|
|
6
|
+
import { Success } from "../../rendering/react/components/Success.js";
|
|
7
|
+
import { Value } from "../../rendering/react/components/Value.js";
|
|
8
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
9
|
+
export class Delete extends ExecRenderBaseCommand {
|
|
10
|
+
static summary = "Remove one or more volumes";
|
|
11
|
+
static description = "Removes named volumes from the project stack. Be careful as this will permanently delete the volume data.";
|
|
12
|
+
static aliases = ["volume:rm"];
|
|
13
|
+
static flags = {
|
|
14
|
+
...projectFlags,
|
|
15
|
+
...processFlags,
|
|
16
|
+
force: Flags.boolean({
|
|
17
|
+
summary: "force removal without confirmation",
|
|
18
|
+
char: "f",
|
|
19
|
+
default: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static args = {
|
|
23
|
+
name: Args.string({
|
|
24
|
+
description: "name of the volume to remove",
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async exec() {
|
|
29
|
+
const process = makeProcessRenderer(this.flags, "Removing volume");
|
|
30
|
+
const projectId = await this.withProjectId(Delete);
|
|
31
|
+
const stackId = projectId; // In mStudio, project and stack are the same for volumes
|
|
32
|
+
const { name: volumeName } = this.args;
|
|
33
|
+
// Get current stack state
|
|
34
|
+
const currentStack = await process.runStep("getting current stack state", async () => {
|
|
35
|
+
const r = await this.apiClient.container.getStack({ stackId });
|
|
36
|
+
assertStatus(r, 200);
|
|
37
|
+
return r.data;
|
|
38
|
+
});
|
|
39
|
+
// Check if volume exists
|
|
40
|
+
if (!(currentStack.volumes || []).some((v) => v.name === volumeName)) {
|
|
41
|
+
throw new Error(`Volume "${volumeName}" does not exist`);
|
|
42
|
+
}
|
|
43
|
+
// Check if volume is in use by any services
|
|
44
|
+
const volumeInUse = this.checkVolumeInUse(currentStack.services, volumeName);
|
|
45
|
+
if (volumeInUse && !this.flags.force) {
|
|
46
|
+
throw new Error(`Volume "${volumeName}" is in use by one or more containers. Use --force to remove anyway.`);
|
|
47
|
+
}
|
|
48
|
+
// Delete the actual volume data if it exists
|
|
49
|
+
await process.runStep("deleting volume", async () => {
|
|
50
|
+
const volumesResponse = await this.apiClient.container.listVolumes({
|
|
51
|
+
projectId,
|
|
52
|
+
});
|
|
53
|
+
assertStatus(volumesResponse, 200);
|
|
54
|
+
const volume = volumesResponse.data.find((v) => v.name === volumeName && v.stackId === stackId);
|
|
55
|
+
if (volume) {
|
|
56
|
+
const deleteResponse = await this.apiClient.container.deleteVolume({
|
|
57
|
+
stackId,
|
|
58
|
+
volumeId: volume.id,
|
|
59
|
+
});
|
|
60
|
+
assertStatus(deleteResponse, 204);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
await process.complete(_jsxs(Success, { children: ["Volume ", _jsx(Value, { children: volumeName }), " was successfully removed."] }));
|
|
64
|
+
return { volumeName };
|
|
65
|
+
}
|
|
66
|
+
checkVolumeInUse(services, volumeName) {
|
|
67
|
+
if (!services) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
for (const service of services) {
|
|
71
|
+
const volumes = service.deployedState?.volumes;
|
|
72
|
+
if (volumes) {
|
|
73
|
+
for (const volumeMount of volumes) {
|
|
74
|
+
// Check if this is a named volume (not a bind mount)
|
|
75
|
+
if (volumeMount.includes(":") &&
|
|
76
|
+
volumeMount.split(":")[0] === volumeName) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
render({ volumeName }) {
|
|
85
|
+
if (this.flags.quiet) {
|
|
86
|
+
return volumeName;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Simplify } from "@mittwald/api-client-commons";
|
|
2
|
+
import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
|
|
3
|
+
import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
|
|
4
|
+
import { ListColumns } from "../../rendering/formatter/Table.js";
|
|
5
|
+
type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2ProjectsProjectIdVolumes.Get.Responses.$200.Content.ApplicationJson[number]>;
|
|
6
|
+
type Response = Awaited<ReturnType<MittwaldAPIV2Client["container"]["listVolumes"]>>;
|
|
7
|
+
export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
|
|
8
|
+
static description: string;
|
|
9
|
+
static aliases: string[];
|
|
10
|
+
static args: {};
|
|
11
|
+
static flags: {
|
|
12
|
+
"project-id": import("@oclif/core/interfaces").OptionFlag<string>;
|
|
13
|
+
output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
|
|
14
|
+
extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
"no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
"no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
"no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
"csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
|
|
19
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
getData(): Promise<Response>;
|
|
22
|
+
protected getColumns(data: ResponseItem[]): ListColumns<ResponseItem>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
|
|
2
|
+
import { projectFlags } from "../../lib/resources/project/flags.js";
|
|
3
|
+
import { makeDateRendererForFormat } from "../../rendering/textformat/formatDate.js";
|
|
4
|
+
import ByteQuantity from "../../lib/units/ByteQuantity.js";
|
|
5
|
+
export class List extends ListBaseCommand {
|
|
6
|
+
static description = "List volumes belonging to a project.";
|
|
7
|
+
static aliases = ["volume:ls"];
|
|
8
|
+
static args = {};
|
|
9
|
+
static flags = {
|
|
10
|
+
...ListBaseCommand.baseFlags,
|
|
11
|
+
...projectFlags,
|
|
12
|
+
};
|
|
13
|
+
async getData() {
|
|
14
|
+
const projectId = await this.withProjectId(List);
|
|
15
|
+
return this.apiClient.container.listVolumes({ projectId });
|
|
16
|
+
}
|
|
17
|
+
getColumns(data) {
|
|
18
|
+
const { id } = super.getColumns(data);
|
|
19
|
+
const dateFormatter = makeDateRendererForFormat(this.flags.output, !this.flags["no-relative-dates"]);
|
|
20
|
+
return {
|
|
21
|
+
id: {
|
|
22
|
+
...id,
|
|
23
|
+
extended: true,
|
|
24
|
+
},
|
|
25
|
+
name: {
|
|
26
|
+
get: (volume) => volume.name,
|
|
27
|
+
},
|
|
28
|
+
stackId: {
|
|
29
|
+
header: "Stack",
|
|
30
|
+
extended: true,
|
|
31
|
+
exactWidth: 40,
|
|
32
|
+
},
|
|
33
|
+
usedIn: {
|
|
34
|
+
header: "Used in",
|
|
35
|
+
get: (volume) => {
|
|
36
|
+
if (volume.orphaned) {
|
|
37
|
+
return "orphaned";
|
|
38
|
+
}
|
|
39
|
+
return (volume.linkedServices ?? []).length + " services";
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
storageUsage: {
|
|
43
|
+
header: "Size",
|
|
44
|
+
get: (volume) => ByteQuantity.fromBytes(volume.storageUsageInBytes).format() +
|
|
45
|
+
" (updated " +
|
|
46
|
+
dateFormatter(volume.storageUsageInBytesSetAt) +
|
|
47
|
+
")",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Hand-crafted CLI for the mittwald API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -261,6 +261,9 @@
|
|
|
261
261
|
"description": "Manage your SSH keys"
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
+
},
|
|
265
|
+
"volume": {
|
|
266
|
+
"description": "Manage volumes"
|
|
264
267
|
}
|
|
265
268
|
},
|
|
266
269
|
"plugins": [
|