@fredlackey/cli-proxmox 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `proxmox configure`
3
+ *
4
+ * Writes Proxmox credentials to ~/.config/cli-proxmox/config.json. Supports
5
+ * two modes:
6
+ *
7
+ * - Non-interactive: pass --base-url, --token-id, --token-secret (and
8
+ * optionally --default-node / --verify-ssl) as flags. Any value not
9
+ * provided falls back to the existing saved config, so partial rotations
10
+ * work (e.g. only --token-secret to rotate).
11
+ *
12
+ * - Interactive: if stdin/stdout are a TTY, any value still missing
13
+ * after flag + existing-config fallback is prompted for.
14
+ *
15
+ * In neither mode do we read environment variables.
16
+ */
17
+
18
+ import { Command } from 'commander';
19
+ import { getRuntime } from '../utils/runtime.js';
20
+ import { createOutput } from '../utils/output.js';
21
+ import { loadConfig, saveConfig, getConfigPath } from '../utils/config.js';
22
+ import { ask, confirm, closeRl } from '../utils/readline.js';
23
+
24
+ export function configureCommand() {
25
+ const cmd = new Command('configure');
26
+ cmd
27
+ .description('Write Proxmox credentials to ~/.config/cli-proxmox/config.json')
28
+ .option('--base-url <url>', 'Proxmox host root URL (e.g. https://pve.example.com:8006)')
29
+ .option('--token-id <id>', 'Proxmox API token ID (e.g. root@pam!terraform-token)')
30
+ .option('--token-secret <secret>', 'Proxmox API token secret (UUID)')
31
+ .option('--default-node <name>', 'Default PVE node name (optional)')
32
+ .option('--verify-ssl', 'Verify TLS cert (default: false for self-signed)')
33
+ .option('--no-verify-ssl', 'Do not verify TLS cert')
34
+ .action(async (opts) => {
35
+ const runtime = getRuntime();
36
+ const out = createOutput(runtime);
37
+
38
+ const existing = loadConfig() || {};
39
+
40
+ let baseUrl = opts.baseUrl ?? existing.baseUrl;
41
+ let tokenId = opts.tokenId ?? existing.tokenId;
42
+ let tokenSecret = opts.tokenSecret ?? existing.tokenSecret;
43
+ let defaultNode = opts.defaultNode ?? existing.defaultNode;
44
+ // commander sets opts.verifySsl to true/false if --verify-ssl or
45
+ // --no-verify-ssl was passed, and undefined if neither.
46
+ let verifySsl = opts.verifySsl;
47
+ if (verifySsl === undefined) verifySsl = existing.verifySsl;
48
+
49
+ // In interactive mode, prompt for anything still missing.
50
+ if (runtime.interactive) {
51
+ if (!baseUrl) {
52
+ baseUrl = await ask('Proxmox base URL', 'https://pve.example.com:8006');
53
+ }
54
+ if (!tokenId) {
55
+ tokenId = await ask('Token ID (e.g. root@pam!mytoken)');
56
+ }
57
+ if (!tokenSecret) {
58
+ process.stderr.write(
59
+ '(heads up: token secret input will be visible in the terminal — if that is a concern, Ctrl-C and pass --token-secret)\n'
60
+ );
61
+ tokenSecret = await ask('Token secret (UUID)');
62
+ }
63
+ if (defaultNode === undefined || defaultNode === null || defaultNode === '') {
64
+ defaultNode = await ask('Default node name (optional, blank to skip)', '');
65
+ }
66
+ if (verifySsl === undefined) {
67
+ verifySsl = await confirm('Verify TLS certificate?', false);
68
+ }
69
+ closeRl();
70
+ }
71
+
72
+ const missing = [];
73
+ if (!baseUrl) missing.push('--base-url');
74
+ if (!tokenId) missing.push('--token-id');
75
+ if (!tokenSecret) missing.push('--token-secret');
76
+
77
+ if (missing.length) {
78
+ const err = new Error(`Missing required values: ${missing.join(', ')}`);
79
+ err.code = 'missing_required_value';
80
+ err.detail = { missing };
81
+ throw err;
82
+ }
83
+
84
+ const config = {
85
+ baseUrl: baseUrl.replace(/\/+$/, ''),
86
+ tokenId,
87
+ tokenSecret,
88
+ verifySsl: Boolean(verifySsl),
89
+ };
90
+ if (defaultNode) config.defaultNode = defaultNode;
91
+ saveConfig(config);
92
+
93
+ out.heading('Configured');
94
+ out.success('Config written', { path: getConfigPath() });
95
+ out.set('path', getConfigPath());
96
+ out.set('baseUrl', config.baseUrl);
97
+ out.set('tokenId', config.tokenId);
98
+ if (config.defaultNode) out.set('defaultNode', config.defaultNode);
99
+ out.set('verifySsl', config.verifySsl);
100
+ // Deliberately omit tokenSecret from the result payload.
101
+ out.flush();
102
+ });
103
+
104
+ return cmd;
105
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `proxmox node` subcommands: list, status.
3
+ *
4
+ * Each subcommand accepts the standard credential flags and falls back to
5
+ * ~/.config/cli-proxmox/config.json for any that are omitted.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { getRuntime } from '../utils/runtime.js';
10
+ import { createOutput } from '../utils/output.js';
11
+ import { resolveCredentials, resolveNode } from '../utils/config.js';
12
+ import {
13
+ createProxmoxClient,
14
+ withCredentialOptions,
15
+ withNodeOption,
16
+ } from '../utils/proxmox-client.js';
17
+
18
+ export function nodeCommand() {
19
+ const cmd = new Command('node');
20
+ cmd.description('Node operations');
21
+
22
+ // ── list ──────────────────────────────────────────────────────────
23
+ withCredentialOptions(
24
+ cmd.command('list').description('List all nodes in the Proxmox cluster')
25
+ ).action(async (opts) => {
26
+ const runtime = getRuntime();
27
+ const out = createOutput(runtime);
28
+ const client = createProxmoxClient(resolveCredentials(opts));
29
+
30
+ const data = await client.get('nodes');
31
+ const list = Array.isArray(data) ? data : [];
32
+
33
+ out.heading('Nodes');
34
+ if (runtime.interactive) {
35
+ if (!list.length) {
36
+ out.dim('(no nodes)');
37
+ } else {
38
+ for (const n of list) {
39
+ const name = (n.node || '').padEnd(16);
40
+ const status = (n.status || '').padEnd(8);
41
+ out.info(`${name} ${status} cpu=${n.cpu ?? '-'} mem=${n.mem ?? '-'}/${n.maxmem ?? '-'}`);
42
+ }
43
+ out.dim(`${list.length} total`);
44
+ }
45
+ }
46
+ out.set('nodes', list);
47
+ out.set('total', list.length);
48
+ out.flush();
49
+ });
50
+
51
+ // ── status ───────────────────────────────────────────────────────
52
+ withNodeOption(
53
+ withCredentialOptions(
54
+ cmd.command('status').description('Get detailed status of a specific node')
55
+ )
56
+ ).action(async (opts) => {
57
+ const runtime = getRuntime();
58
+ const out = createOutput(runtime);
59
+ const creds = resolveCredentials(opts);
60
+ const node = resolveNode(opts, creds);
61
+ const client = createProxmoxClient(creds);
62
+
63
+ const data = await client.get(`nodes/${node}/status`);
64
+
65
+ out.heading('Node status');
66
+ if (runtime.interactive) {
67
+ out.info(`Node: ${node}`);
68
+ if (data && typeof data === 'object') {
69
+ for (const [k, v] of Object.entries(data)) {
70
+ const display = typeof v === 'object' ? JSON.stringify(v) : v;
71
+ out.info(`${k.padEnd(16)} ${display}`);
72
+ }
73
+ }
74
+ }
75
+ out.set('node', node);
76
+ out.set('status', data);
77
+ out.flush();
78
+ });
79
+
80
+ return cmd;
81
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * `proxmox snapshot` subcommands: list, create, remove, rollback.
3
+ *
4
+ * Each subcommand accepts the standard credential flags, --node (with
5
+ * fallback to configured defaultNode), and --vmid.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { getRuntime } from '../utils/runtime.js';
10
+ import { createOutput } from '../utils/output.js';
11
+ import { resolveCredentials, resolveNode } from '../utils/config.js';
12
+ import {
13
+ createProxmoxClient,
14
+ withCredentialOptions,
15
+ withNodeOption,
16
+ } from '../utils/proxmox-client.js';
17
+
18
+ export function snapshotCommand() {
19
+ const cmd = new Command('snapshot');
20
+ cmd.description('VM snapshot operations');
21
+
22
+ // ── list ──────────────────────────────────────────────────────────
23
+ withNodeOption(
24
+ withCredentialOptions(
25
+ cmd.command('list')
26
+ .description('List snapshots for a VM')
27
+ .requiredOption('--vmid <id>', 'VM ID')
28
+ )
29
+ ).action(async (opts) => {
30
+ const runtime = getRuntime();
31
+ const out = createOutput(runtime);
32
+ const creds = resolveCredentials(opts);
33
+ const node = resolveNode(opts, creds);
34
+ const client = createProxmoxClient(creds);
35
+
36
+ const data = await client.get(`nodes/${node}/qemu/${opts.vmid}/snapshot`);
37
+ const list = Array.isArray(data) ? data : [];
38
+
39
+ out.heading('Snapshots');
40
+ if (runtime.interactive) {
41
+ if (!list.length) {
42
+ out.dim('(no snapshots)');
43
+ } else {
44
+ for (const s of list) {
45
+ const name = (s.name || '').padEnd(20);
46
+ const parent = s.parent ? `parent=${s.parent}` : '';
47
+ out.info(`${name} ${parent}`);
48
+ }
49
+ out.dim(`${list.length} total for VM ${opts.vmid}`);
50
+ }
51
+ }
52
+ out.set('node', node);
53
+ out.set('vmid', opts.vmid);
54
+ out.set('snapshots', list);
55
+ out.set('total', list.length);
56
+ out.flush();
57
+ });
58
+
59
+ // ── create ────────────────────────────────────────────────────────
60
+ withNodeOption(
61
+ withCredentialOptions(
62
+ cmd.command('create')
63
+ .description('Create a snapshot of a VM')
64
+ .requiredOption('--vmid <id>', 'VM ID')
65
+ .requiredOption('--snapname <name>', 'Snapshot name')
66
+ .option('--description <text>', 'Snapshot description')
67
+ .option('--vmstate', 'Include RAM state in the snapshot')
68
+ )
69
+ ).action(async (opts) => {
70
+ const runtime = getRuntime();
71
+ const out = createOutput(runtime);
72
+ const creds = resolveCredentials(opts);
73
+ const node = resolveNode(opts, creds);
74
+ const client = createProxmoxClient(creds);
75
+
76
+ const body = { snapname: opts.snapname };
77
+ if (opts.description) body.description = opts.description;
78
+ if (opts.vmstate) body.vmstate = 1;
79
+
80
+ const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/snapshot`, body);
81
+
82
+ out.heading('Snapshot create');
83
+ if (runtime.interactive) {
84
+ out.success(`Snapshot '${opts.snapname}' requested for VM ${opts.vmid} on ${node}`);
85
+ if (upid) out.info(`UPID: ${upid}`);
86
+ }
87
+ out.set('node', node);
88
+ out.set('vmid', opts.vmid);
89
+ out.set('snapname', opts.snapname);
90
+ out.set('upid', upid);
91
+ out.flush();
92
+ });
93
+
94
+ // ── remove ────────────────────────────────────────────────────────
95
+ withNodeOption(
96
+ withCredentialOptions(
97
+ cmd.command('remove')
98
+ .description('Delete a snapshot from a VM')
99
+ .requiredOption('--vmid <id>', 'VM ID')
100
+ .requiredOption('--snapname <name>', 'Snapshot name to delete')
101
+ )
102
+ ).action(async (opts) => {
103
+ const runtime = getRuntime();
104
+ const out = createOutput(runtime);
105
+ const creds = resolveCredentials(opts);
106
+ const node = resolveNode(opts, creds);
107
+ const client = createProxmoxClient(creds);
108
+
109
+ const upid = await client.delete(
110
+ `nodes/${node}/qemu/${opts.vmid}/snapshot/${encodeURIComponent(opts.snapname)}`
111
+ );
112
+
113
+ out.heading('Snapshot remove');
114
+ if (runtime.interactive) {
115
+ out.success(`Delete requested for snapshot '${opts.snapname}' on VM ${opts.vmid}`);
116
+ if (upid) out.info(`UPID: ${upid}`);
117
+ }
118
+ out.set('node', node);
119
+ out.set('vmid', opts.vmid);
120
+ out.set('snapname', opts.snapname);
121
+ out.set('upid', upid);
122
+ out.flush();
123
+ });
124
+
125
+ // ── rollback ──────────────────────────────────────────────────────
126
+ withNodeOption(
127
+ withCredentialOptions(
128
+ cmd.command('rollback')
129
+ .description('Rollback a VM to a previous snapshot')
130
+ .requiredOption('--vmid <id>', 'VM ID')
131
+ .requiredOption('--snapname <name>', 'Snapshot name to rollback to')
132
+ )
133
+ ).action(async (opts) => {
134
+ const runtime = getRuntime();
135
+ const out = createOutput(runtime);
136
+ const creds = resolveCredentials(opts);
137
+ const node = resolveNode(opts, creds);
138
+ const client = createProxmoxClient(creds);
139
+
140
+ const upid = await client.post(
141
+ `nodes/${node}/qemu/${opts.vmid}/snapshot/${encodeURIComponent(opts.snapname)}/rollback`
142
+ );
143
+
144
+ out.heading('Snapshot rollback');
145
+ if (runtime.interactive) {
146
+ out.success(`Rollback to '${opts.snapname}' requested for VM ${opts.vmid} on ${node}`);
147
+ if (upid) out.info(`UPID: ${upid}`);
148
+ }
149
+ out.set('node', node);
150
+ out.set('vmid', opts.vmid);
151
+ out.set('snapname', opts.snapname);
152
+ out.set('upid', upid);
153
+ out.flush();
154
+ });
155
+
156
+ return cmd;
157
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `proxmox storage` subcommands: list, content.
3
+ *
4
+ * Lists storage pools on a node with usage stats, and browse pool contents.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { getRuntime } from '../utils/runtime.js';
9
+ import { createOutput } from '../utils/output.js';
10
+ import { resolveCredentials, resolveNode } from '../utils/config.js';
11
+ import {
12
+ createProxmoxClient,
13
+ withCredentialOptions,
14
+ withNodeOption,
15
+ } from '../utils/proxmox-client.js';
16
+
17
+ export function storageCommand() {
18
+ const cmd = new Command('storage');
19
+ cmd.description('Storage pool operations');
20
+
21
+ withNodeOption(
22
+ withCredentialOptions(
23
+ cmd.command('list').description('List storage pools on a node')
24
+ )
25
+ ).action(async (opts) => {
26
+ const runtime = getRuntime();
27
+ const out = createOutput(runtime);
28
+ const creds = resolveCredentials(opts);
29
+ const node = resolveNode(opts, creds);
30
+ const client = createProxmoxClient(creds);
31
+
32
+ const data = await client.get(`nodes/${node}/storage`);
33
+ const list = Array.isArray(data) ? data : [];
34
+
35
+ out.heading('Storage');
36
+ if (runtime.interactive) {
37
+ if (!list.length) {
38
+ out.dim('(no storage)');
39
+ } else {
40
+ for (const s of list) {
41
+ const name = (s.storage || '').padEnd(16);
42
+ const type = (s.type || '').padEnd(8);
43
+ out.info(`${name} ${type} used=${s.used ?? '-'}/${s.total ?? '-'}`);
44
+ }
45
+ out.dim(`${list.length} total on ${node}`);
46
+ }
47
+ }
48
+ out.set('node', node);
49
+ out.set('storage', list);
50
+ out.set('total', list.length);
51
+ out.flush();
52
+ });
53
+
54
+ // ── content ───────────────────────────────────────────────────────
55
+ withNodeOption(
56
+ withCredentialOptions(
57
+ cmd.command('content')
58
+ .description('List contents of a storage pool (ISOs, disk images, backups, templates)')
59
+ .requiredOption('--storage <name>', 'Storage pool name')
60
+ .option('--content <type>', 'Content type filter (iso, images, backup, vztmpl)')
61
+ )
62
+ ).action(async (opts) => {
63
+ const runtime = getRuntime();
64
+ const out = createOutput(runtime);
65
+ const creds = resolveCredentials(opts);
66
+ const node = resolveNode(opts, creds);
67
+ const client = createProxmoxClient(creds);
68
+
69
+ const params = opts.content ? { content: opts.content } : undefined;
70
+ const data = await client.get(`nodes/${node}/storage/${opts.storage}/content`, params);
71
+ const list = Array.isArray(data) ? data : [];
72
+
73
+ out.heading('Storage content');
74
+ if (runtime.interactive) {
75
+ if (!list.length) {
76
+ out.dim('(no content)');
77
+ } else {
78
+ for (const item of list) {
79
+ const volid = (item.volid || '').padEnd(40);
80
+ const format = (item.format || '').padEnd(8);
81
+ const size = item.size ?? '-';
82
+ out.info(`${volid} ${format} size=${size}`);
83
+ }
84
+ out.dim(`${list.length} item(s) in ${opts.storage} on ${node}`);
85
+ }
86
+ }
87
+ out.set('node', node);
88
+ out.set('storage', opts.storage);
89
+ out.set('content', list);
90
+ out.set('total', list.length);
91
+ out.flush();
92
+ });
93
+
94
+ return cmd;
95
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `proxmox task` subcommands: list, get, wait.
3
+ *
4
+ * Each subcommand accepts the standard credential flags and --node, which
5
+ * falls back to the configured defaultNode.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { getRuntime } from '../utils/runtime.js';
10
+ import { createOutput } from '../utils/output.js';
11
+ import { resolveCredentials, resolveNode } from '../utils/config.js';
12
+ import {
13
+ createProxmoxClient,
14
+ withCredentialOptions,
15
+ withNodeOption,
16
+ } from '../utils/proxmox-client.js';
17
+
18
+ export function taskCommand() {
19
+ const cmd = new Command('task');
20
+ cmd.description('Task operations');
21
+
22
+ // ── list ──────────────────────────────────────────────────────────
23
+ withNodeOption(
24
+ withCredentialOptions(
25
+ cmd.command('list')
26
+ .description('List recent tasks on a node')
27
+ .option('--limit <n>', 'Max tasks to return (default: 50)')
28
+ .option('--source <type>', 'Filter by source (active, archive, all)')
29
+ )
30
+ ).action(async (opts) => {
31
+ const runtime = getRuntime();
32
+ const out = createOutput(runtime);
33
+ const creds = resolveCredentials(opts);
34
+ const node = resolveNode(opts, creds);
35
+ const client = createProxmoxClient(creds);
36
+
37
+ const params = {
38
+ limit: opts.limit || '50',
39
+ source: opts.source || 'active',
40
+ };
41
+ const data = await client.get(`nodes/${node}/tasks`, params);
42
+ const list = Array.isArray(data) ? data : [];
43
+
44
+ out.heading('Tasks');
45
+ if (runtime.interactive) {
46
+ if (!list.length) {
47
+ out.dim('(no tasks)');
48
+ } else {
49
+ for (const t of list) {
50
+ const type = (t.type || '').padEnd(12);
51
+ const status = (t.status || '').padEnd(10);
52
+ const id = t.id || '-';
53
+ out.info(`${type} ${status} id=${id} user=${t.user || '-'}`);
54
+ }
55
+ out.dim(`${list.length} task(s) on ${node}`);
56
+ }
57
+ }
58
+ out.set('node', node);
59
+ out.set('tasks', list);
60
+ out.set('total', list.length);
61
+ out.flush();
62
+ });
63
+
64
+ // ── get ───────────────────────────────────────────────────────────
65
+ withNodeOption(
66
+ withCredentialOptions(
67
+ cmd.command('get')
68
+ .description('Get status and log of a specific task by UPID')
69
+ .requiredOption('--upid <upid>', 'Task UPID')
70
+ )
71
+ ).action(async (opts) => {
72
+ const runtime = getRuntime();
73
+ const out = createOutput(runtime);
74
+ const creds = resolveCredentials(opts);
75
+ const node = resolveNode(opts, creds);
76
+ const client = createProxmoxClient(creds);
77
+
78
+ const upid = encodeURIComponent(opts.upid);
79
+ const [status, log] = await Promise.all([
80
+ client.get(`nodes/${node}/tasks/${upid}/status`),
81
+ client.get(`nodes/${node}/tasks/${upid}/log`),
82
+ ]);
83
+
84
+ const logLines = Array.isArray(log)
85
+ ? log.map((l) => l.t).filter(Boolean)
86
+ : [];
87
+
88
+ out.heading('Task detail');
89
+ if (runtime.interactive) {
90
+ out.info(`Node: ${node}`);
91
+ if (status && typeof status === 'object') {
92
+ for (const [k, v] of Object.entries(status)) {
93
+ out.info(`${k.padEnd(14)} ${v}`);
94
+ }
95
+ }
96
+ if (logLines.length) {
97
+ out.info('');
98
+ out.info('Log:');
99
+ for (const line of logLines) {
100
+ out.info(` ${line}`);
101
+ }
102
+ }
103
+ }
104
+ out.set('node', node);
105
+ out.set('upid', opts.upid);
106
+ out.set('status', status);
107
+ out.set('log', logLines);
108
+ out.flush();
109
+ });
110
+
111
+ // ── wait ──────────────────────────────────────────────────────────
112
+ withNodeOption(
113
+ withCredentialOptions(
114
+ cmd.command('wait')
115
+ .description('Wait for a task to complete by polling its UPID')
116
+ .requiredOption('--upid <upid>', 'Task UPID to wait for')
117
+ .option('--timeout <seconds>', 'Timeout in seconds (default: 300)')
118
+ )
119
+ ).action(async (opts) => {
120
+ const runtime = getRuntime();
121
+ const out = createOutput(runtime);
122
+ const creds = resolveCredentials(opts);
123
+ const node = resolveNode(opts, creds);
124
+ const client = createProxmoxClient(creds);
125
+
126
+ const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) * 1000 : 300000;
127
+ const upid = encodeURIComponent(opts.upid);
128
+ const start = Date.now();
129
+
130
+ let status;
131
+ while (Date.now() - start < timeoutMs) {
132
+ status = await client.get(`nodes/${node}/tasks/${upid}/status`);
133
+ if (status && status.status === 'stopped') break;
134
+ await new Promise((r) => setTimeout(r, 2000));
135
+ }
136
+
137
+ const finished = status && status.status === 'stopped';
138
+
139
+ out.heading('Task wait');
140
+ if (runtime.interactive) {
141
+ if (finished) {
142
+ out.success(`Task completed: ${status.exitstatus || 'OK'}`);
143
+ } else {
144
+ out.info('Task did not complete within timeout');
145
+ }
146
+ }
147
+ out.set('node', node);
148
+ out.set('upid', opts.upid);
149
+ out.set('status', status);
150
+ out.set('finished', finished);
151
+ out.flush();
152
+ });
153
+
154
+ return cmd;
155
+ }