@lamalibre/portlama-agent 1.0.6 → 1.0.7
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/package.json +1 -1
- package/src/index.js +0 -36
- package/src/lib/panel-api.js +0 -205
- package/src/commands/cp.js +0 -192
- package/src/commands/shell-log.js +0 -183
- package/src/commands/shell-server.js +0 -498
- package/src/commands/shell.js +0 -171
- package/src/lib/portlama-shell.sh +0 -177
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -24,10 +24,6 @@ ${b('COMMANDS')}
|
|
|
24
24
|
${c('logs')} Stream Chisel log output (tail -f)
|
|
25
25
|
${c('sites')} List, create, or delete static sites
|
|
26
26
|
${c('deploy')} Deploy a local directory to a static site
|
|
27
|
-
${c('shell-server')} Run the shell gateway (background service)
|
|
28
|
-
${c('shell')} Connect to a remote agent shell
|
|
29
|
-
${c('cp')} Copy files to/from a remote agent
|
|
30
|
-
${c('shell-log')} List or download shell session recordings
|
|
31
27
|
${c('plugin')} Manage agent plugins (install, uninstall, update, status)
|
|
32
28
|
|
|
33
29
|
${b('EXAMPLES')}
|
|
@@ -50,18 +46,6 @@ ${b('EXAMPLES')}
|
|
|
50
46
|
${d('# Deploy local build to a site')}
|
|
51
47
|
${c('portlama-agent deploy blog ./dist')}
|
|
52
48
|
|
|
53
|
-
${d('# Start the shell gateway (runs as background service)')}
|
|
54
|
-
${c('portlama-agent shell-server')}
|
|
55
|
-
|
|
56
|
-
${d('# Connect to a remote agent shell')}
|
|
57
|
-
${c('portlama-agent shell myagent')}
|
|
58
|
-
|
|
59
|
-
${d('# Copy a file from a remote agent')}
|
|
60
|
-
${c('portlama-agent cp myagent:/var/log/app.log ./app.log')}
|
|
61
|
-
|
|
62
|
-
${d('# List shell session recordings')}
|
|
63
|
-
${c('portlama-agent shell-log myagent')}
|
|
64
|
-
|
|
65
49
|
${d('# Manage plugins')}
|
|
66
50
|
${c('portlama-agent plugin status')}
|
|
67
51
|
${c('portlama-agent plugin install @lamalibre/shell-agent')}
|
|
@@ -123,26 +107,6 @@ export async function main() {
|
|
|
123
107
|
await runDeploy(args.slice(1));
|
|
124
108
|
break;
|
|
125
109
|
}
|
|
126
|
-
case 'shell-server': {
|
|
127
|
-
const { runShellServer } = await import('./commands/shell-server.js');
|
|
128
|
-
await runShellServer();
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
case 'shell': {
|
|
132
|
-
const { runShell } = await import('./commands/shell.js');
|
|
133
|
-
await runShell(args.slice(1));
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case 'cp': {
|
|
137
|
-
const { runCp } = await import('./commands/cp.js');
|
|
138
|
-
await runCp(args.slice(1));
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
case 'shell-log': {
|
|
142
|
-
const { runShellLog } = await import('./commands/shell-log.js');
|
|
143
|
-
await runShellLog(args.slice(1));
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
110
|
case 'plugin': {
|
|
147
111
|
const { runPlugin } = await import('./commands/plugin.js');
|
|
148
112
|
await runPlugin(args.slice(1));
|
package/src/lib/panel-api.js
CHANGED
|
@@ -406,211 +406,6 @@ export async function uploadSiteFiles(
|
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
-
/**
|
|
410
|
-
* Fetch shell configuration for this agent.
|
|
411
|
-
* @param {string|object} panelUrlOrConfig
|
|
412
|
-
* @param {string} [p12Path]
|
|
413
|
-
* @param {string} [p12Password]
|
|
414
|
-
* @returns {Promise<{ enabled: boolean, label: string, blocklist?: string[], timeWindow?: object }>}
|
|
415
|
-
*/
|
|
416
|
-
export async function fetchShellConfig(panelUrlOrConfig, p12Path, p12Password) {
|
|
417
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
418
|
-
const url = `${panelUrl}/api/shell/config`;
|
|
419
|
-
try {
|
|
420
|
-
const { stdout } = typeof panelUrlOrConfig === 'object'
|
|
421
|
-
? await curlAuthenticated(panelUrlOrConfig, [url])
|
|
422
|
-
: await curlWithConfig(p12Path, p12Password, [url]);
|
|
423
|
-
return JSON.parse(stdout);
|
|
424
|
-
} catch (err) {
|
|
425
|
-
throw new Error(
|
|
426
|
-
`Failed to fetch shell config from panel. ` + `Details: ${err.stderr || err.message}`,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Fetch the agent's own status from the panel.
|
|
433
|
-
* The server derives the agent label from the mTLS client certificate CN.
|
|
434
|
-
* @param {string|object} panelUrlOrConfig
|
|
435
|
-
* @param {string} [p12Path]
|
|
436
|
-
* @param {string} [p12Password]
|
|
437
|
-
* @returns {Promise<{ label: string }>}
|
|
438
|
-
*/
|
|
439
|
-
export async function fetchAgentStatus(panelUrlOrConfig, p12Path, p12Password) {
|
|
440
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
441
|
-
const url = `${panelUrl}/api/shell/agent-status`;
|
|
442
|
-
try {
|
|
443
|
-
const { stdout } = typeof panelUrlOrConfig === 'object'
|
|
444
|
-
? await curlAuthenticated(panelUrlOrConfig, [url])
|
|
445
|
-
: await curlWithConfig(p12Path, p12Password, [url]);
|
|
446
|
-
return JSON.parse(stdout);
|
|
447
|
-
} catch (err) {
|
|
448
|
-
throw new Error(
|
|
449
|
-
`Failed to fetch agent status from panel. ` + `Details: ${err.stderr || err.message}`,
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Fetch shell session list from the panel.
|
|
456
|
-
* @param {string|object} panelUrlOrConfig
|
|
457
|
-
* @param {string} [p12Path]
|
|
458
|
-
* @param {string} [p12Password]
|
|
459
|
-
* @returns {Promise<{ sessions: Array }>}
|
|
460
|
-
*/
|
|
461
|
-
export async function fetchShellSessions(panelUrlOrConfig, p12Path, p12Password) {
|
|
462
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
463
|
-
const url = `${panelUrl}/api/shell/sessions`;
|
|
464
|
-
try {
|
|
465
|
-
const { stdout } = typeof panelUrlOrConfig === 'object'
|
|
466
|
-
? await curlAuthenticated(panelUrlOrConfig, [url])
|
|
467
|
-
: await curlWithConfig(p12Path, p12Password, [url]);
|
|
468
|
-
return JSON.parse(stdout);
|
|
469
|
-
} catch (err) {
|
|
470
|
-
throw new Error(
|
|
471
|
-
`Failed to fetch shell sessions from panel. ` + `Details: ${err.stderr || err.message}`,
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Download a shell session recording from the panel.
|
|
478
|
-
* @param {string|object} panelUrlOrConfig
|
|
479
|
-
* @param {string} [p12Path]
|
|
480
|
-
* @param {string} [p12Password]
|
|
481
|
-
* @param {string} agentLabel - The agent label that owns the recording
|
|
482
|
-
* @param {string} sessionId
|
|
483
|
-
* @param {string} outputPath - Local path to write the recording to
|
|
484
|
-
* @returns {Promise<void>}
|
|
485
|
-
*/
|
|
486
|
-
export async function downloadShellRecording(
|
|
487
|
-
panelUrlOrConfig,
|
|
488
|
-
p12Path,
|
|
489
|
-
p12Password,
|
|
490
|
-
agentLabel,
|
|
491
|
-
sessionId,
|
|
492
|
-
outputPath,
|
|
493
|
-
) {
|
|
494
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
495
|
-
let actualAgentLabel, actualSessionId, actualOutputPath;
|
|
496
|
-
if (typeof panelUrlOrConfig === 'object') {
|
|
497
|
-
actualAgentLabel = p12Path;
|
|
498
|
-
actualSessionId = p12Password;
|
|
499
|
-
actualOutputPath = agentLabel;
|
|
500
|
-
} else {
|
|
501
|
-
actualAgentLabel = agentLabel;
|
|
502
|
-
actualSessionId = sessionId;
|
|
503
|
-
actualOutputPath = outputPath;
|
|
504
|
-
}
|
|
505
|
-
const url = `${panelUrl}/api/shell/recordings/${encodeURIComponent(actualAgentLabel)}/${encodeURIComponent(actualSessionId)}`;
|
|
506
|
-
const curlArgs = ['-o', actualOutputPath, url];
|
|
507
|
-
try {
|
|
508
|
-
if (typeof panelUrlOrConfig === 'object') {
|
|
509
|
-
await curlAuthenticated(panelUrlOrConfig, curlArgs);
|
|
510
|
-
} else {
|
|
511
|
-
await curlWithConfig(p12Path, p12Password, curlArgs);
|
|
512
|
-
}
|
|
513
|
-
} catch (err) {
|
|
514
|
-
throw new Error(
|
|
515
|
-
`Failed to download shell recording from panel. ` + `Details: ${err.stderr || err.message}`,
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Download a file from a remote agent via the panel relay.
|
|
522
|
-
* @param {string|object} panelUrlOrConfig
|
|
523
|
-
* @param {string} [p12Path]
|
|
524
|
-
* @param {string} [p12Password]
|
|
525
|
-
* @param {string} agentLabel
|
|
526
|
-
* @param {string} remotePath - Absolute path on the remote agent
|
|
527
|
-
* @param {string} outputPath - Local path to write the file to
|
|
528
|
-
* @returns {Promise<void>}
|
|
529
|
-
*/
|
|
530
|
-
export async function downloadRemoteFile(
|
|
531
|
-
panelUrlOrConfig,
|
|
532
|
-
p12Path,
|
|
533
|
-
p12Password,
|
|
534
|
-
agentLabel,
|
|
535
|
-
remotePath,
|
|
536
|
-
outputPath,
|
|
537
|
-
) {
|
|
538
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
539
|
-
let actualAgentLabel, actualRemotePath, actualOutputPath;
|
|
540
|
-
if (typeof panelUrlOrConfig === 'object') {
|
|
541
|
-
actualAgentLabel = p12Path;
|
|
542
|
-
actualRemotePath = p12Password;
|
|
543
|
-
actualOutputPath = agentLabel;
|
|
544
|
-
} else {
|
|
545
|
-
actualAgentLabel = agentLabel;
|
|
546
|
-
actualRemotePath = remotePath;
|
|
547
|
-
actualOutputPath = outputPath;
|
|
548
|
-
}
|
|
549
|
-
const url = `${panelUrl}/api/shell/file/${encodeURIComponent(actualAgentLabel)}?path=${encodeURIComponent(actualRemotePath)}`;
|
|
550
|
-
const curlArgs = ['-o', actualOutputPath, url];
|
|
551
|
-
try {
|
|
552
|
-
if (typeof panelUrlOrConfig === 'object') {
|
|
553
|
-
await curlAuthenticated(panelUrlOrConfig, curlArgs);
|
|
554
|
-
} else {
|
|
555
|
-
await curlWithConfig(p12Path, p12Password, curlArgs);
|
|
556
|
-
}
|
|
557
|
-
} catch (err) {
|
|
558
|
-
throw new Error(
|
|
559
|
-
`Failed to download file from agent ${actualAgentLabel}. ` +
|
|
560
|
-
`Details: ${err.stderr || err.message}`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Upload a local file to a remote agent via the panel relay.
|
|
567
|
-
* @param {string|object} panelUrlOrConfig
|
|
568
|
-
* @param {string} [p12Path]
|
|
569
|
-
* @param {string} [p12Password]
|
|
570
|
-
* @param {string} agentLabel
|
|
571
|
-
* @param {string} remotePath - Absolute path on the remote agent
|
|
572
|
-
* @param {string} localFilePath - Local file to upload
|
|
573
|
-
* @returns {Promise<object>}
|
|
574
|
-
*/
|
|
575
|
-
export async function uploadRemoteFile(
|
|
576
|
-
panelUrlOrConfig,
|
|
577
|
-
p12Path,
|
|
578
|
-
p12Password,
|
|
579
|
-
agentLabel,
|
|
580
|
-
remotePath,
|
|
581
|
-
localFilePath,
|
|
582
|
-
) {
|
|
583
|
-
const panelUrl = resolvePanelUrl(panelUrlOrConfig);
|
|
584
|
-
let actualAgentLabel, actualRemotePath, actualLocalFilePath;
|
|
585
|
-
if (typeof panelUrlOrConfig === 'object') {
|
|
586
|
-
actualAgentLabel = p12Path;
|
|
587
|
-
actualRemotePath = p12Password;
|
|
588
|
-
actualLocalFilePath = agentLabel;
|
|
589
|
-
} else {
|
|
590
|
-
actualAgentLabel = agentLabel;
|
|
591
|
-
actualRemotePath = remotePath;
|
|
592
|
-
actualLocalFilePath = localFilePath;
|
|
593
|
-
}
|
|
594
|
-
const url = `${panelUrl}/api/shell/file/${encodeURIComponent(actualAgentLabel)}?path=${encodeURIComponent(actualRemotePath)}`;
|
|
595
|
-
const curlArgs = [
|
|
596
|
-
'-X',
|
|
597
|
-
'POST',
|
|
598
|
-
'-F',
|
|
599
|
-
`file=@${actualLocalFilePath}`,
|
|
600
|
-
url,
|
|
601
|
-
];
|
|
602
|
-
try {
|
|
603
|
-
const { stdout } = typeof panelUrlOrConfig === 'object'
|
|
604
|
-
? await curlAuthenticated(panelUrlOrConfig, curlArgs)
|
|
605
|
-
: await curlWithConfig(p12Path, p12Password, curlArgs);
|
|
606
|
-
return JSON.parse(stdout);
|
|
607
|
-
} catch (err) {
|
|
608
|
-
throw new Error(
|
|
609
|
-
`Failed to upload file to agent ${actualAgentLabel}. ` + `Details: ${err.stderr || err.message}`,
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
409
|
/**
|
|
615
410
|
* Delete a file from a static site.
|
|
616
411
|
* @param {string|object} panelUrlOrConfig
|
package/src/commands/cp.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { assertMacOS } from '../lib/platform.js';
|
|
6
|
-
import { requireAgentConfig } from '../lib/config.js';
|
|
7
|
-
import { downloadRemoteFile, uploadRemoteFile } from '../lib/panel-api.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse a cp argument to determine if it's a remote path (agent-label:/path)
|
|
11
|
-
* or a local path.
|
|
12
|
-
* @param {string} arg
|
|
13
|
-
* @returns {{ isRemote: boolean, agentLabel?: string, path: string }}
|
|
14
|
-
*/
|
|
15
|
-
function parseLocation(arg) {
|
|
16
|
-
// Match pattern: label:/path (label must be lowercase alphanumeric with hyphens, matching server validation)
|
|
17
|
-
const match = arg.match(/^([a-z0-9-]+):(.+)$/);
|
|
18
|
-
if (match) {
|
|
19
|
-
return { isRemote: true, agentLabel: match[1], path: match[2] };
|
|
20
|
-
}
|
|
21
|
-
return { isRemote: false, path: arg };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Run the file copy command.
|
|
26
|
-
* Supports download (remote → local) and upload (local → remote).
|
|
27
|
-
* @param {string[]} args
|
|
28
|
-
*/
|
|
29
|
-
export async function runCp(args) {
|
|
30
|
-
assertMacOS();
|
|
31
|
-
const config = await requireAgentConfig();
|
|
32
|
-
|
|
33
|
-
if (args.length < 2) {
|
|
34
|
-
printUsage();
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const source = parseLocation(args[0]);
|
|
39
|
-
const dest = parseLocation(args[1]);
|
|
40
|
-
|
|
41
|
-
// Validate: exactly one side must be remote
|
|
42
|
-
if (source.isRemote && dest.isRemote) {
|
|
43
|
-
console.error(
|
|
44
|
-
chalk.red('\n Cannot copy between two remote agents. One side must be a local path.\n'),
|
|
45
|
-
);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!source.isRemote && !dest.isRemote) {
|
|
50
|
-
console.error(
|
|
51
|
-
chalk.red(
|
|
52
|
-
'\n Both paths are local. Use the system cp command for local copies.\n' +
|
|
53
|
-
' For remote transfers, prefix with agent-label: (e.g. myagent:/path/to/file)\n',
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (source.isRemote) {
|
|
60
|
-
await runDownload(config, source, dest);
|
|
61
|
-
} else {
|
|
62
|
-
await runUpload(config, source, dest);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Download a file from a remote agent.
|
|
68
|
-
* @param {object} config
|
|
69
|
-
* @param {{ agentLabel: string, path: string }} source
|
|
70
|
-
* @param {{ path: string }} dest
|
|
71
|
-
*/
|
|
72
|
-
async function runDownload(config, source, dest) {
|
|
73
|
-
const agentLabel = source.agentLabel;
|
|
74
|
-
const remotePath = source.path;
|
|
75
|
-
let localPath = path.resolve(dest.path);
|
|
76
|
-
|
|
77
|
-
// If dest is a directory, use the remote filename
|
|
78
|
-
try {
|
|
79
|
-
const destStat = await stat(localPath);
|
|
80
|
-
if (destStat.isDirectory()) {
|
|
81
|
-
const remoteBasename = path.basename(remotePath);
|
|
82
|
-
localPath = path.join(localPath, remoteBasename);
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Path doesn't exist yet — that's fine, we'll create the file
|
|
86
|
-
// Ensure the parent directory exists
|
|
87
|
-
const parentDir = path.dirname(localPath);
|
|
88
|
-
if (!existsSync(parentDir)) {
|
|
89
|
-
console.error(chalk.red(`\n Parent directory does not exist: ${parentDir}\n`));
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log('');
|
|
95
|
-
console.log(chalk.dim(` Downloading ${chalk.bold(agentLabel)}:${remotePath} → ${localPath}`));
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await downloadRemoteFile(
|
|
99
|
-
config,
|
|
100
|
-
agentLabel,
|
|
101
|
-
remotePath,
|
|
102
|
-
localPath,
|
|
103
|
-
);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
console.error(chalk.red(`\n Download failed: ${err.message}\n`));
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
console.log(` ${chalk.green('✓')} Downloaded to ${chalk.cyan(localPath)}`);
|
|
110
|
-
console.log('');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Upload a local file to a remote agent.
|
|
115
|
-
* @param {object} config
|
|
116
|
-
* @param {{ path: string }} source
|
|
117
|
-
* @param {{ agentLabel: string, path: string }} dest
|
|
118
|
-
*/
|
|
119
|
-
async function runUpload(config, source, dest) {
|
|
120
|
-
const localPath = path.resolve(source.path);
|
|
121
|
-
const agentLabel = dest.agentLabel;
|
|
122
|
-
const remotePath = dest.path;
|
|
123
|
-
|
|
124
|
-
// Validate local file exists
|
|
125
|
-
if (!existsSync(localPath)) {
|
|
126
|
-
console.error(chalk.red(`\n Local file not found: ${localPath}\n`));
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let localStat;
|
|
131
|
-
try {
|
|
132
|
-
localStat = await stat(localPath);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error(chalk.red(`\n Cannot read file: ${err.message}\n`));
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!localStat.isFile()) {
|
|
139
|
-
console.error(
|
|
140
|
-
chalk.red('\n Only single file transfers are currently supported.\n') +
|
|
141
|
-
chalk.dim(
|
|
142
|
-
' To transfer a directory, archive it first (e.g. tar -czf archive.tar.gz dir/).\n',
|
|
143
|
-
),
|
|
144
|
-
);
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.log('');
|
|
149
|
-
console.log(chalk.dim(` Uploading ${localPath} → ${chalk.bold(agentLabel)}:${remotePath}`));
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
await uploadRemoteFile(
|
|
153
|
-
config,
|
|
154
|
-
agentLabel,
|
|
155
|
-
remotePath,
|
|
156
|
-
localPath,
|
|
157
|
-
);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
console.error(chalk.red(`\n Upload failed: ${err.message}\n`));
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
console.log(` ${chalk.green('✓')} Uploaded to ${chalk.cyan(`${agentLabel}:${remotePath}`)}`);
|
|
164
|
-
console.log('');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Print usage information.
|
|
169
|
-
*/
|
|
170
|
-
function printUsage() {
|
|
171
|
-
const c = chalk.cyan;
|
|
172
|
-
const d = chalk.dim;
|
|
173
|
-
|
|
174
|
-
console.error(`
|
|
175
|
-
${chalk.bold('Usage:')}
|
|
176
|
-
|
|
177
|
-
${c('portlama-agent cp')} ${d('<source> <destination>')}
|
|
178
|
-
|
|
179
|
-
${chalk.bold('Download from agent:')}
|
|
180
|
-
|
|
181
|
-
${c('portlama-agent cp myagent:/var/log/app.log ./app.log')}
|
|
182
|
-
|
|
183
|
-
${chalk.bold('Upload to agent:')}
|
|
184
|
-
|
|
185
|
-
${c('portlama-agent cp ./config.json myagent:/etc/app/config.json')}
|
|
186
|
-
|
|
187
|
-
${chalk.bold('Notes:')}
|
|
188
|
-
|
|
189
|
-
Remote paths use the format: ${c('agent-label:/absolute/path')}
|
|
190
|
-
Only single file transfers are supported.
|
|
191
|
-
`);
|
|
192
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { assertMacOS } from '../lib/platform.js';
|
|
4
|
-
import { requireAgentConfig } from '../lib/config.js';
|
|
5
|
-
import { fetchShellSessions, downloadShellRecording } from '../lib/panel-api.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Parse simple CLI flags from an array of arguments.
|
|
9
|
-
* @param {string[]} args
|
|
10
|
-
* @returns {{ positional: string[], flags: Record<string, string|boolean> }}
|
|
11
|
-
*/
|
|
12
|
-
function parseFlags(args) {
|
|
13
|
-
const positional = [];
|
|
14
|
-
const flags = {};
|
|
15
|
-
for (let i = 0; i < args.length; i++) {
|
|
16
|
-
if (args[i].startsWith('--')) {
|
|
17
|
-
const key = args[i].slice(2);
|
|
18
|
-
const next = args[i + 1];
|
|
19
|
-
if (next && !next.startsWith('--')) {
|
|
20
|
-
flags[key] = next;
|
|
21
|
-
i++;
|
|
22
|
-
} else {
|
|
23
|
-
flags[key] = true;
|
|
24
|
-
}
|
|
25
|
-
} else {
|
|
26
|
-
positional.push(args[i]);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return { positional, flags };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Format a date string for display.
|
|
34
|
-
* @param {string} isoDate
|
|
35
|
-
* @returns {string}
|
|
36
|
-
*/
|
|
37
|
-
function formatDate(isoDate) {
|
|
38
|
-
try {
|
|
39
|
-
const d = new Date(isoDate);
|
|
40
|
-
return d.toLocaleString();
|
|
41
|
-
} catch {
|
|
42
|
-
return isoDate || 'unknown';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Format duration in seconds to human-readable.
|
|
48
|
-
* @param {number} seconds
|
|
49
|
-
* @returns {string}
|
|
50
|
-
*/
|
|
51
|
-
function formatDuration(seconds) {
|
|
52
|
-
if (seconds == null) return 'unknown';
|
|
53
|
-
if (seconds < 60) return `${seconds}s`;
|
|
54
|
-
const mins = Math.floor(seconds / 60);
|
|
55
|
-
const secs = seconds % 60;
|
|
56
|
-
if (mins < 60) return `${mins}m ${secs}s`;
|
|
57
|
-
const hours = Math.floor(mins / 60);
|
|
58
|
-
const remainMins = mins % 60;
|
|
59
|
-
return `${hours}h ${remainMins}m`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* List shell sessions, optionally filtered by agent label.
|
|
64
|
-
* @param {object} config
|
|
65
|
-
* @param {string} [agentLabel]
|
|
66
|
-
*/
|
|
67
|
-
async function runList(config, agentLabel) {
|
|
68
|
-
const b = chalk.bold;
|
|
69
|
-
const c = chalk.cyan;
|
|
70
|
-
const d = chalk.dim;
|
|
71
|
-
const y = chalk.yellow;
|
|
72
|
-
|
|
73
|
-
console.log('');
|
|
74
|
-
console.log(b(' Shell Sessions'));
|
|
75
|
-
console.log(d(' ─'.repeat(28)));
|
|
76
|
-
|
|
77
|
-
let sessions;
|
|
78
|
-
try {
|
|
79
|
-
const data = await fetchShellSessions(config);
|
|
80
|
-
sessions = data.sessions || [];
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.log(` ${y(`Could not fetch sessions: ${err.message}`)}`);
|
|
83
|
-
console.log('');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Filter by agent label if specified
|
|
88
|
-
if (agentLabel) {
|
|
89
|
-
sessions = sessions.filter((s) => s.agentLabel === agentLabel);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (sessions.length === 0) {
|
|
93
|
-
const suffix = agentLabel ? ` for agent ${chalk.bold(agentLabel)}` : '';
|
|
94
|
-
console.log(` ${d(`No sessions found${suffix}.`)}`);
|
|
95
|
-
console.log('');
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Sort by start time, newest first
|
|
100
|
-
sessions.sort((a, b2) => {
|
|
101
|
-
const da = new Date(a.startedAt || 0);
|
|
102
|
-
const db = new Date(b2.startedAt || 0);
|
|
103
|
-
return db - da;
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
for (const session of sessions) {
|
|
107
|
-
const statusLabel =
|
|
108
|
-
session.status === 'active'
|
|
109
|
-
? chalk.green('Active')
|
|
110
|
-
: session.status === 'ended'
|
|
111
|
-
? d('Ended')
|
|
112
|
-
: d(session.status || 'unknown');
|
|
113
|
-
|
|
114
|
-
console.log(` ${c('•')} ${b(session.id)}`);
|
|
115
|
-
console.log(` Agent: ${session.agentLabel || d('unknown')}`);
|
|
116
|
-
console.log(` Status: ${statusLabel}`);
|
|
117
|
-
console.log(` Started: ${formatDate(session.startedAt)}`);
|
|
118
|
-
if (session.endedAt) {
|
|
119
|
-
console.log(` Ended: ${formatDate(session.endedAt)}`);
|
|
120
|
-
}
|
|
121
|
-
if (session.duration != null) {
|
|
122
|
-
console.log(` Duration: ${formatDuration(session.duration)}`);
|
|
123
|
-
}
|
|
124
|
-
if (session.commandCount != null) {
|
|
125
|
-
console.log(` Commands: ${session.commandCount}`);
|
|
126
|
-
}
|
|
127
|
-
console.log('');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Download a session recording.
|
|
133
|
-
* @param {object} config
|
|
134
|
-
* @param {string} agentLabel
|
|
135
|
-
* @param {string} sessionId
|
|
136
|
-
*/
|
|
137
|
-
async function runDownload(config, agentLabel, sessionId) {
|
|
138
|
-
const outputPath = path.resolve(`${sessionId}.log`);
|
|
139
|
-
|
|
140
|
-
console.log('');
|
|
141
|
-
console.log(chalk.dim(` Downloading recording for session ${chalk.bold(sessionId)}...`));
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
await downloadShellRecording(
|
|
145
|
-
config,
|
|
146
|
-
agentLabel,
|
|
147
|
-
sessionId,
|
|
148
|
-
outputPath,
|
|
149
|
-
);
|
|
150
|
-
} catch (err) {
|
|
151
|
-
console.error(chalk.red(`\n Download failed: ${err.message}\n`));
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
console.log(` ${chalk.green('✓')} Recording saved to ${chalk.cyan(outputPath)}`);
|
|
156
|
-
console.log('');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Shell log command: list or download session recordings.
|
|
161
|
-
* @param {string[]} args
|
|
162
|
-
*/
|
|
163
|
-
export async function runShellLog(args) {
|
|
164
|
-
assertMacOS();
|
|
165
|
-
const config = await requireAgentConfig();
|
|
166
|
-
|
|
167
|
-
const { positional, flags } = parseFlags(args);
|
|
168
|
-
const agentLabel = positional[0];
|
|
169
|
-
|
|
170
|
-
if (flags.download) {
|
|
171
|
-
const sessionId = typeof flags.download === 'string' ? flags.download : null;
|
|
172
|
-
if (!sessionId || !agentLabel) {
|
|
173
|
-
console.error(
|
|
174
|
-
`\n Usage: ${chalk.cyan('portlama-agent shell-log <agent-label> --download <session-id>')}\n`,
|
|
175
|
-
);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
return runDownload(config, agentLabel, sessionId);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Default to --list behavior
|
|
182
|
-
return runList(config, agentLabel);
|
|
183
|
-
}
|