@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.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, hostname } from 'node:os';
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
- const error = await response.json().catch(() => ({ message: response.statusText }));
60
- return { success: false, error: error.message ?? 'Upload failed' };
233
+ return { success: false, error: data.message ?? data.error ?? 'Upload failed' };
61
234
  }
62
- return { success: true };
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, onProgress) {
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
- if (onProgress) {
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: true,
114
- filesChanged: response.result.filesChanged,
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, onProgress) {
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
- if (onProgress)
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
- return { success: true, filesChanged: 0, filesDeleted: 0 };
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
- if (onProgress)
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
- filesChanged: deployResponse.filesChanged,
202
- filesDeleted: deployResponse.filesDeleted,
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.4.3',
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
- ctx.output.info(`Deploying ${configName}`);
265
- ctx.output.info(` WAR: ${warPath}`);
266
- ctx.output.info(` Hosts: ${config.hosts.length}`);
267
- ctx.output.info(` Mode: ${options.sequential || !config.parallel ? 'sequential' : 'parallel'}`);
268
- console.log();
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
- ctx.output.info('Analyzing WAR file...');
477
+ progress.analyzing(warPath);
271
478
  const localHashes = await calculateWarHashes(warPath);
272
- ctx.output.info(`Found ${Object.keys(localHashes).length} files`);
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
- ctx.output.info(`Deploying to ${host}...`);
284
- const result = await deployToHost(ctx, host, config.port, warPath, localHashes, options.force ?? false, (message) => ctx.output.info(` ${host}: ${message}`));
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
- changed: result.filesChanged,
290
- deleted: result.filesDeleted,
495
+ result: result.result,
291
496
  });
292
- if (result.success) {
293
- ctx.output.success(` ✓ ${host}: ${result.filesChanged} changed, ${result.filesDeleted} deleted`);
497
+ if (result.success && result.result) {
498
+ progress.deployed(result.result);
294
499
  }
295
500
  else {
296
- ctx.output.error(` ✗ ${host}: ${result.error}`);
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
- if (failed === 0) {
313
- ctx.output.success(`Deployment complete: ${successful}/${config.hosts.length} hosts successful`);
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('Deployment Configurations:\n');
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(`\nDeployment Config: ${config.name}\n`);
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(' (none)');
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
- ctx.output.info(`Analyzing WAR file: ${warFile}`);
809
+ progress.analyzing(warFile);
731
810
  // Calculate local hashes
732
811
  const localHashes = await calculateWarHashes(warFile);
733
- const fileCount = Object.keys(localHashes).length;
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
- ctx.output.info(`Diff: ${changed.length} changed, ${deleted.length} deleted`);
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 (${fileCount} files)`);
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(` + ${file}`);
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(` - ${file}`);
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
- ctx.output.success('No changes to deploy');
869
+ progress.noChanges();
792
870
  }
793
871
  return;
794
872
  }
795
- // Deploy using deployToHost (handles full upload, chunking, etc.)
796
- const result = await deployToHost(ctx, target, parseInt(options.port, 10), warFile, localHashes, options.force ?? false, (message) => ctx.output.info(message));
797
- if (result.success) {
798
- ctx.output.success(`Deployed: ${result.filesChanged} files changed, ${result.filesDeleted} deleted`);
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
- ctx.output.error(`Deployment failed: ${result.error}`);
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 : `https://${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
- ctx.output.success(` ${host} restarted`);
913
+ console.log(` ${ANSI.green}✓${ANSI.reset} ${host} restarted`);
835
914
  }
836
915
  catch (err) {
837
- ctx.output.error(` ${host}: ${err instanceof Error ? err.message : String(err)}`);
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 pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
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(`\nStatus for ${configName}:\n`);
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 : `https://${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
- console.log(` ${icon} ${host}: ${state} (${status.domain})`);
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 (err) {
885
- console.log(` ${host}: unreachable`);
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 pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
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 pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
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');