@zincapp/znvault-plugin-payara 1.4.3 → 1.5.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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +309 -223
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/routes.d.ts +3 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +102 -33
- package/dist/routes.js.map +1 -1
- package/dist/types.d.ts +24 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/war-deployer.d.ts +31 -8
- package/dist/war-deployer.d.ts.map +1 -1
- package/dist/war-deployer.js +73 -14
- package/dist/war-deployer.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Path: src/cli.ts
|
|
2
|
-
// CLI commands for Payara plugin
|
|
2
|
+
// CLI commands for Payara plugin with visual progress
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { stat, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
|
-
import { join, resolve } from 'node:path';
|
|
7
|
-
import { homedir
|
|
6
|
+
import { join, resolve, basename } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
8
|
import AdmZip from 'adm-zip';
|
|
9
9
|
/**
|
|
10
10
|
* Chunk size for batched deployments (number of files per chunk)
|
|
@@ -14,6 +14,179 @@ const CHUNK_SIZE = 50;
|
|
|
14
14
|
// Config file path
|
|
15
15
|
const CONFIG_DIR = join(homedir(), '.znvault');
|
|
16
16
|
const CONFIG_FILE = join(CONFIG_DIR, 'deploy-configs.json');
|
|
17
|
+
// ANSI escape codes for colors and cursor control
|
|
18
|
+
const ANSI = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bold: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
gray: '\x1b[90m',
|
|
28
|
+
clearLine: '\x1b[2K',
|
|
29
|
+
cursorUp: '\x1b[1A',
|
|
30
|
+
cursorHide: '\x1b[?25l',
|
|
31
|
+
cursorShow: '\x1b[?25h',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Format file size to human readable
|
|
35
|
+
*/
|
|
36
|
+
function formatSize(bytes) {
|
|
37
|
+
if (bytes < 1024)
|
|
38
|
+
return `${bytes}B`;
|
|
39
|
+
if (bytes < 1024 * 1024)
|
|
40
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
41
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format duration in ms to human readable
|
|
45
|
+
*/
|
|
46
|
+
function formatDuration(ms) {
|
|
47
|
+
if (ms < 1000)
|
|
48
|
+
return `${ms}ms`;
|
|
49
|
+
if (ms < 60000)
|
|
50
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
51
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a progress bar string
|
|
55
|
+
*/
|
|
56
|
+
function progressBar(current, total, width = 30) {
|
|
57
|
+
const percent = Math.round((current / total) * 100);
|
|
58
|
+
const filled = Math.round((current / total) * width);
|
|
59
|
+
const empty = width - filled;
|
|
60
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
61
|
+
return `${ANSI.cyan}${bar}${ANSI.reset} ${percent}%`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Progress reporter for visual feedback
|
|
65
|
+
*/
|
|
66
|
+
class ProgressReporter {
|
|
67
|
+
isPlain;
|
|
68
|
+
currentHost = '';
|
|
69
|
+
lastFiles = [];
|
|
70
|
+
maxFileDisplay = 5;
|
|
71
|
+
constructor(isPlain) {
|
|
72
|
+
this.isPlain = isPlain;
|
|
73
|
+
}
|
|
74
|
+
setHost(host) {
|
|
75
|
+
this.currentHost = host;
|
|
76
|
+
if (!this.isPlain) {
|
|
77
|
+
console.log(`\n${ANSI.bold}${ANSI.blue}▶ Deploying to ${host}${ANSI.reset}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
analyzing(warPath) {
|
|
81
|
+
const name = basename(warPath);
|
|
82
|
+
if (this.isPlain) {
|
|
83
|
+
console.log(`Analyzing ${name}...`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(`${ANSI.dim} Analyzing ${name}...${ANSI.reset}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
foundFiles(count, warSize) {
|
|
90
|
+
if (this.isPlain) {
|
|
91
|
+
console.log(`Found ${count} files (${formatSize(warSize)})`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`${ANSI.dim} Found ${ANSI.bold}${count}${ANSI.reset}${ANSI.dim} files (${formatSize(warSize)})${ANSI.reset}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
diff(changed, deleted) {
|
|
98
|
+
if (this.isPlain) {
|
|
99
|
+
console.log(`Diff: ${changed} changed, ${deleted} deleted`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const changeStr = changed > 0 ? `${ANSI.green}+${changed}${ANSI.reset}` : `${ANSI.dim}+0${ANSI.reset}`;
|
|
103
|
+
const deleteStr = deleted > 0 ? `${ANSI.red}-${deleted}${ANSI.reset}` : `${ANSI.dim}-0${ANSI.reset}`;
|
|
104
|
+
console.log(` ${ANSI.dim}Diff:${ANSI.reset} ${changeStr} ${deleteStr}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
uploadingFullWar() {
|
|
108
|
+
if (this.isPlain) {
|
|
109
|
+
console.log('Uploading full WAR file...');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(` ${ANSI.yellow}⬆ Uploading full WAR file...${ANSI.reset}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
uploadProgress(sent, total, currentFiles) {
|
|
116
|
+
if (this.isPlain) {
|
|
117
|
+
console.log(` Sent ${sent}/${total} files`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Store last files for display
|
|
121
|
+
if (currentFiles) {
|
|
122
|
+
this.lastFiles = currentFiles.slice(-this.maxFileDisplay);
|
|
123
|
+
}
|
|
124
|
+
// Clear previous lines and redraw
|
|
125
|
+
const lines = this.maxFileDisplay + 2; // progress bar + files
|
|
126
|
+
process.stdout.write(`${ANSI.cursorUp.repeat(lines)}${ANSI.clearLine}`);
|
|
127
|
+
// Progress bar
|
|
128
|
+
console.log(` ${progressBar(sent, total)} ${sent}/${total} files`);
|
|
129
|
+
// File list
|
|
130
|
+
console.log(`${ANSI.dim} Recent files:${ANSI.reset}`);
|
|
131
|
+
for (const file of this.lastFiles) {
|
|
132
|
+
const shortFile = file.length > 60 ? '...' + file.slice(-57) : file;
|
|
133
|
+
console.log(`${ANSI.dim} ${shortFile}${ANSI.reset}`);
|
|
134
|
+
}
|
|
135
|
+
// Pad empty lines
|
|
136
|
+
for (let i = this.lastFiles.length; i < this.maxFileDisplay; i++) {
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
deploying() {
|
|
141
|
+
if (this.isPlain) {
|
|
142
|
+
console.log('Deploying via asadmin...');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(` ${ANSI.yellow}⏳ Deploying via asadmin...${ANSI.reset}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
deployed(result) {
|
|
149
|
+
if (this.isPlain) {
|
|
150
|
+
console.log(`Deployed: ${result.filesChanged} changed, ${result.filesDeleted} deleted (${formatDuration(result.deploymentTime)})`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(` ${ANSI.green}✓ Deployed${ANSI.reset} ${result.filesChanged} changed, ${result.filesDeleted} deleted ${ANSI.dim}(${formatDuration(result.deploymentTime)})${ANSI.reset}`);
|
|
154
|
+
if (result.applications && result.applications.length > 0) {
|
|
155
|
+
console.log(` ${ANSI.dim} Applications: ${result.applications.join(', ')}${ANSI.reset}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
noChanges() {
|
|
160
|
+
if (this.isPlain) {
|
|
161
|
+
console.log('No changes to deploy');
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(` ${ANSI.green}✓ No changes${ANSI.reset}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
failed(error) {
|
|
168
|
+
if (this.isPlain) {
|
|
169
|
+
console.log(`Failed: ${error}`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(` ${ANSI.red}✗ Failed: ${error}${ANSI.reset}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
summary(successful, total, failed) {
|
|
176
|
+
console.log('');
|
|
177
|
+
if (this.isPlain) {
|
|
178
|
+
console.log(`Deployment complete: ${successful}/${total} hosts successful${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
if (failed === 0) {
|
|
182
|
+
console.log(`${ANSI.bold}${ANSI.green}✓ Deployment complete${ANSI.reset}: ${successful}/${total} hosts successful`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(`${ANSI.bold}${ANSI.yellow}⚠ Deployment complete${ANSI.reset}: ${successful}/${total} hosts successful, ${ANSI.red}${failed} failed${ANSI.reset}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
17
190
|
/**
|
|
18
191
|
* Load deployment configs
|
|
19
192
|
*/
|
|
@@ -41,12 +214,12 @@ async function saveDeployConfigs(store) {
|
|
|
41
214
|
/**
|
|
42
215
|
* Upload full WAR file to server
|
|
43
216
|
*/
|
|
44
|
-
async function uploadFullWar(ctx, pluginUrl, warPath) {
|
|
217
|
+
async function uploadFullWar(ctx, pluginUrl, warPath, progress) {
|
|
45
218
|
try {
|
|
219
|
+
progress.uploadingFullWar();
|
|
46
220
|
// Read WAR file
|
|
47
221
|
const warBuffer = await readFile(warPath);
|
|
48
222
|
// Upload using raw POST
|
|
49
|
-
// Note: ctx.client might not support raw binary upload, so we use fetch directly
|
|
50
223
|
const response = await fetch(`${pluginUrl}/deploy/upload`, {
|
|
51
224
|
method: 'POST',
|
|
52
225
|
headers: {
|
|
@@ -55,23 +228,44 @@ async function uploadFullWar(ctx, pluginUrl, warPath) {
|
|
|
55
228
|
},
|
|
56
229
|
body: warBuffer,
|
|
57
230
|
});
|
|
231
|
+
const data = await response.json();
|
|
58
232
|
if (!response.ok) {
|
|
59
|
-
|
|
60
|
-
return { success: false, error: error.message ?? 'Upload failed' };
|
|
233
|
+
return { success: false, error: data.message ?? data.error ?? 'Upload failed' };
|
|
61
234
|
}
|
|
62
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
result: {
|
|
238
|
+
success: true,
|
|
239
|
+
filesChanged: Object.keys(await calculateWarHashes(warPath)).length,
|
|
240
|
+
filesDeleted: 0,
|
|
241
|
+
message: data.message ?? 'Deployment successful',
|
|
242
|
+
deploymentTime: data.deploymentTime ?? 0,
|
|
243
|
+
appName: data.appName ?? '',
|
|
244
|
+
deployed: data.deployed,
|
|
245
|
+
applications: data.applications,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
63
248
|
}
|
|
64
249
|
catch (err) {
|
|
65
250
|
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
66
251
|
}
|
|
67
252
|
}
|
|
68
253
|
/**
|
|
69
|
-
* Deploy files using chunked upload
|
|
254
|
+
* Deploy files using chunked upload with progress
|
|
70
255
|
*/
|
|
71
|
-
async function deployChunked(ctx, pluginUrl, zip, changed, deleted,
|
|
256
|
+
async function deployChunked(ctx, pluginUrl, zip, changed, deleted, progress) {
|
|
72
257
|
try {
|
|
73
258
|
let sessionId;
|
|
74
259
|
const totalFiles = changed.length;
|
|
260
|
+
// Initialize progress display
|
|
261
|
+
if (!ctx.isPlainMode()) {
|
|
262
|
+
// Print placeholder lines for progress display
|
|
263
|
+
console.log(` ${progressBar(0, totalFiles)} 0/${totalFiles} files`);
|
|
264
|
+
console.log(`${ANSI.dim} Recent files:${ANSI.reset}`);
|
|
265
|
+
for (let i = 0; i < 5; i++) {
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
75
269
|
// Send files in chunks
|
|
76
270
|
for (let i = 0; i < changed.length; i += CHUNK_SIZE) {
|
|
77
271
|
const chunkPaths = changed.slice(i, i + CHUNK_SIZE);
|
|
@@ -104,15 +298,12 @@ async function deployChunked(ctx, pluginUrl, zip, changed, deleted, onProgress)
|
|
|
104
298
|
const response = await ctx.client.post(`${pluginUrl}/deploy/chunk`, chunkRequest);
|
|
105
299
|
sessionId = response.sessionId;
|
|
106
300
|
// Report progress
|
|
107
|
-
|
|
108
|
-
onProgress(response.filesReceived, totalFiles);
|
|
109
|
-
}
|
|
301
|
+
progress.uploadProgress(response.filesReceived, totalFiles, chunkPaths);
|
|
110
302
|
// Check if committed (final chunk)
|
|
111
303
|
if (response.committed && response.result) {
|
|
112
304
|
return {
|
|
113
|
-
success:
|
|
114
|
-
|
|
115
|
-
filesDeleted: response.result.filesDeleted,
|
|
305
|
+
success: response.result.success,
|
|
306
|
+
result: response.result,
|
|
116
307
|
};
|
|
117
308
|
}
|
|
118
309
|
}
|
|
@@ -124,9 +315,9 @@ async function deployChunked(ctx, pluginUrl, zip, changed, deleted, onProgress)
|
|
|
124
315
|
}
|
|
125
316
|
}
|
|
126
317
|
/**
|
|
127
|
-
* Deploy to a single host
|
|
318
|
+
* Deploy to a single host with progress reporting
|
|
128
319
|
*/
|
|
129
|
-
async function deployToHost(ctx, host, port, warPath, localHashes, force,
|
|
320
|
+
async function deployToHost(ctx, host, port, warPath, localHashes, force, progress) {
|
|
130
321
|
try {
|
|
131
322
|
const baseUrl = host.replace(/\/$/, '');
|
|
132
323
|
// Add protocol if missing (default to HTTP for local agent communication)
|
|
@@ -152,32 +343,29 @@ async function deployToHost(ctx, host, port, warPath, localHashes, force, onProg
|
|
|
152
343
|
}
|
|
153
344
|
// If remote has no WAR, upload the full WAR file
|
|
154
345
|
if (remoteIsEmpty) {
|
|
155
|
-
|
|
156
|
-
onProgress('Uploading full WAR file...');
|
|
157
|
-
const uploadResult = await uploadFullWar(ctx, pluginUrl, warPath);
|
|
158
|
-
if (uploadResult.success) {
|
|
159
|
-
return {
|
|
160
|
-
success: true,
|
|
161
|
-
filesChanged: Object.keys(localHashes).length,
|
|
162
|
-
filesDeleted: 0,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return uploadResult;
|
|
346
|
+
return uploadFullWar(ctx, pluginUrl, warPath, progress);
|
|
166
347
|
}
|
|
167
348
|
// Calculate diff
|
|
168
349
|
const { changed, deleted } = calculateDiff(localHashes, remoteHashes);
|
|
350
|
+
progress.diff(changed.length, deleted.length);
|
|
169
351
|
if (changed.length === 0 && deleted.length === 0) {
|
|
170
|
-
|
|
352
|
+
progress.noChanges();
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
result: {
|
|
356
|
+
success: true,
|
|
357
|
+
filesChanged: 0,
|
|
358
|
+
filesDeleted: 0,
|
|
359
|
+
message: 'No changes',
|
|
360
|
+
deploymentTime: 0,
|
|
361
|
+
appName: '',
|
|
362
|
+
},
|
|
363
|
+
};
|
|
171
364
|
}
|
|
172
365
|
const zip = new AdmZip(warPath);
|
|
173
366
|
// Use chunked deployment if there are many files
|
|
174
367
|
if (changed.length > CHUNK_SIZE) {
|
|
175
|
-
|
|
176
|
-
onProgress(`Deploying ${changed.length} files in chunks...`);
|
|
177
|
-
return deployChunked(ctx, pluginUrl, zip, changed, deleted, (sent, total) => {
|
|
178
|
-
if (onProgress)
|
|
179
|
-
onProgress(`Sent ${sent}/${total} files`);
|
|
180
|
-
});
|
|
368
|
+
return deployChunked(ctx, pluginUrl, zip, changed, deleted, progress);
|
|
181
369
|
}
|
|
182
370
|
// Small deployment - use single request
|
|
183
371
|
const files = changed.map(path => {
|
|
@@ -190,6 +378,7 @@ async function deployToHost(ctx, host, port, warPath, localHashes, force, onProg
|
|
|
190
378
|
content: entry.getData().toString('base64'),
|
|
191
379
|
};
|
|
192
380
|
});
|
|
381
|
+
progress.deploying();
|
|
193
382
|
// Deploy
|
|
194
383
|
const deployResponse = await ctx.client.post(`${pluginUrl}/deploy`, {
|
|
195
384
|
files,
|
|
@@ -198,8 +387,16 @@ async function deployToHost(ctx, host, port, warPath, localHashes, force, onProg
|
|
|
198
387
|
if (deployResponse.status === 'deployed') {
|
|
199
388
|
return {
|
|
200
389
|
success: true,
|
|
201
|
-
|
|
202
|
-
|
|
390
|
+
result: {
|
|
391
|
+
success: true,
|
|
392
|
+
filesChanged: deployResponse.filesChanged,
|
|
393
|
+
filesDeleted: deployResponse.filesDeleted,
|
|
394
|
+
message: deployResponse.message ?? 'Deployment successful',
|
|
395
|
+
deploymentTime: deployResponse.deploymentTime ?? 0,
|
|
396
|
+
appName: deployResponse.appName ?? '',
|
|
397
|
+
deployed: deployResponse.deployed,
|
|
398
|
+
applications: deployResponse.applications,
|
|
399
|
+
},
|
|
203
400
|
};
|
|
204
401
|
}
|
|
205
402
|
else {
|
|
@@ -221,8 +418,8 @@ async function deployToHost(ctx, host, port, warPath, localHashes, force, onProg
|
|
|
221
418
|
export function createPayaraCLIPlugin() {
|
|
222
419
|
return {
|
|
223
420
|
name: 'payara',
|
|
224
|
-
version: '1.
|
|
225
|
-
description: 'Payara WAR deployment commands',
|
|
421
|
+
version: '1.5.1',
|
|
422
|
+
description: 'Payara WAR deployment commands with visual progress',
|
|
226
423
|
registerCommands(program, ctx) {
|
|
227
424
|
// Create deploy command group
|
|
228
425
|
const deploy = program
|
|
@@ -239,6 +436,7 @@ export function createPayaraCLIPlugin() {
|
|
|
239
436
|
.option('--dry-run', 'Show what would be deployed without deploying')
|
|
240
437
|
.option('--sequential', 'Deploy to hosts one at a time (override parallel setting)')
|
|
241
438
|
.action(async (configName, options) => {
|
|
439
|
+
const progress = new ProgressReporter(ctx.isPlainMode());
|
|
242
440
|
try {
|
|
243
441
|
const store = await loadDeployConfigs();
|
|
244
442
|
const config = store.configs[configName];
|
|
@@ -254,23 +452,31 @@ export function createPayaraCLIPlugin() {
|
|
|
254
452
|
}
|
|
255
453
|
// Resolve WAR path
|
|
256
454
|
const warPath = resolve(config.warPath);
|
|
455
|
+
let warStats;
|
|
257
456
|
try {
|
|
258
|
-
await stat(warPath);
|
|
457
|
+
warStats = await stat(warPath);
|
|
259
458
|
}
|
|
260
459
|
catch {
|
|
261
460
|
ctx.output.error(`WAR file not found: ${warPath}`);
|
|
262
461
|
process.exit(1);
|
|
263
462
|
}
|
|
264
|
-
|
|
265
|
-
ctx.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
463
|
+
// Header
|
|
464
|
+
if (!ctx.isPlainMode()) {
|
|
465
|
+
console.log(`\n${ANSI.bold}Deploying ${ANSI.cyan}${configName}${ANSI.reset}`);
|
|
466
|
+
console.log(`${ANSI.dim} WAR: ${basename(warPath)}${ANSI.reset}`);
|
|
467
|
+
console.log(`${ANSI.dim} Hosts: ${config.hosts.length}${ANSI.reset}`);
|
|
468
|
+
console.log(`${ANSI.dim} Mode: ${options.sequential || !config.parallel ? 'sequential' : 'parallel'}${ANSI.reset}`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
ctx.output.info(`Deploying ${configName}`);
|
|
472
|
+
ctx.output.info(` WAR: ${warPath}`);
|
|
473
|
+
ctx.output.info(` Hosts: ${config.hosts.length}`);
|
|
474
|
+
ctx.output.info(` Mode: ${options.sequential || !config.parallel ? 'sequential' : 'parallel'}`);
|
|
475
|
+
}
|
|
269
476
|
// Calculate local hashes once
|
|
270
|
-
|
|
477
|
+
progress.analyzing(warPath);
|
|
271
478
|
const localHashes = await calculateWarHashes(warPath);
|
|
272
|
-
|
|
273
|
-
console.log();
|
|
479
|
+
progress.foundFiles(Object.keys(localHashes).length, warStats.size);
|
|
274
480
|
if (options.dryRun) {
|
|
275
481
|
ctx.output.info('Dry run - checking each host:');
|
|
276
482
|
for (const host of config.hosts) {
|
|
@@ -280,20 +486,19 @@ export function createPayaraCLIPlugin() {
|
|
|
280
486
|
}
|
|
281
487
|
const results = [];
|
|
282
488
|
const deployToHostWrapper = async (host) => {
|
|
283
|
-
|
|
284
|
-
const result = await deployToHost(ctx, host, config.port, warPath, localHashes, options.force ?? false,
|
|
489
|
+
progress.setHost(host);
|
|
490
|
+
const result = await deployToHost(ctx, host, config.port, warPath, localHashes, options.force ?? false, progress);
|
|
285
491
|
results.push({
|
|
286
492
|
host,
|
|
287
493
|
success: result.success,
|
|
288
494
|
error: result.error,
|
|
289
|
-
|
|
290
|
-
deleted: result.filesDeleted,
|
|
495
|
+
result: result.result,
|
|
291
496
|
});
|
|
292
|
-
if (result.success) {
|
|
293
|
-
|
|
497
|
+
if (result.success && result.result) {
|
|
498
|
+
progress.deployed(result.result);
|
|
294
499
|
}
|
|
295
500
|
else {
|
|
296
|
-
|
|
501
|
+
progress.failed(result.error ?? 'Unknown error');
|
|
297
502
|
}
|
|
298
503
|
};
|
|
299
504
|
if (options.sequential || !config.parallel) {
|
|
@@ -306,14 +511,10 @@ export function createPayaraCLIPlugin() {
|
|
|
306
511
|
// Parallel deployment
|
|
307
512
|
await Promise.all(config.hosts.map(deployToHostWrapper));
|
|
308
513
|
}
|
|
309
|
-
console.log();
|
|
310
514
|
const successful = results.filter(r => r.success).length;
|
|
311
515
|
const failed = results.filter(r => !r.success).length;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
ctx.output.warn(`Deployment complete: ${successful}/${config.hosts.length} hosts successful, ${failed} failed`);
|
|
516
|
+
progress.summary(successful, config.hosts.length, failed);
|
|
517
|
+
if (failed > 0) {
|
|
317
518
|
process.exit(1);
|
|
318
519
|
}
|
|
319
520
|
}
|
|
@@ -392,14 +593,14 @@ export function createPayaraCLIPlugin() {
|
|
|
392
593
|
console.log(JSON.stringify(configs, null, 2));
|
|
393
594
|
return;
|
|
394
595
|
}
|
|
395
|
-
console.log('
|
|
596
|
+
console.log('\nDeployment Configurations:\n');
|
|
396
597
|
for (const config of configs) {
|
|
397
|
-
console.log(` ${config.name}`);
|
|
598
|
+
console.log(` ${ANSI.bold}${config.name}${ANSI.reset}`);
|
|
398
599
|
if (config.description) {
|
|
399
|
-
console.log(` ${config.description}`);
|
|
600
|
+
console.log(` ${ANSI.dim}${config.description}${ANSI.reset}`);
|
|
400
601
|
}
|
|
401
|
-
console.log(` Hosts: ${config.hosts.length > 0 ? config.hosts.join(', ') : '(none)'}`);
|
|
402
|
-
console.log(` WAR: ${config.warPath || '(not set)'}`);
|
|
602
|
+
console.log(` Hosts: ${config.hosts.length > 0 ? config.hosts.join(', ') : ANSI.dim + '(none)' + ANSI.reset}`);
|
|
603
|
+
console.log(` WAR: ${config.warPath || ANSI.dim + '(not set)' + ANSI.reset}`);
|
|
403
604
|
console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
|
|
404
605
|
console.log();
|
|
405
606
|
}
|
|
@@ -426,16 +627,16 @@ export function createPayaraCLIPlugin() {
|
|
|
426
627
|
console.log(JSON.stringify(config, null, 2));
|
|
427
628
|
return;
|
|
428
629
|
}
|
|
429
|
-
console.log(`\
|
|
630
|
+
console.log(`\n${ANSI.bold}Deployment Config: ${config.name}${ANSI.reset}\n`);
|
|
430
631
|
if (config.description) {
|
|
431
632
|
console.log(` Description: ${config.description}`);
|
|
432
633
|
}
|
|
433
|
-
console.log(` WAR Path: ${config.warPath || '(not set)'}`);
|
|
634
|
+
console.log(` WAR Path: ${config.warPath || ANSI.dim + '(not set)' + ANSI.reset}`);
|
|
434
635
|
console.log(` Port: ${config.port}`);
|
|
435
636
|
console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
|
|
436
637
|
console.log(`\n Hosts (${config.hosts.length}):`);
|
|
437
638
|
if (config.hosts.length === 0) {
|
|
438
|
-
console.log(
|
|
639
|
+
console.log(` ${ANSI.dim}(none)${ANSI.reset}`);
|
|
439
640
|
}
|
|
440
641
|
else {
|
|
441
642
|
for (const host of config.hosts) {
|
|
@@ -539,130 +740,6 @@ export function createPayaraCLIPlugin() {
|
|
|
539
740
|
process.exit(1);
|
|
540
741
|
}
|
|
541
742
|
});
|
|
542
|
-
// deploy config push - Push configs to vault
|
|
543
|
-
configCmd
|
|
544
|
-
.command('push')
|
|
545
|
-
.description('Push deployment configs to vault for sharing/backup')
|
|
546
|
-
.option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
|
|
547
|
-
.action(async (options) => {
|
|
548
|
-
try {
|
|
549
|
-
const alias = options.alias ?? 'deploy/configs';
|
|
550
|
-
const store = await loadDeployConfigs();
|
|
551
|
-
if (Object.keys(store.configs).length === 0) {
|
|
552
|
-
ctx.output.error('No configs to push');
|
|
553
|
-
process.exit(1);
|
|
554
|
-
}
|
|
555
|
-
ctx.output.info(`Pushing ${Object.keys(store.configs).length} config(s) to vault...`);
|
|
556
|
-
// Create/update secret in vault
|
|
557
|
-
const secretData = {
|
|
558
|
-
configs: store.configs,
|
|
559
|
-
pushedAt: new Date().toISOString(),
|
|
560
|
-
pushedFrom: hostname(),
|
|
561
|
-
};
|
|
562
|
-
try {
|
|
563
|
-
// Try to update existing secret
|
|
564
|
-
await ctx.client.post(`/v1/secrets/by-alias/${encodeURIComponent(alias)}`, {
|
|
565
|
-
value: JSON.stringify(secretData, null, 2),
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
// Create new secret
|
|
570
|
-
await ctx.client.post('/v1/secrets', {
|
|
571
|
-
alias,
|
|
572
|
-
name: 'Deployment Configurations',
|
|
573
|
-
description: 'Shared deployment configs for znvault deploy command',
|
|
574
|
-
value: JSON.stringify(secretData, null, 2),
|
|
575
|
-
type: 'generic',
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
// Update local store with vault info
|
|
579
|
-
store.vaultEnabled = true;
|
|
580
|
-
store.vaultAlias = alias;
|
|
581
|
-
await saveDeployConfigs(store);
|
|
582
|
-
ctx.output.success(`Pushed to vault: ${alias}`);
|
|
583
|
-
ctx.output.info('Other users can pull with: znvault deploy config pull');
|
|
584
|
-
}
|
|
585
|
-
catch (err) {
|
|
586
|
-
ctx.output.error(`Failed to push: ${err instanceof Error ? err.message : String(err)}`);
|
|
587
|
-
process.exit(1);
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
// deploy config pull - Pull configs from vault
|
|
591
|
-
configCmd
|
|
592
|
-
.command('pull')
|
|
593
|
-
.description('Pull deployment configs from vault')
|
|
594
|
-
.option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
|
|
595
|
-
.option('--merge', 'Merge with local configs instead of replacing')
|
|
596
|
-
.action(async (options) => {
|
|
597
|
-
try {
|
|
598
|
-
const localStore = await loadDeployConfigs();
|
|
599
|
-
const alias = options.alias ?? localStore.vaultAlias ?? 'deploy/configs';
|
|
600
|
-
ctx.output.info(`Pulling configs from vault: ${alias}...`);
|
|
601
|
-
const response = await ctx.client.get(`/v1/secrets/by-alias/${encodeURIComponent(alias)}/value`);
|
|
602
|
-
const vaultData = JSON.parse(response.value);
|
|
603
|
-
const vaultConfigs = vaultData.configs;
|
|
604
|
-
const configCount = Object.keys(vaultConfigs).length;
|
|
605
|
-
if (options.merge) {
|
|
606
|
-
// Merge: vault configs override local on conflict
|
|
607
|
-
const newStore = {
|
|
608
|
-
configs: { ...localStore.configs, ...vaultConfigs },
|
|
609
|
-
vaultEnabled: true,
|
|
610
|
-
vaultAlias: alias,
|
|
611
|
-
};
|
|
612
|
-
await saveDeployConfigs(newStore);
|
|
613
|
-
const merged = Object.keys(newStore.configs).length;
|
|
614
|
-
ctx.output.success(`Merged ${configCount} config(s) from vault (${merged} total)`);
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// Replace local configs
|
|
618
|
-
const newStore = {
|
|
619
|
-
configs: vaultConfigs,
|
|
620
|
-
vaultEnabled: true,
|
|
621
|
-
vaultAlias: alias,
|
|
622
|
-
};
|
|
623
|
-
await saveDeployConfigs(newStore);
|
|
624
|
-
ctx.output.success(`Pulled ${configCount} config(s) from vault`);
|
|
625
|
-
}
|
|
626
|
-
ctx.output.info(`Last pushed: ${vaultData.pushedAt} from ${vaultData.pushedFrom}`);
|
|
627
|
-
}
|
|
628
|
-
catch (err) {
|
|
629
|
-
if (String(err).includes('404') || String(err).includes('not found')) {
|
|
630
|
-
ctx.output.error('No configs found in vault');
|
|
631
|
-
ctx.output.info('Push configs first with: znvault deploy config push');
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
ctx.output.error(`Failed to pull: ${err instanceof Error ? err.message : String(err)}`);
|
|
635
|
-
}
|
|
636
|
-
process.exit(1);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
// deploy config sync - Show sync status
|
|
640
|
-
configCmd
|
|
641
|
-
.command('sync')
|
|
642
|
-
.description('Show vault sync status')
|
|
643
|
-
.action(async () => {
|
|
644
|
-
try {
|
|
645
|
-
const store = await loadDeployConfigs();
|
|
646
|
-
console.log('\nVault Sync Status:\n');
|
|
647
|
-
if (!store.vaultEnabled) {
|
|
648
|
-
console.log(' Status: Local only (not synced to vault)');
|
|
649
|
-
console.log(' Configs: ' + Object.keys(store.configs).length);
|
|
650
|
-
console.log('\n To enable vault sync: znvault deploy config push');
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
console.log(' Status: Vault sync enabled');
|
|
654
|
-
console.log(' Alias: ' + (store.vaultAlias ?? 'deploy/configs'));
|
|
655
|
-
console.log(' Configs: ' + Object.keys(store.configs).length);
|
|
656
|
-
console.log('\n Push changes: znvault deploy config push');
|
|
657
|
-
console.log(' Pull updates: znvault deploy config pull');
|
|
658
|
-
}
|
|
659
|
-
console.log();
|
|
660
|
-
}
|
|
661
|
-
catch (err) {
|
|
662
|
-
ctx.output.error(`Failed to get sync status: ${err instanceof Error ? err.message : String(err)}`);
|
|
663
|
-
process.exit(1);
|
|
664
|
-
}
|
|
665
|
-
});
|
|
666
743
|
// deploy config set <name> <key> <value>
|
|
667
744
|
configCmd
|
|
668
745
|
.command('set <name> <key> <value>')
|
|
@@ -718,20 +795,21 @@ export function createPayaraCLIPlugin() {
|
|
|
718
795
|
.option('-f, --force', 'Force full deployment (no diff)')
|
|
719
796
|
.option('--dry-run', 'Show what would be deployed without deploying')
|
|
720
797
|
.action(async (warFile, options) => {
|
|
798
|
+
const progress = new ProgressReporter(ctx.isPlainMode());
|
|
721
799
|
try {
|
|
722
800
|
// Verify WAR file exists
|
|
801
|
+
let warStats;
|
|
723
802
|
try {
|
|
724
|
-
await stat(warFile);
|
|
803
|
+
warStats = await stat(warFile);
|
|
725
804
|
}
|
|
726
805
|
catch {
|
|
727
806
|
ctx.output.error(`WAR file not found: ${warFile}`);
|
|
728
807
|
process.exit(1);
|
|
729
808
|
}
|
|
730
|
-
|
|
809
|
+
progress.analyzing(warFile);
|
|
731
810
|
// Calculate local hashes
|
|
732
811
|
const localHashes = await calculateWarHashes(warFile);
|
|
733
|
-
|
|
734
|
-
ctx.output.info(`Found ${fileCount} files in WAR`);
|
|
812
|
+
progress.foundFiles(Object.keys(localHashes).length, warStats.size);
|
|
735
813
|
// Build target URL
|
|
736
814
|
const target = options.target ?? ctx.getConfig().url;
|
|
737
815
|
const baseUrl = target.replace(/\/$/, '');
|
|
@@ -761,44 +839,45 @@ export function createPayaraCLIPlugin() {
|
|
|
761
839
|
ctx.output.info('Remote has no WAR, will upload full WAR file');
|
|
762
840
|
}
|
|
763
841
|
else {
|
|
764
|
-
|
|
842
|
+
progress.diff(changed.length, deleted.length);
|
|
765
843
|
}
|
|
766
844
|
// Dry run - just show what would be deployed
|
|
767
845
|
if (options.dryRun) {
|
|
768
846
|
if (remoteIsEmpty) {
|
|
769
|
-
ctx.output.info(`Would upload full WAR (${
|
|
847
|
+
ctx.output.info(`Would upload full WAR (${Object.keys(localHashes).length} files)`);
|
|
770
848
|
return;
|
|
771
849
|
}
|
|
772
850
|
if (changed.length > 0) {
|
|
773
851
|
ctx.output.info('\nFiles to update:');
|
|
774
852
|
for (const file of changed.slice(0, 20)) {
|
|
775
|
-
console.log(`
|
|
853
|
+
console.log(` ${ANSI.green}+${ANSI.reset} ${file}`);
|
|
776
854
|
}
|
|
777
855
|
if (changed.length > 20) {
|
|
778
|
-
console.log(` ... and ${changed.length - 20} more`);
|
|
856
|
+
console.log(` ${ANSI.dim}... and ${changed.length - 20} more${ANSI.reset}`);
|
|
779
857
|
}
|
|
780
858
|
}
|
|
781
859
|
if (deleted.length > 0) {
|
|
782
860
|
ctx.output.info('\nFiles to delete:');
|
|
783
861
|
for (const file of deleted.slice(0, 20)) {
|
|
784
|
-
console.log(`
|
|
862
|
+
console.log(` ${ANSI.red}-${ANSI.reset} ${file}`);
|
|
785
863
|
}
|
|
786
864
|
if (deleted.length > 20) {
|
|
787
|
-
console.log(` ... and ${deleted.length - 20} more`);
|
|
865
|
+
console.log(` ${ANSI.dim}... and ${deleted.length - 20} more${ANSI.reset}`);
|
|
788
866
|
}
|
|
789
867
|
}
|
|
790
868
|
if (changed.length === 0 && deleted.length === 0) {
|
|
791
|
-
|
|
869
|
+
progress.noChanges();
|
|
792
870
|
}
|
|
793
871
|
return;
|
|
794
872
|
}
|
|
795
|
-
// Deploy using deployToHost
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
873
|
+
// Deploy using deployToHost
|
|
874
|
+
progress.setHost(target);
|
|
875
|
+
const result = await deployToHost(ctx, target, parseInt(options.port, 10), warFile, localHashes, options.force ?? false, progress);
|
|
876
|
+
if (result.success && result.result) {
|
|
877
|
+
progress.deployed(result.result);
|
|
799
878
|
}
|
|
800
879
|
else {
|
|
801
|
-
|
|
880
|
+
progress.failed(result.error ?? 'Unknown error');
|
|
802
881
|
process.exit(1);
|
|
803
882
|
}
|
|
804
883
|
}
|
|
@@ -827,14 +906,14 @@ export function createPayaraCLIPlugin() {
|
|
|
827
906
|
}
|
|
828
907
|
ctx.output.info(`Restarting Payara on ${config.hosts.length} host(s)...`);
|
|
829
908
|
for (const host of config.hosts) {
|
|
830
|
-
const baseUrl = host.startsWith('http') ? host : `
|
|
909
|
+
const baseUrl = host.startsWith('http') ? host : `http://${host}`;
|
|
831
910
|
const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
|
|
832
911
|
try {
|
|
833
912
|
await ctx.client.post(`${pluginUrl}/restart`, {});
|
|
834
|
-
|
|
913
|
+
console.log(` ${ANSI.green}✓${ANSI.reset} ${host} restarted`);
|
|
835
914
|
}
|
|
836
915
|
catch (err) {
|
|
837
|
-
|
|
916
|
+
console.log(` ${ANSI.red}✗${ANSI.reset} ${host}: ${err instanceof Error ? err.message : String(err)}`);
|
|
838
917
|
}
|
|
839
918
|
}
|
|
840
919
|
}
|
|
@@ -842,7 +921,8 @@ export function createPayaraCLIPlugin() {
|
|
|
842
921
|
// Single host restart
|
|
843
922
|
const target = options.target ?? ctx.getConfig().url;
|
|
844
923
|
const baseUrl = target.replace(/\/$/, '');
|
|
845
|
-
const
|
|
924
|
+
const fullUrl = baseUrl.startsWith('http') ? baseUrl : `http://${baseUrl}`;
|
|
925
|
+
const pluginUrl = `${fullUrl}:${options.port}/plugins/payara`;
|
|
846
926
|
ctx.output.info('Restarting Payara...');
|
|
847
927
|
await ctx.client.post(`${pluginUrl}/restart`, {});
|
|
848
928
|
ctx.output.success('Payara restarted');
|
|
@@ -871,18 +951,19 @@ export function createPayaraCLIPlugin() {
|
|
|
871
951
|
ctx.output.error(`Config '${configName}' not found`);
|
|
872
952
|
process.exit(1);
|
|
873
953
|
}
|
|
874
|
-
console.log(`\
|
|
954
|
+
console.log(`\n${ANSI.bold}Status for ${configName}:${ANSI.reset}\n`);
|
|
875
955
|
for (const host of config.hosts) {
|
|
876
|
-
const baseUrl = host.startsWith('http') ? host : `
|
|
956
|
+
const baseUrl = host.startsWith('http') ? host : `http://${host}`;
|
|
877
957
|
const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
|
|
878
958
|
try {
|
|
879
959
|
const status = await ctx.client.get(`${pluginUrl}/status`);
|
|
880
|
-
const icon = status.healthy ? '✓' : status.running ? '!' : '✗';
|
|
881
|
-
const state = status.healthy ? 'healthy' : status.running ? 'degraded' : 'down';
|
|
882
|
-
|
|
960
|
+
const icon = status.healthy && status.appDeployed ? ANSI.green + '✓' : status.running ? ANSI.yellow + '!' : ANSI.red + '✗';
|
|
961
|
+
const state = status.healthy && status.appDeployed ? 'healthy' : status.running ? 'degraded' : 'down';
|
|
962
|
+
const appInfo = status.appDeployed ? `${status.appName || 'app'} deployed` : 'no app';
|
|
963
|
+
console.log(` ${icon}${ANSI.reset} ${host}: ${state} (${status.domain}, ${appInfo})`);
|
|
883
964
|
}
|
|
884
|
-
catch
|
|
885
|
-
console.log(`
|
|
965
|
+
catch {
|
|
966
|
+
console.log(` ${ANSI.red}✗${ANSI.reset} ${host}: unreachable`);
|
|
886
967
|
}
|
|
887
968
|
}
|
|
888
969
|
console.log();
|
|
@@ -891,12 +972,16 @@ export function createPayaraCLIPlugin() {
|
|
|
891
972
|
// Single host status
|
|
892
973
|
const target = options.target ?? ctx.getConfig().url;
|
|
893
974
|
const baseUrl = target.replace(/\/$/, '');
|
|
894
|
-
const
|
|
975
|
+
const fullUrl = baseUrl.startsWith('http') ? baseUrl : `http://${baseUrl}`;
|
|
976
|
+
const pluginUrl = `${fullUrl}:${options.port}/plugins/payara`;
|
|
895
977
|
const status = await ctx.client.get(`${pluginUrl}/status`);
|
|
896
978
|
ctx.output.keyValue({
|
|
897
979
|
'Domain': status.domain,
|
|
898
980
|
'Running': status.running,
|
|
899
981
|
'Healthy': status.healthy,
|
|
982
|
+
'App Deployed': status.appDeployed ?? false,
|
|
983
|
+
'App Name': status.appName ?? 'N/A',
|
|
984
|
+
'WAR Path': status.warPath ?? 'N/A',
|
|
900
985
|
'PID': status.pid ?? 'N/A',
|
|
901
986
|
});
|
|
902
987
|
}
|
|
@@ -919,7 +1004,8 @@ export function createPayaraCLIPlugin() {
|
|
|
919
1004
|
try {
|
|
920
1005
|
const target = options.target ?? ctx.getConfig().url;
|
|
921
1006
|
const baseUrl = target.replace(/\/$/, '');
|
|
922
|
-
const
|
|
1007
|
+
const fullUrl = baseUrl.startsWith('http') ? baseUrl : `http://${baseUrl}`;
|
|
1008
|
+
const pluginUrl = `${fullUrl}:${options.port}/plugins/payara`;
|
|
923
1009
|
const response = await ctx.client.get(`${pluginUrl}/applications`);
|
|
924
1010
|
if (response.applications.length === 0) {
|
|
925
1011
|
ctx.output.info('No applications deployed');
|