@cloud-cli/vm 1.3.2 → 1.4.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/dist/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
- interface NameOption {
1
+ import { help } from "@cloud-cli/cli";
2
+ interface Options {
3
+ _?: string[];
4
+ }
5
+ interface NameOption extends Options {
2
6
  name: string;
3
7
  }
4
- interface NameAndPathOption {
8
+ interface NameAndPathOption extends Options {
5
9
  name: string;
6
10
  path?: string;
7
11
  }
@@ -28,5 +32,6 @@ declare const _default: {
28
32
  rm: typeof rm;
29
33
  cat: typeof cat;
30
34
  fixPermissions: typeof fixPermissions;
35
+ [help]: () => string;
31
36
  };
32
37
  export default _default;
package/dist/index.js CHANGED
@@ -1,61 +1,70 @@
1
- import { join } from 'node:path';
2
- import { readFile } from 'node:fs/promises';
3
- import { exec } from '@cloud-cli/exec';
1
+ import { join } from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { exec } from "@cloud-cli/exec";
4
+ import { help } from "@cloud-cli/cli";
4
5
  const nameRe = /^[a-z][a-z0-9-]+$/;
5
6
  const isValidName = (name) => nameRe.test(name);
6
- const invalidNameError = new Error('Invalid name');
7
+ const invalidNameError = new Error("Invalid name");
8
+ const readOptions = (options) => {
9
+ options.name || (options.name = options._?.[0]);
10
+ if (!isValidName(options.name)) {
11
+ throw invalidNameError;
12
+ }
13
+ };
7
14
  async function list() {
8
- const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
15
+ const output = await exec("docker", ["volume", "ls", "--format={{.Name}}"]);
9
16
  if (!output.ok) {
10
- throw new Error('Unable to list volumes');
17
+ throw new Error("Unable to list volumes");
11
18
  }
12
- return output.stdout.trim().split('\n');
19
+ return output.stdout.trim().split("\n");
13
20
  }
14
21
  async function getVolumeMountpoint(name) {
15
- const output = await exec('docker', ['volume', 'inspect', name]);
22
+ const output = await exec("docker", ["volume", "inspect", name]);
16
23
  const volumes = JSON.parse(output.stdout);
17
24
  if (!volumes.length) {
18
- throw new Error('Volume not found');
25
+ throw new Error("Volume not found");
19
26
  }
20
27
  return volumes[0].Mountpoint;
21
28
  }
22
29
  async function ls(options) {
23
- const { name, path = '' } = options;
30
+ readOptions(options);
31
+ const { name, path = "" } = options;
24
32
  const root = await getVolumeMountpoint(name);
25
- const files = await exec('ls', ['-1', join(root, path)]);
26
- return files.stdout.trim().split('\n').filter(Boolean);
33
+ const files = await exec("ls", ["-1", join(root, path)]);
34
+ return files.stdout.trim().split("\n").filter(Boolean);
27
35
  }
28
36
  async function fixPermissions(options) {
37
+ readOptions(options);
29
38
  const { name } = options;
30
39
  const root = await getVolumeMountpoint(name);
31
- const result = await exec('chmod', ['-R', 'a+w', root]);
40
+ const result = await exec("chmod", ["-R", "a+w", root]);
32
41
  return result.ok;
33
42
  }
34
43
  async function rm(options) {
44
+ readOptions(options);
35
45
  const { name, path } = options;
36
46
  if (!path) {
37
- throw new Error('Path not specified');
47
+ throw new Error("Path not specified");
38
48
  }
39
49
  const root = await getVolumeMountpoint(name);
40
- const result = await exec('rm', ['-r', join(root, path)]);
50
+ const result = await exec("rm", ["-r", join(root, path)]);
41
51
  return result.ok;
42
52
  }
43
53
  async function cat(options) {
54
+ readOptions(options);
44
55
  const { name, path } = options;
45
56
  if (!path) {
46
- throw new Error('Path not specified');
57
+ throw new Error("Path not specified");
47
58
  }
48
59
  const root = await getVolumeMountpoint(name);
49
- return await readFile(join(root, path), 'utf8');
60
+ return await readFile(join(root, path), "utf8");
50
61
  }
51
62
  async function show(options) {
52
- if (!isValidName(options.name)) {
53
- throw invalidNameError;
54
- }
55
- const output = await exec('docker', ['volume', 'inspect', options.name]);
63
+ readOptions(options);
64
+ const output = await exec("docker", ["volume", "inspect", options.name]);
56
65
  const list = JSON.parse(output.stdout);
57
66
  if (!list.length) {
58
- throw new Error('Volume not found');
67
+ throw new Error("Volume not found");
59
68
  }
60
69
  const { Name, Labels, CreatedAt } = list[0];
61
70
  return {
@@ -65,27 +74,50 @@ async function show(options) {
65
74
  };
66
75
  }
67
76
  async function add(options) {
68
- if (!isValidName(options.name)) {
69
- throw invalidNameError;
70
- }
71
- const output = await exec('docker', ['volume', 'create', options.name]);
77
+ readOptions(options);
78
+ const output = await exec("docker", ["volume", "create", options.name]);
72
79
  if (output.ok) {
80
+ await fixPermissions(options);
73
81
  return true;
74
82
  }
75
- throw new Error('Unable to create volume');
83
+ throw new Error("Unable to create volume");
76
84
  }
77
85
  async function remove(options) {
78
- if (!isValidName(options.name)) {
79
- throw invalidNameError;
80
- }
81
- const output = await exec('docker', ['volume', 'rm', options.name]);
86
+ readOptions(options);
87
+ const output = await exec("docker", ["volume", "rm", options.name]);
82
88
  if (output.ok) {
83
89
  return true;
84
90
  }
85
- throw new Error('Unable to remove volume');
91
+ throw new Error("Unable to remove volume");
86
92
  }
87
93
  async function prune() {
88
- await exec('docker', ['volume', 'prune']);
89
- return '';
94
+ await exec("docker", ["volume", "prune"]);
95
+ return "";
90
96
  }
91
- export default { add, remove, list, show, prune, ls, rm, cat, fixPermissions };
97
+ export default {
98
+ add,
99
+ remove,
100
+ list,
101
+ show,
102
+ prune,
103
+ ls,
104
+ rm,
105
+ cat,
106
+ fixPermissions,
107
+ [help]: () => `Manage Docker volumes
108
+
109
+ Available commands:
110
+ vm.add --name <name> - Create a new Docker volume (name must be lowercase alphanumeric with hyphens)
111
+ vm.remove --name <name> - Remove a Docker volume
112
+ vm.list - List all Docker volumes
113
+ vm.ls --name <name> [--path <path>] - List files in a volume
114
+ vm.cat --name <name> --path <path> - Read a file from a volume
115
+ vm.show --name <name> - Show volume details
116
+ vm.fixPermissions --name <name> - Fix permissions on a volume
117
+ vm.rm --name <name> --path <path> - Remove a file or directory from a volume
118
+ vm.prune - Remove all unused Docker volumes
119
+
120
+ Options:
121
+ name - Volume name (lowercase alphanumeric with hyphens)
122
+ path - Path within the volume`,
123
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-cli/vm",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Docker volume management",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,6 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@cloud-cli/prettier-config": "^1.0.0",
27
27
  "@cloud-cli/typescript-config": "^1.0.1",
28
+ "@types/node": "^20.19.43",
28
29
  "typescript": "^5.4.5",
29
30
  "vitest": "^1.6.0"
30
31
  },
@@ -32,6 +33,6 @@
32
33
  "@cloud-cli/exec": "^1.0.1"
33
34
  },
34
35
  "peerDependencies": {
35
- "@cloud-cli/cli": "^2.0.0"
36
+ "@cloud-cli/cli": "^2.2.0"
36
37
  }
37
38
  }
package/src/index.spec.ts CHANGED
@@ -1,17 +1,22 @@
1
- import { describe, expect, it, vi, beforeEach } from 'vitest';
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { help } from "@cloud-cli/cli";
3
+ import vm from "./index";
2
4
 
3
5
  const execMocks = vi.hoisted(() => ({
4
6
  exec: vi.fn(),
5
7
  readFile: vi.fn(),
6
8
  }));
7
9
 
8
- vi.mock('get-port', () => ({ default: vi.fn().mockReturnValue(1234) }));
9
- vi.mock('@cloud-cli/exec', () => ({ exec: execMocks.exec }));
10
-
11
- vi.mock('@cloud-cli/exec', () => ({ exec: execMocks.exec }));
12
- vi.mock('node:fs/promises', () => ({ readFile: execMocks.readFile }));
13
-
14
- import vm from './index';
10
+ vi.mock("get-port", () => ({ default: vi.fn().mockReturnValue(1234) }));
11
+ vi.mock("@cloud-cli/exec", () => ({ exec: execMocks.exec }));
12
+ vi.mock("node:fs/promises", () => ({ readFile: execMocks.readFile }));
13
+ vi.mock("@cloud-cli/cli", async (importOriginal) => {
14
+ const mod: any = await importOriginal();
15
+ return {
16
+ ...mod,
17
+ help: mod.help,
18
+ };
19
+ });
15
20
 
16
21
  const inspectOutput = `[{
17
22
  "CreatedAt": "2023-03-10T10:15:10Z",
@@ -26,14 +31,40 @@ const inspectOutput = `[{
26
31
  "Scope": "local"
27
32
  }]`;
28
33
 
29
- describe('volume manager', () => {
34
+ describe("help", () => {
35
+ it("should have a [help] Symbol export that is a function", () => {
36
+ expect(vm[help]).toBeDefined();
37
+ expect(typeof vm[help]).toBe("function");
38
+ });
39
+
40
+ it("should return a string help text", () => {
41
+ const helpText = vm[help]();
42
+ expect(typeof helpText).toBe("string");
43
+ expect(helpText).toContain("Docker");
44
+ expect(helpText).toContain("vm.add");
45
+ expect(helpText).toContain("vm.remove");
46
+ expect(helpText).toContain("vm.list");
47
+ expect(helpText).toContain("vm.ls");
48
+ expect(helpText).toContain("vm.cat");
49
+ expect(helpText).toContain("vm.show");
50
+ expect(helpText).toContain("vm.fixPermissions");
51
+ expect(helpText).toContain("vm.rm");
52
+ expect(helpText).toContain("vm.prune");
53
+ });
54
+
55
+ it('should not expose "help" as a normal command key', () => {
56
+ expect(Object.hasOwn(vm, "help")).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe("volume manager", () => {
30
61
  beforeEach(() => {
31
62
  execMocks.exec.mockReset();
32
63
  execMocks.readFile.mockReset();
33
64
  });
34
65
 
35
- describe('list', () => {
36
- it('should list the available volumes', async () => {
66
+ describe("list", () => {
67
+ it("should list the available volumes", async () => {
37
68
  const execOutput = {
38
69
  ok: true,
39
70
  stdout: `test\ntest2\n`,
@@ -41,178 +72,176 @@ describe('volume manager', () => {
41
72
 
42
73
  execMocks.exec.mockResolvedValue(execOutput);
43
74
 
44
- await expect(vm.list()).resolves.toEqual(['test', 'test2']);
45
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'ls', '--format={{.Name}}']);
75
+ await expect(vm.list()).resolves.toEqual(["test", "test2"]);
76
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "ls", "--format={{.Name}}"]);
46
77
  });
47
78
 
48
- it('should reject in case of error', async () => {
79
+ it("should reject in case of error", async () => {
49
80
  const output = { ok: false };
50
81
  execMocks.exec.mockResolvedValue(output);
51
82
 
52
- await expect(vm.list()).rejects.toThrowError('Unable to list volumes');
83
+ await expect(vm.list()).rejects.toThrowError("Unable to list volumes");
53
84
  });
54
85
  });
55
86
 
56
- describe('fixPermissions', () => {
57
- it('should fix write permissions on volume folder', async () => {
58
- const outputs = [inspectOutput, ''];
87
+ describe("fixPermissions", () => {
88
+ it("should fix write permissions on volume folder", async () => {
89
+ const outputs = [inspectOutput, ""];
59
90
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
60
91
 
61
- await expect(vm.fixPermissions({ name: 'test' })).resolves.toEqual(true);
62
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
63
- expect(execMocks.exec).toHaveBeenCalledWith('chmod', ['-R', 'a+w', '/var/lib/docker/volumes/test/_data']);
92
+ await expect(vm.fixPermissions({ name: "test" })).resolves.toEqual(true);
93
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "inspect", "test"]);
94
+ expect(execMocks.exec).toHaveBeenCalledWith("chmod", ["-R", "a+w", "/var/lib/docker/volumes/test/_data"]);
64
95
  });
65
96
  });
66
97
 
67
- describe('ls', () => {
68
- it('should list files of a volume and path', async () => {
69
- const outputs = [inspectOutput, 'dir', inspectOutput, 'a.txt\nb.txt'];
98
+ describe("ls", () => {
99
+ it("should list files of a volume and path", async () => {
100
+ const outputs = [inspectOutput, "dir", inspectOutput, "a.txt\nb.txt"];
70
101
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
71
102
 
72
- await expect(vm.ls({ name: 'test' })).resolves.toEqual(['dir']);
73
- await expect(vm.ls({ name: 'test', path: 'dir' })).resolves.toEqual(['a.txt', 'b.txt']);
103
+ await expect(vm.ls({ name: "test" })).resolves.toEqual(["dir"]);
104
+ await expect(vm.ls({ name: "test", path: "dir" })).resolves.toEqual(["a.txt", "b.txt"]);
74
105
 
75
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
76
- expect(execMocks.exec).toHaveBeenCalledWith('ls', ['-1', '/var/lib/docker/volumes/test/_data']);
77
- expect(execMocks.exec).toHaveBeenCalledWith('ls', ['-1', '/var/lib/docker/volumes/test/_data/dir']);
106
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "inspect", "test"]);
107
+ expect(execMocks.exec).toHaveBeenCalledWith("ls", ["-1", "/var/lib/docker/volumes/test/_data"]);
108
+ expect(execMocks.exec).toHaveBeenCalledWith("ls", ["-1", "/var/lib/docker/volumes/test/_data/dir"]);
78
109
  });
79
110
 
80
- it('should throw an error if volume name is invalid', async () => {
81
- execMocks.exec.mockImplementation(() => ({ ok: true, stdout: '[]' }));
82
- await expect(vm.ls({ name: 'notfound' })).rejects.toThrowError('Volume not found');
111
+ it("should throw an error if volume name is invalid", async () => {
112
+ execMocks.exec.mockImplementation(() => ({ ok: true, stdout: "[]" }));
113
+ await expect(vm.ls({ name: "notfound" })).rejects.toThrowError("Volume not found");
83
114
  });
84
115
  });
85
116
 
86
- describe('rm', () => {
87
- it('should remove files of a volume by path', async () => {
88
- const outputs = [inspectOutput, ''];
117
+ describe("rm", () => {
118
+ it("should remove files of a volume by path", async () => {
119
+ const outputs = [inspectOutput, ""];
89
120
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: outputs.shift() }));
90
121
 
91
- await expect(vm.rm({ name: 'test', path: 'file.txt' })).resolves.toEqual(true);
122
+ await expect(vm.rm({ name: "test", path: "file.txt" })).resolves.toEqual(true);
92
123
 
93
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
94
- expect(execMocks.exec).toHaveBeenCalledWith('rm', ['-r', '/var/lib/docker/volumes/test/_data/file.txt']);
124
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "inspect", "test"]);
125
+ expect(execMocks.exec).toHaveBeenCalledWith("rm", ["-r", "/var/lib/docker/volumes/test/_data/file.txt"]);
95
126
  });
96
127
 
97
- it('should throw an error if volume name is invalid', async () => {
98
- execMocks.exec.mockImplementation(() => ({ ok: true, stdout: '[]' }));
99
- await expect(vm.rm({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
128
+ it("should throw an error if volume name is invalid", async () => {
129
+ execMocks.exec.mockImplementation(() => ({ ok: true, stdout: "[]" }));
130
+ await expect(vm.rm({ name: "notfound", path: "file.txt" })).rejects.toThrowError("Volume not found");
100
131
  });
101
132
 
102
- it('should throw an error if path is invalid', async () => {
133
+ it("should throw an error if path is invalid", async () => {
103
134
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
104
- await expect(vm.rm({ name: 'nopath' })).rejects.toThrowError('Path not specified');
135
+ await expect(vm.rm({ name: "nopath" })).rejects.toThrowError("Path not specified");
105
136
  expect(execMocks.exec).not.toHaveBeenCalled();
106
137
  });
107
138
  });
108
139
 
109
- describe('cat', () => {
110
- it('should read the content of a file in a volume by path', async () => {
140
+ describe("cat", () => {
141
+ it("should read the content of a file in a volume by path", async () => {
111
142
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
112
- execMocks.readFile.mockImplementation(() => 'test file');
143
+ execMocks.readFile.mockImplementation(() => "test file");
113
144
 
114
- await expect(vm.cat({ name: 'test', path: 'file.txt' })).resolves.toEqual('test file');
145
+ await expect(vm.cat({ name: "test", path: "file.txt" })).resolves.toEqual("test file");
115
146
 
116
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
117
- expect(execMocks.readFile).toHaveBeenCalledWith('/var/lib/docker/volumes/test/_data/file.txt', 'utf8');
147
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "inspect", "test"]);
148
+ expect(execMocks.readFile).toHaveBeenCalledWith("/var/lib/docker/volumes/test/_data/file.txt", "utf8");
118
149
  });
119
150
 
120
- it('should throw an error if volume name is invalid', async () => {
121
- execMocks.exec.mockImplementation(() => ({ ok: true, stdout: '[]' }));
122
- await expect(vm.cat({ name: 'notfound', path: 'file.txt' })).rejects.toThrowError('Volume not found');
151
+ it("should throw an error if volume name is invalid", async () => {
152
+ execMocks.exec.mockImplementation(() => ({ ok: true, stdout: "[]" }));
153
+ await expect(vm.cat({ name: "notfound", path: "file.txt" })).rejects.toThrowError("Volume not found");
123
154
  });
124
155
 
125
- it('should throw an error if path is invalid', async () => {
156
+ it("should throw an error if path is invalid", async () => {
126
157
  execMocks.exec.mockImplementation(() => ({ ok: true, stdout: inspectOutput }));
127
- await expect(vm.cat({ name: 'nopath' })).rejects.toThrowError('Path not specified');
158
+ await expect(vm.cat({ name: "nopath" })).rejects.toThrowError("Path not specified");
128
159
  expect(execMocks.exec).not.toHaveBeenCalled();
129
160
  });
130
161
  });
131
162
 
132
- describe('show', () => {
133
- it('should show details of a volume', async () => {
163
+ describe("show", () => {
164
+ it("should show details of a volume", async () => {
134
165
  const execOutput = { ok: true, stdout: inspectOutput };
135
166
  execMocks.exec.mockResolvedValue(execOutput);
136
167
 
137
- await expect(vm.show({ name: 'test' })).resolves.toEqual({
138
- name: 'test',
139
- createdAt: '2023-03-10T10:15:10Z',
168
+ await expect(vm.show({ name: "test" })).resolves.toEqual({
169
+ name: "test",
170
+ createdAt: "2023-03-10T10:15:10Z",
140
171
  labels: {
141
- origin: 'test/container',
142
- version: '534524',
172
+ origin: "test/container",
173
+ version: "534524",
143
174
  },
144
175
  });
145
176
 
146
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'inspect', 'test']);
177
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "inspect", "test"]);
147
178
  });
148
179
 
149
- it('should throw an error if volume name is invalid', async () => {
150
- await expect(vm.show({ name: 'In$%valid' })).rejects.toThrowError('Invalid name');
180
+ it("should throw an error if volume name is invalid", async () => {
181
+ await expect(vm.show({ name: "In$%valid" })).rejects.toThrowError("Invalid name");
151
182
  expect(execMocks.exec).not.toHaveBeenCalled();
152
183
  });
153
184
 
154
- it('should throw error if volume does not exist', async () => {
155
- const output = { ok: true, stdout: '[]' };
185
+ it("should throw error if volume does not exist", async () => {
186
+ const output = { ok: true, stdout: "[]" };
156
187
  execMocks.exec.mockResolvedValue(output);
157
188
 
158
- await expect(vm.show({ name: 'invalid' })).rejects.toThrowError('Volume not found');
189
+ await expect(vm.show({ name: "invalid" })).rejects.toThrowError("Volume not found");
159
190
  });
160
191
  });
161
192
 
162
- describe('add', () => {
163
- it('should add a volume', async () => {
164
- const execOutput = { ok: true, stdout: 'test' };
165
- execMocks.exec.mockResolvedValue(execOutput);
193
+ describe("add", () => {
194
+ it("should add a volume", async () => {
195
+ execMocks.exec
196
+ .mockResolvedValueOnce({ ok: true, stdout: "test" })
197
+ .mockResolvedValueOnce({ ok: true, stdout: inspectOutput })
198
+ .mockResolvedValueOnce({ ok: true, stdout: "" });
166
199
 
167
- await expect(vm.add({ name: 'test' })).resolves.toEqual(true);
200
+ await expect(vm.add({ name: "test" })).resolves.toEqual(true);
168
201
 
169
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'create', 'test']);
202
+ expect(execMocks.exec).toHaveBeenCalledWith("docker", ["volume", "create", "test"]);
170
203
  });
171
204
 
172
- it('should throw error if volume name is invalid', async () => {
173
- await expect(vm.add({ name: 'In$%valid' })).rejects.toThrowError('Invalid name');
205
+ it("should throw error if volume name is invalid", async () => {
206
+ await expect(vm.add({ name: "In$%valid" })).rejects.toThrowError("Invalid name");
174
207
  expect(execMocks.exec).not.toHaveBeenCalled();
175
208
  });
176
209
 
177
- it('should throw error if volume creation fails', async () => {
178
- const output = { ok: false, stdout: '' };
210
+ it("should throw error if volume creation fails", async () => {
211
+ const output = { ok: false, stdout: "" };
179
212
  execMocks.exec.mockResolvedValue(output);
180
213
 
181
- await expect(vm.add({ name: 'failed' })).rejects.toThrowError('Unable to create volume');
214
+ await expect(vm.add({ name: "failed" })).rejects.toThrowError("Unable to create volume");
182
215
  });
183
216
  });
184
217
 
185
- describe('remove', () => {
186
- it('should add a volume', async () => {
187
- const execOutput = { ok: true, stdout: '' };
218
+ describe("remove", () => {
219
+ it("should add a volume", async () => {
220
+ const execOutput = { ok: true, stdout: "" };
188
221
  execMocks.exec.mockResolvedValue(execOutput);
189
222
 
190
- await expect(vm.remove({ name: 'test' })).resolves.toEqual(true);
191
-
192
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'rm', 'test']);
223
+ await expect(vm.remove({ name: "test" })).resolves.toEqual(true);
193
224
  });
194
225
 
195
- it('should throw error if volume name is invalid', async () => {
196
- await expect(vm.remove({ name: 'In$%valid' })).rejects.toThrowError('Invalid name');
226
+ it("should throw error if volume name is invalid", async () => {
227
+ await expect(vm.remove({ name: "In$%valid" })).rejects.toThrowError("Invalid name");
197
228
  expect(execMocks.exec).not.toHaveBeenCalled();
198
229
  });
199
230
 
200
- it('should throw error if volume removal fails', async () => {
201
- const output = { ok: false, stdout: '' };
231
+ it("should throw error if volume removal fails", async () => {
232
+ const output = { ok: false, stdout: "" };
202
233
  execMocks.exec.mockResolvedValue(output);
203
234
 
204
- await expect(vm.remove({ name: 'failed' })).rejects.toThrowError('Unable to remove volume');
235
+ await expect(vm.remove({ name: "failed" })).rejects.toThrowError("Unable to remove volume");
205
236
  });
206
237
  });
207
238
 
208
- describe('prune', () => {
209
- it('should remove ununsed volumes', async () => {
210
- const execOutput = { ok: true, stdout: '' };
239
+ describe("prune", () => {
240
+ it("should remove ununsed volumes", async () => {
241
+ const execOutput = { ok: true, stdout: "" };
211
242
  execMocks.exec.mockResolvedValue(execOutput);
212
243
 
213
- await expect(vm.prune()).resolves.toEqual('');
214
-
215
- expect(execMocks.exec).toHaveBeenCalledWith('docker', ['volume', 'prune']);
244
+ await expect(vm.prune()).resolves.toEqual("");
216
245
  });
217
246
  });
218
247
  });
package/src/index.ts CHANGED
@@ -1,91 +1,104 @@
1
- import { join } from 'node:path';
2
- import { readFile } from 'node:fs/promises';
3
- import { exec } from '@cloud-cli/exec';
1
+ import { join } from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { exec } from "@cloud-cli/exec";
4
+ import { help } from "@cloud-cli/cli";
4
5
 
5
6
  const nameRe = /^[a-z][a-z0-9-]+$/;
6
7
  const isValidName = (name) => nameRe.test(name);
7
- const invalidNameError = new Error('Invalid name');
8
+ const invalidNameError = new Error("Invalid name");
9
+ const readOptions = (options: NameOption) => {
10
+ options.name ||= options._?.[0];
11
+ if (!isValidName(options.name)) {
12
+ throw invalidNameError;
13
+ }
14
+ };
15
+
16
+ interface Options {
17
+ _?: string[];
18
+ }
8
19
 
9
- interface NameOption {
20
+ interface NameOption extends Options {
10
21
  name: string;
11
22
  }
12
23
 
13
- interface NameAndPathOption {
24
+ interface NameAndPathOption extends Options {
14
25
  name: string;
15
26
  path?: string;
16
27
  }
17
28
 
18
29
  async function list() {
19
- const output = await exec('docker', ['volume', 'ls', '--format={{.Name}}']);
30
+ const output = await exec("docker", ["volume", "ls", "--format={{.Name}}"]);
20
31
 
21
32
  if (!output.ok) {
22
- throw new Error('Unable to list volumes');
33
+ throw new Error("Unable to list volumes");
23
34
  }
24
35
 
25
- return output.stdout.trim().split('\n');
36
+ return output.stdout.trim().split("\n");
26
37
  }
27
38
 
28
39
  async function getVolumeMountpoint(name): Promise<string> {
29
- const output = await exec('docker', ['volume', 'inspect', name]);
40
+ const output = await exec("docker", ["volume", "inspect", name]);
30
41
  const volumes = JSON.parse(output.stdout);
31
42
 
32
43
  if (!volumes.length) {
33
- throw new Error('Volume not found');
44
+ throw new Error("Volume not found");
34
45
  }
35
46
 
36
47
  return volumes[0].Mountpoint;
37
48
  }
38
49
 
39
50
  async function ls(options: NameAndPathOption) {
40
- const { name, path = '' } = options;
51
+ readOptions(options);
52
+ const { name, path = "" } = options;
41
53
  const root = await getVolumeMountpoint(name);
42
- const files = await exec('ls', ['-1', join(root, path)]);
54
+ const files = await exec("ls", ["-1", join(root, path)]);
43
55
 
44
- return files.stdout.trim().split('\n').filter(Boolean);
56
+ return files.stdout.trim().split("\n").filter(Boolean);
45
57
  }
46
58
 
47
59
  async function fixPermissions(options: NameOption) {
60
+ readOptions(options);
48
61
  const { name } = options;
49
62
  const root = await getVolumeMountpoint(name);
50
- const result = await exec('chmod', ['-R', 'a+w', root]);
63
+ const result = await exec("chmod", ["-R", "a+w", root]);
51
64
 
52
65
  return result.ok;
53
66
  }
54
67
 
55
68
  async function rm(options: NameAndPathOption) {
69
+ readOptions(options);
56
70
  const { name, path } = options;
57
71
 
58
72
  if (!path) {
59
- throw new Error('Path not specified');
73
+ throw new Error("Path not specified");
60
74
  }
61
75
 
62
76
  const root = await getVolumeMountpoint(name);
63
- const result = await exec('rm', ['-r', join(root, path)]);
77
+ const result = await exec("rm", ["-r", join(root, path)]);
64
78
 
65
79
  return result.ok;
66
80
  }
67
81
 
68
82
  async function cat(options: NameAndPathOption) {
83
+ readOptions(options);
69
84
  const { name, path } = options;
70
85
 
71
86
  if (!path) {
72
- throw new Error('Path not specified');
87
+ throw new Error("Path not specified");
73
88
  }
74
89
 
75
90
  const root = await getVolumeMountpoint(name);
76
- return await readFile(join(root, path), 'utf8');
91
+ return await readFile(join(root, path), "utf8");
77
92
  }
78
93
 
79
94
  async function show(options: NameOption) {
80
- if (!isValidName(options.name)) {
81
- throw invalidNameError;
82
- }
95
+ readOptions(options);
83
96
 
84
- const output = await exec('docker', ['volume', 'inspect', options.name]);
97
+ const output = await exec("docker", ["volume", "inspect", options.name]);
85
98
  const list = JSON.parse(output.stdout);
86
99
 
87
100
  if (!list.length) {
88
- throw new Error('Volume not found');
101
+ throw new Error("Volume not found");
89
102
  }
90
103
 
91
104
  const { Name, Labels, CreatedAt } = list[0];
@@ -98,36 +111,57 @@ async function show(options: NameOption) {
98
111
  }
99
112
 
100
113
  async function add(options: NameOption) {
101
- if (!isValidName(options.name)) {
102
- throw invalidNameError;
103
- }
104
-
105
- const output = await exec('docker', ['volume', 'create', options.name]);
114
+ readOptions(options);
115
+ const output = await exec("docker", ["volume", "create", options.name]);
106
116
 
107
117
  if (output.ok) {
118
+ await fixPermissions(options);
108
119
  return true;
109
120
  }
110
121
 
111
- throw new Error('Unable to create volume');
122
+ throw new Error("Unable to create volume");
112
123
  }
113
124
 
114
125
  async function remove(options: NameOption) {
115
- if (!isValidName(options.name)) {
116
- throw invalidNameError;
117
- }
118
-
119
- const output = await exec('docker', ['volume', 'rm', options.name]);
126
+ readOptions(options);
127
+ const output = await exec("docker", ["volume", "rm", options.name]);
120
128
 
121
129
  if (output.ok) {
122
130
  return true;
123
131
  }
124
132
 
125
- throw new Error('Unable to remove volume');
133
+ throw new Error("Unable to remove volume");
126
134
  }
127
135
 
128
136
  async function prune() {
129
- await exec('docker', ['volume', 'prune']);
130
- return '';
137
+ await exec("docker", ["volume", "prune"]);
138
+ return "";
131
139
  }
132
140
 
133
- export default { add, remove, list, show, prune, ls, rm, cat, fixPermissions };
141
+ export default {
142
+ add,
143
+ remove,
144
+ list,
145
+ show,
146
+ prune,
147
+ ls,
148
+ rm,
149
+ cat,
150
+ fixPermissions,
151
+ [help]: () => `Manage Docker volumes
152
+
153
+ Available commands:
154
+ vm.add --name <name> - Create a new Docker volume (name must be lowercase alphanumeric with hyphens)
155
+ vm.remove --name <name> - Remove a Docker volume
156
+ vm.list - List all Docker volumes
157
+ vm.ls --name <name> [--path <path>] - List files in a volume
158
+ vm.cat --name <name> --path <path> - Read a file from a volume
159
+ vm.show --name <name> - Show volume details
160
+ vm.fixPermissions --name <name> - Fix permissions on a volume
161
+ vm.rm --name <name> --path <path> - Remove a file or directory from a volume
162
+ vm.prune - Remove all unused Docker volumes
163
+
164
+ Options:
165
+ name - Volume name (lowercase alphanumeric with hyphens)
166
+ path - Path within the volume`,
167
+ };