@zincapp/znvault-plugin-payara 1.0.6 → 1.3.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/cli.js CHANGED
@@ -1,8 +1,98 @@
1
1
  // Path: src/cli.ts
2
2
  // CLI commands for Payara plugin
3
3
  import { createHash } from 'node:crypto';
4
- import { stat } from 'node:fs/promises';
4
+ import { stat, readFile, writeFile, mkdir } from 'node:fs/promises';
5
+ import { existsSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { homedir, hostname } from 'node:os';
5
8
  import AdmZip from 'adm-zip';
9
+ // Config file path
10
+ const CONFIG_DIR = join(homedir(), '.znvault');
11
+ const CONFIG_FILE = join(CONFIG_DIR, 'deploy-configs.json');
12
+ /**
13
+ * Load deployment configs
14
+ */
15
+ async function loadDeployConfigs() {
16
+ try {
17
+ if (existsSync(CONFIG_FILE)) {
18
+ const content = await readFile(CONFIG_FILE, 'utf-8');
19
+ return JSON.parse(content);
20
+ }
21
+ }
22
+ catch {
23
+ // Ignore parse errors
24
+ }
25
+ return { configs: {} };
26
+ }
27
+ /**
28
+ * Save deployment configs
29
+ */
30
+ async function saveDeployConfigs(store) {
31
+ if (!existsSync(CONFIG_DIR)) {
32
+ await mkdir(CONFIG_DIR, { recursive: true });
33
+ }
34
+ await writeFile(CONFIG_FILE, JSON.stringify(store, null, 2));
35
+ }
36
+ /**
37
+ * Deploy to a single host
38
+ */
39
+ async function deployToHost(ctx, host, port, warPath, localHashes, force) {
40
+ try {
41
+ const baseUrl = host.replace(/\/$/, '');
42
+ // Add protocol if missing
43
+ const fullUrl = baseUrl.startsWith('http') ? baseUrl : `https://${baseUrl}`;
44
+ const pluginUrl = `${fullUrl}:${port}/plugins/payara`;
45
+ // Get remote hashes
46
+ let remoteHashes = {};
47
+ if (!force) {
48
+ try {
49
+ const response = await ctx.client.get(`${pluginUrl}/hashes`);
50
+ remoteHashes = response.hashes;
51
+ }
52
+ catch {
53
+ // Full deployment if can't get hashes
54
+ }
55
+ }
56
+ // Calculate diff
57
+ const { changed, deleted } = calculateDiff(localHashes, remoteHashes);
58
+ if (changed.length === 0 && deleted.length === 0) {
59
+ return { success: true, filesChanged: 0, filesDeleted: 0 };
60
+ }
61
+ // Prepare files for upload
62
+ const zip = new AdmZip(warPath);
63
+ const files = changed.map(path => {
64
+ const entry = zip.getEntry(path);
65
+ if (!entry) {
66
+ throw new Error(`Entry not found in WAR: ${path}`);
67
+ }
68
+ return {
69
+ path,
70
+ content: entry.getData().toString('base64'),
71
+ };
72
+ });
73
+ // Deploy
74
+ const deployResponse = await ctx.client.post(`${pluginUrl}/deploy`, {
75
+ files,
76
+ deletions: deleted,
77
+ });
78
+ if (deployResponse.status === 'deployed') {
79
+ return {
80
+ success: true,
81
+ filesChanged: deployResponse.filesChanged,
82
+ filesDeleted: deployResponse.filesDeleted,
83
+ };
84
+ }
85
+ else {
86
+ return { success: false, error: deployResponse.message };
87
+ }
88
+ }
89
+ catch (err) {
90
+ return {
91
+ success: false,
92
+ error: err instanceof Error ? err.message : String(err),
93
+ };
94
+ }
95
+ }
6
96
  /**
7
97
  * Payara CLI plugin
8
98
  *
@@ -11,17 +101,498 @@ import AdmZip from 'adm-zip';
11
101
  export function createPayaraCLIPlugin() {
12
102
  return {
13
103
  name: 'payara',
14
- version: '1.0.0',
104
+ version: '1.1.0',
15
105
  description: 'Payara WAR deployment commands',
16
106
  registerCommands(program, ctx) {
17
107
  // Create deploy command group
18
108
  const deploy = program
19
109
  .command('deploy')
20
110
  .description('Deploy WAR files to remote Payara servers');
21
- // deploy war <file>
111
+ // ========================================================================
112
+ // deploy <config-name> - Deploy using saved configuration
113
+ // ========================================================================
114
+ deploy
115
+ .command('run <configName>')
116
+ .alias('to')
117
+ .description('Deploy WAR to all hosts in a saved configuration')
118
+ .option('-f, --force', 'Force full deployment (no diff)')
119
+ .option('--dry-run', 'Show what would be deployed without deploying')
120
+ .option('--sequential', 'Deploy to hosts one at a time (override parallel setting)')
121
+ .action(async (configName, options) => {
122
+ try {
123
+ const store = await loadDeployConfigs();
124
+ const config = store.configs[configName];
125
+ if (!config) {
126
+ ctx.output.error(`Deployment config '${configName}' not found`);
127
+ ctx.output.info('Use "znvault deploy config list" to see available configs');
128
+ process.exit(1);
129
+ }
130
+ if (config.hosts.length === 0) {
131
+ ctx.output.error('No hosts configured for this deployment');
132
+ ctx.output.info(`Use "znvault deploy config add-host ${configName} <host>" to add hosts`);
133
+ process.exit(1);
134
+ }
135
+ // Resolve WAR path
136
+ const warPath = resolve(config.warPath);
137
+ try {
138
+ await stat(warPath);
139
+ }
140
+ catch {
141
+ ctx.output.error(`WAR file not found: ${warPath}`);
142
+ process.exit(1);
143
+ }
144
+ ctx.output.info(`Deploying ${configName}`);
145
+ ctx.output.info(` WAR: ${warPath}`);
146
+ ctx.output.info(` Hosts: ${config.hosts.length}`);
147
+ ctx.output.info(` Mode: ${options.sequential || !config.parallel ? 'sequential' : 'parallel'}`);
148
+ console.log();
149
+ // Calculate local hashes once
150
+ ctx.output.info('Analyzing WAR file...');
151
+ const localHashes = await calculateWarHashes(warPath);
152
+ ctx.output.info(`Found ${Object.keys(localHashes).length} files`);
153
+ console.log();
154
+ if (options.dryRun) {
155
+ ctx.output.info('Dry run - checking each host:');
156
+ for (const host of config.hosts) {
157
+ console.log(` - ${host}`);
158
+ }
159
+ return;
160
+ }
161
+ const results = [];
162
+ const deployToHostWrapper = async (host) => {
163
+ ctx.output.info(`Deploying to ${host}...`);
164
+ const result = await deployToHost(ctx, host, config.port, warPath, localHashes, options.force ?? false);
165
+ results.push({
166
+ host,
167
+ success: result.success,
168
+ error: result.error,
169
+ changed: result.filesChanged,
170
+ deleted: result.filesDeleted,
171
+ });
172
+ if (result.success) {
173
+ ctx.output.success(` ✓ ${host}: ${result.filesChanged} changed, ${result.filesDeleted} deleted`);
174
+ }
175
+ else {
176
+ ctx.output.error(` ✗ ${host}: ${result.error}`);
177
+ }
178
+ };
179
+ if (options.sequential || !config.parallel) {
180
+ // Sequential deployment
181
+ for (const host of config.hosts) {
182
+ await deployToHostWrapper(host);
183
+ }
184
+ }
185
+ else {
186
+ // Parallel deployment
187
+ await Promise.all(config.hosts.map(deployToHostWrapper));
188
+ }
189
+ console.log();
190
+ const successful = results.filter(r => r.success).length;
191
+ const failed = results.filter(r => !r.success).length;
192
+ if (failed === 0) {
193
+ ctx.output.success(`Deployment complete: ${successful}/${config.hosts.length} hosts successful`);
194
+ }
195
+ else {
196
+ ctx.output.warn(`Deployment complete: ${successful}/${config.hosts.length} hosts successful, ${failed} failed`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+ catch (err) {
201
+ ctx.output.error(`Deployment failed: ${err instanceof Error ? err.message : String(err)}`);
202
+ process.exit(1);
203
+ }
204
+ });
205
+ // ========================================================================
206
+ // deploy config - Manage deployment configurations
207
+ // ========================================================================
208
+ const configCmd = deploy
209
+ .command('config')
210
+ .description('Manage deployment configurations');
211
+ // deploy config create <name>
212
+ configCmd
213
+ .command('create <name>')
214
+ .description('Create a new deployment configuration')
215
+ .option('-w, --war <path>', 'Path to WAR file')
216
+ .option('-H, --host <host>', 'Add a host (can be used multiple times)', (val, arr) => [...arr, val], [])
217
+ .option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
218
+ .option('--parallel', 'Deploy to all hosts in parallel (default)')
219
+ .option('--sequential', 'Deploy to hosts one at a time')
220
+ .option('-d, --description <text>', 'Description for this config')
221
+ .action(async (name, options) => {
222
+ try {
223
+ const store = await loadDeployConfigs();
224
+ if (store.configs[name]) {
225
+ ctx.output.error(`Config '${name}' already exists. Use "znvault deploy config delete ${name}" first.`);
226
+ process.exit(1);
227
+ }
228
+ const config = {
229
+ name,
230
+ hosts: options.host,
231
+ warPath: options.war ?? '',
232
+ port: parseInt(options.port, 10),
233
+ parallel: !options.sequential,
234
+ description: options.description,
235
+ };
236
+ store.configs[name] = config;
237
+ await saveDeployConfigs(store);
238
+ ctx.output.success(`Created deployment config: ${name}`);
239
+ if (config.hosts.length === 0) {
240
+ ctx.output.info(`Add hosts with: znvault deploy config add-host ${name} <host>`);
241
+ }
242
+ if (!config.warPath) {
243
+ ctx.output.info(`Set WAR path with: znvault deploy config set ${name} war /path/to/app.war`);
244
+ }
245
+ }
246
+ catch (err) {
247
+ ctx.output.error(`Failed to create config: ${err instanceof Error ? err.message : String(err)}`);
248
+ process.exit(1);
249
+ }
250
+ });
251
+ // deploy config list
252
+ configCmd
253
+ .command('list')
254
+ .alias('ls')
255
+ .description('List all deployment configurations')
256
+ .option('--json', 'Output as JSON')
257
+ .action(async (options) => {
258
+ try {
259
+ const store = await loadDeployConfigs();
260
+ const configs = Object.values(store.configs);
261
+ if (configs.length === 0) {
262
+ if (options.json) {
263
+ console.log(JSON.stringify([], null, 2));
264
+ }
265
+ else {
266
+ ctx.output.info('No deployment configurations found.');
267
+ ctx.output.info('Create one with: znvault deploy config create <name>');
268
+ }
269
+ return;
270
+ }
271
+ if (options.json) {
272
+ console.log(JSON.stringify(configs, null, 2));
273
+ return;
274
+ }
275
+ console.log('Deployment Configurations:\n');
276
+ for (const config of configs) {
277
+ console.log(` ${config.name}`);
278
+ if (config.description) {
279
+ console.log(` ${config.description}`);
280
+ }
281
+ console.log(` Hosts: ${config.hosts.length > 0 ? config.hosts.join(', ') : '(none)'}`);
282
+ console.log(` WAR: ${config.warPath || '(not set)'}`);
283
+ console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
284
+ console.log();
285
+ }
286
+ }
287
+ catch (err) {
288
+ ctx.output.error(`Failed to list configs: ${err instanceof Error ? err.message : String(err)}`);
289
+ process.exit(1);
290
+ }
291
+ });
292
+ // deploy config show <name>
293
+ configCmd
294
+ .command('show <name>')
295
+ .description('Show deployment configuration details')
296
+ .option('--json', 'Output as JSON')
297
+ .action(async (name, options) => {
298
+ try {
299
+ const store = await loadDeployConfigs();
300
+ const config = store.configs[name];
301
+ if (!config) {
302
+ ctx.output.error(`Config '${name}' not found`);
303
+ process.exit(1);
304
+ }
305
+ if (options.json) {
306
+ console.log(JSON.stringify(config, null, 2));
307
+ return;
308
+ }
309
+ console.log(`\nDeployment Config: ${config.name}\n`);
310
+ if (config.description) {
311
+ console.log(` Description: ${config.description}`);
312
+ }
313
+ console.log(` WAR Path: ${config.warPath || '(not set)'}`);
314
+ console.log(` Port: ${config.port}`);
315
+ console.log(` Mode: ${config.parallel ? 'parallel' : 'sequential'}`);
316
+ console.log(`\n Hosts (${config.hosts.length}):`);
317
+ if (config.hosts.length === 0) {
318
+ console.log(' (none)');
319
+ }
320
+ else {
321
+ for (const host of config.hosts) {
322
+ console.log(` - ${host}`);
323
+ }
324
+ }
325
+ console.log();
326
+ }
327
+ catch (err) {
328
+ ctx.output.error(`Failed to show config: ${err instanceof Error ? err.message : String(err)}`);
329
+ process.exit(1);
330
+ }
331
+ });
332
+ // deploy config delete <name>
333
+ configCmd
334
+ .command('delete <name>')
335
+ .alias('rm')
336
+ .description('Delete a deployment configuration')
337
+ .option('-y, --yes', 'Skip confirmation')
338
+ .action(async (name, options) => {
339
+ try {
340
+ const store = await loadDeployConfigs();
341
+ if (!store.configs[name]) {
342
+ ctx.output.error(`Config '${name}' not found`);
343
+ process.exit(1);
344
+ }
345
+ if (!options.yes) {
346
+ // Dynamic import of inquirer (available from znvault-cli context)
347
+ const inquirerModule = await import('inquirer');
348
+ const inquirer = inquirerModule.default;
349
+ const answers = await inquirer.prompt([{
350
+ type: 'confirm',
351
+ name: 'confirm',
352
+ message: `Delete deployment config '${name}'?`,
353
+ default: false,
354
+ }]);
355
+ if (!answers.confirm) {
356
+ ctx.output.info('Cancelled');
357
+ return;
358
+ }
359
+ }
360
+ delete store.configs[name];
361
+ await saveDeployConfigs(store);
362
+ ctx.output.success(`Deleted config: ${name}`);
363
+ }
364
+ catch (err) {
365
+ ctx.output.error(`Failed to delete config: ${err instanceof Error ? err.message : String(err)}`);
366
+ process.exit(1);
367
+ }
368
+ });
369
+ // deploy config add-host <name> <host>
370
+ configCmd
371
+ .command('add-host <name> <host>')
372
+ .description('Add a host to deployment configuration')
373
+ .action(async (name, host) => {
374
+ try {
375
+ const store = await loadDeployConfigs();
376
+ const config = store.configs[name];
377
+ if (!config) {
378
+ ctx.output.error(`Config '${name}' not found`);
379
+ process.exit(1);
380
+ }
381
+ if (config.hosts.includes(host)) {
382
+ ctx.output.warn(`Host '${host}' already in config`);
383
+ return;
384
+ }
385
+ config.hosts.push(host);
386
+ await saveDeployConfigs(store);
387
+ ctx.output.success(`Added host: ${host}`);
388
+ ctx.output.info(`Config '${name}' now has ${config.hosts.length} host(s)`);
389
+ }
390
+ catch (err) {
391
+ ctx.output.error(`Failed to add host: ${err instanceof Error ? err.message : String(err)}`);
392
+ process.exit(1);
393
+ }
394
+ });
395
+ // deploy config remove-host <name> <host>
396
+ configCmd
397
+ .command('remove-host <name> <host>')
398
+ .description('Remove a host from deployment configuration')
399
+ .action(async (name, host) => {
400
+ try {
401
+ const store = await loadDeployConfigs();
402
+ const config = store.configs[name];
403
+ if (!config) {
404
+ ctx.output.error(`Config '${name}' not found`);
405
+ process.exit(1);
406
+ }
407
+ const index = config.hosts.indexOf(host);
408
+ if (index === -1) {
409
+ ctx.output.error(`Host '${host}' not found in config`);
410
+ process.exit(1);
411
+ }
412
+ config.hosts.splice(index, 1);
413
+ await saveDeployConfigs(store);
414
+ ctx.output.success(`Removed host: ${host}`);
415
+ ctx.output.info(`Config '${name}' now has ${config.hosts.length} host(s)`);
416
+ }
417
+ catch (err) {
418
+ ctx.output.error(`Failed to remove host: ${err instanceof Error ? err.message : String(err)}`);
419
+ process.exit(1);
420
+ }
421
+ });
422
+ // deploy config push - Push configs to vault
423
+ configCmd
424
+ .command('push')
425
+ .description('Push deployment configs to vault for sharing/backup')
426
+ .option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
427
+ .action(async (options) => {
428
+ try {
429
+ const alias = options.alias ?? 'deploy/configs';
430
+ const store = await loadDeployConfigs();
431
+ if (Object.keys(store.configs).length === 0) {
432
+ ctx.output.error('No configs to push');
433
+ process.exit(1);
434
+ }
435
+ ctx.output.info(`Pushing ${Object.keys(store.configs).length} config(s) to vault...`);
436
+ // Create/update secret in vault
437
+ const secretData = {
438
+ configs: store.configs,
439
+ pushedAt: new Date().toISOString(),
440
+ pushedFrom: hostname(),
441
+ };
442
+ try {
443
+ // Try to update existing secret
444
+ await ctx.client.post(`/v1/secrets/by-alias/${encodeURIComponent(alias)}`, {
445
+ value: JSON.stringify(secretData, null, 2),
446
+ });
447
+ }
448
+ catch {
449
+ // Create new secret
450
+ await ctx.client.post('/v1/secrets', {
451
+ alias,
452
+ name: 'Deployment Configurations',
453
+ description: 'Shared deployment configs for znvault deploy command',
454
+ value: JSON.stringify(secretData, null, 2),
455
+ type: 'generic',
456
+ });
457
+ }
458
+ // Update local store with vault info
459
+ store.vaultEnabled = true;
460
+ store.vaultAlias = alias;
461
+ await saveDeployConfigs(store);
462
+ ctx.output.success(`Pushed to vault: ${alias}`);
463
+ ctx.output.info('Other users can pull with: znvault deploy config pull');
464
+ }
465
+ catch (err) {
466
+ ctx.output.error(`Failed to push: ${err instanceof Error ? err.message : String(err)}`);
467
+ process.exit(1);
468
+ }
469
+ });
470
+ // deploy config pull - Pull configs from vault
471
+ configCmd
472
+ .command('pull')
473
+ .description('Pull deployment configs from vault')
474
+ .option('-a, --alias <alias>', 'Vault secret alias (default: deploy/configs)')
475
+ .option('--merge', 'Merge with local configs instead of replacing')
476
+ .action(async (options) => {
477
+ try {
478
+ const localStore = await loadDeployConfigs();
479
+ const alias = options.alias ?? localStore.vaultAlias ?? 'deploy/configs';
480
+ ctx.output.info(`Pulling configs from vault: ${alias}...`);
481
+ const response = await ctx.client.get(`/v1/secrets/by-alias/${encodeURIComponent(alias)}/value`);
482
+ const vaultData = JSON.parse(response.value);
483
+ const vaultConfigs = vaultData.configs;
484
+ const configCount = Object.keys(vaultConfigs).length;
485
+ if (options.merge) {
486
+ // Merge: vault configs override local on conflict
487
+ const newStore = {
488
+ configs: { ...localStore.configs, ...vaultConfigs },
489
+ vaultEnabled: true,
490
+ vaultAlias: alias,
491
+ };
492
+ await saveDeployConfigs(newStore);
493
+ const merged = Object.keys(newStore.configs).length;
494
+ ctx.output.success(`Merged ${configCount} config(s) from vault (${merged} total)`);
495
+ }
496
+ else {
497
+ // Replace local configs
498
+ const newStore = {
499
+ configs: vaultConfigs,
500
+ vaultEnabled: true,
501
+ vaultAlias: alias,
502
+ };
503
+ await saveDeployConfigs(newStore);
504
+ ctx.output.success(`Pulled ${configCount} config(s) from vault`);
505
+ }
506
+ ctx.output.info(`Last pushed: ${vaultData.pushedAt} from ${vaultData.pushedFrom}`);
507
+ }
508
+ catch (err) {
509
+ if (String(err).includes('404') || String(err).includes('not found')) {
510
+ ctx.output.error('No configs found in vault');
511
+ ctx.output.info('Push configs first with: znvault deploy config push');
512
+ }
513
+ else {
514
+ ctx.output.error(`Failed to pull: ${err instanceof Error ? err.message : String(err)}`);
515
+ }
516
+ process.exit(1);
517
+ }
518
+ });
519
+ // deploy config sync - Show sync status
520
+ configCmd
521
+ .command('sync')
522
+ .description('Show vault sync status')
523
+ .action(async () => {
524
+ try {
525
+ const store = await loadDeployConfigs();
526
+ console.log('\nVault Sync Status:\n');
527
+ if (!store.vaultEnabled) {
528
+ console.log(' Status: Local only (not synced to vault)');
529
+ console.log(' Configs: ' + Object.keys(store.configs).length);
530
+ console.log('\n To enable vault sync: znvault deploy config push');
531
+ }
532
+ else {
533
+ console.log(' Status: Vault sync enabled');
534
+ console.log(' Alias: ' + (store.vaultAlias ?? 'deploy/configs'));
535
+ console.log(' Configs: ' + Object.keys(store.configs).length);
536
+ console.log('\n Push changes: znvault deploy config push');
537
+ console.log(' Pull updates: znvault deploy config pull');
538
+ }
539
+ console.log();
540
+ }
541
+ catch (err) {
542
+ ctx.output.error(`Failed to get sync status: ${err instanceof Error ? err.message : String(err)}`);
543
+ process.exit(1);
544
+ }
545
+ });
546
+ // deploy config set <name> <key> <value>
547
+ configCmd
548
+ .command('set <name> <key> <value>')
549
+ .description('Set a configuration value (war, port, parallel, description)')
550
+ .action(async (name, key, value) => {
551
+ try {
552
+ const store = await loadDeployConfigs();
553
+ const config = store.configs[name];
554
+ if (!config) {
555
+ ctx.output.error(`Config '${name}' not found`);
556
+ process.exit(1);
557
+ }
558
+ switch (key.toLowerCase()) {
559
+ case 'war':
560
+ case 'warpath':
561
+ config.warPath = value;
562
+ break;
563
+ case 'port':
564
+ config.port = parseInt(value, 10);
565
+ if (isNaN(config.port)) {
566
+ ctx.output.error('Port must be a number');
567
+ process.exit(1);
568
+ }
569
+ break;
570
+ case 'parallel':
571
+ config.parallel = value.toLowerCase() === 'true' || value === '1';
572
+ break;
573
+ case 'description':
574
+ case 'desc':
575
+ config.description = value;
576
+ break;
577
+ default:
578
+ ctx.output.error(`Unknown config key: ${key}`);
579
+ ctx.output.info('Valid keys: war, port, parallel, description');
580
+ process.exit(1);
581
+ }
582
+ await saveDeployConfigs(store);
583
+ ctx.output.success(`Set ${key} = ${value}`);
584
+ }
585
+ catch (err) {
586
+ ctx.output.error(`Failed to set config: ${err instanceof Error ? err.message : String(err)}`);
587
+ process.exit(1);
588
+ }
589
+ });
590
+ // ========================================================================
591
+ // deploy war <file> - Original single-host deployment
592
+ // ========================================================================
22
593
  deploy
23
594
  .command('war <warFile>')
24
- .description('Deploy WAR file using diff transfer')
595
+ .description('Deploy WAR file using diff transfer (single host)')
25
596
  .option('-t, --target <host>', 'Target server URL (default: from profile)')
26
597
  .option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
27
598
  .option('-f, --force', 'Force full deployment (no diff)')
@@ -121,51 +692,108 @@ export function createPayaraCLIPlugin() {
121
692
  process.exit(1);
122
693
  }
123
694
  });
695
+ // ========================================================================
124
696
  // deploy restart
697
+ // ========================================================================
125
698
  deploy
126
- .command('restart')
127
- .description('Restart Payara on remote server')
128
- .option('-t, --target <host>', 'Target server URL')
699
+ .command('restart [configName]')
700
+ .description('Restart Payara on remote server(s)')
701
+ .option('-t, --target <host>', 'Target server URL (single host mode)')
129
702
  .option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
130
- .action(async (options) => {
703
+ .action(async (configName, options) => {
131
704
  try {
132
- const target = options.target ?? ctx.getConfig().url;
133
- const baseUrl = target.replace(/\/$/, '');
134
- const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
135
- ctx.output.info('Restarting Payara...');
136
- await ctx.client.post(`${pluginUrl}/restart`, {});
137
- ctx.output.success('Payara restarted');
705
+ if (configName) {
706
+ // Multi-host restart using config
707
+ const store = await loadDeployConfigs();
708
+ const config = store.configs[configName];
709
+ if (!config) {
710
+ ctx.output.error(`Config '${configName}' not found`);
711
+ process.exit(1);
712
+ }
713
+ ctx.output.info(`Restarting Payara on ${config.hosts.length} host(s)...`);
714
+ for (const host of config.hosts) {
715
+ const baseUrl = host.startsWith('http') ? host : `https://${host}`;
716
+ const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
717
+ try {
718
+ await ctx.client.post(`${pluginUrl}/restart`, {});
719
+ ctx.output.success(` ✓ ${host} restarted`);
720
+ }
721
+ catch (err) {
722
+ ctx.output.error(` ✗ ${host}: ${err instanceof Error ? err.message : String(err)}`);
723
+ }
724
+ }
725
+ }
726
+ else {
727
+ // Single host restart
728
+ const target = options.target ?? ctx.getConfig().url;
729
+ const baseUrl = target.replace(/\/$/, '');
730
+ const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
731
+ ctx.output.info('Restarting Payara...');
732
+ await ctx.client.post(`${pluginUrl}/restart`, {});
733
+ ctx.output.success('Payara restarted');
734
+ }
138
735
  }
139
736
  catch (err) {
140
737
  ctx.output.error(`Restart failed: ${err instanceof Error ? err.message : String(err)}`);
141
738
  process.exit(1);
142
739
  }
143
740
  });
741
+ // ========================================================================
144
742
  // deploy status
743
+ // ========================================================================
145
744
  deploy
146
- .command('status')
147
- .description('Get Payara status from remote server')
148
- .option('-t, --target <host>', 'Target server URL')
745
+ .command('status [configName]')
746
+ .description('Get Payara status from remote server(s)')
747
+ .option('-t, --target <host>', 'Target server URL (single host mode)')
149
748
  .option('-p, --port <port>', 'Agent health port (default: 9100)', '9100')
150
- .action(async (options) => {
749
+ .action(async (configName, options) => {
151
750
  try {
152
- const target = options.target ?? ctx.getConfig().url;
153
- const baseUrl = target.replace(/\/$/, '');
154
- const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
155
- const status = await ctx.client.get(`${pluginUrl}/status`);
156
- ctx.output.keyValue({
157
- 'Domain': status.domain,
158
- 'Running': status.running,
159
- 'Healthy': status.healthy,
160
- 'PID': status.pid ?? 'N/A',
161
- });
751
+ if (configName) {
752
+ // Multi-host status using config
753
+ const store = await loadDeployConfigs();
754
+ const config = store.configs[configName];
755
+ if (!config) {
756
+ ctx.output.error(`Config '${configName}' not found`);
757
+ process.exit(1);
758
+ }
759
+ console.log(`\nStatus for ${configName}:\n`);
760
+ for (const host of config.hosts) {
761
+ const baseUrl = host.startsWith('http') ? host : `https://${host}`;
762
+ const pluginUrl = `${baseUrl}:${config.port}/plugins/payara`;
763
+ try {
764
+ const status = await ctx.client.get(`${pluginUrl}/status`);
765
+ const icon = status.healthy ? '✓' : status.running ? '!' : '✗';
766
+ const state = status.healthy ? 'healthy' : status.running ? 'degraded' : 'down';
767
+ console.log(` ${icon} ${host}: ${state} (${status.domain})`);
768
+ }
769
+ catch (err) {
770
+ console.log(` ✗ ${host}: unreachable`);
771
+ }
772
+ }
773
+ console.log();
774
+ }
775
+ else {
776
+ // Single host status
777
+ const target = options.target ?? ctx.getConfig().url;
778
+ const baseUrl = target.replace(/\/$/, '');
779
+ const pluginUrl = `${baseUrl}:${options.port}/plugins/payara`;
780
+ const status = await ctx.client.get(`${pluginUrl}/status`);
781
+ ctx.output.keyValue({
782
+ 'Domain': status.domain,
783
+ 'Running': status.running,
784
+ 'Healthy': status.healthy,
785
+ 'PID': status.pid ?? 'N/A',
786
+ });
787
+ }
162
788
  }
163
789
  catch (err) {
164
790
  ctx.output.error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
165
791
  process.exit(1);
166
792
  }
167
793
  });
794
+ // ========================================================================
168
795
  // deploy applications
796
+ // ========================================================================
169
797
  deploy
170
798
  .command('applications')
171
799
  .alias('apps')