@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/portlama-agent",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Mac agent for Portlama — installs Chisel tunnel client and manages launchd agent",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
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));
@@ -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
@@ -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
- }