@lamalibre/portlama-agent 1.0.5 → 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/commands/deploy.js +5 -25
- package/src/commands/setup.js +279 -3
- package/src/commands/sites.js +4 -4
- package/src/commands/status.js +1 -1
- package/src/commands/update.js +2 -6
- package/src/index.js +0 -36
- package/src/lib/config.js +18 -1
- package/src/lib/keychain.js +208 -0
- package/src/lib/panel-api.js +232 -204
- package/src/commands/cp.js +0 -196
- package/src/commands/shell-log.js +0 -185
- package/src/commands/shell-server.js +0 -491
- package/src/commands/shell.js +0 -164
- package/src/lib/portlama-shell.sh +0 -177
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function runDeploy(args) {
|
|
|
71
71
|
|
|
72
72
|
let data;
|
|
73
73
|
try {
|
|
74
|
-
data = await fetchSites(config
|
|
74
|
+
data = await fetchSites(config);
|
|
75
75
|
} catch (err) {
|
|
76
76
|
console.error(`\n ${chalk.red(`Failed to connect to panel: ${err.message}`)}\n`);
|
|
77
77
|
process.exit(1);
|
|
@@ -161,22 +161,10 @@ export async function runDeploy(args) {
|
|
|
161
161
|
{
|
|
162
162
|
title: 'Clearing remote files',
|
|
163
163
|
task: async (_ctx, task) => {
|
|
164
|
-
const { files: remoteFiles } = await fetchSiteFiles(
|
|
165
|
-
config.panelUrl,
|
|
166
|
-
config.p12Path,
|
|
167
|
-
config.p12Password,
|
|
168
|
-
siteId,
|
|
169
|
-
'.',
|
|
170
|
-
);
|
|
164
|
+
const { files: remoteFiles } = await fetchSiteFiles(config, siteId, '.');
|
|
171
165
|
for (const f of remoteFiles) {
|
|
172
166
|
task.output = `Removing ${f.name}`;
|
|
173
|
-
await deleteSiteFile(
|
|
174
|
-
config.panelUrl,
|
|
175
|
-
config.p12Path,
|
|
176
|
-
config.p12Password,
|
|
177
|
-
siteId,
|
|
178
|
-
f.name,
|
|
179
|
-
);
|
|
167
|
+
await deleteSiteFile(config, siteId, f.name);
|
|
180
168
|
}
|
|
181
169
|
},
|
|
182
170
|
rendererOptions: { persistentOutput: false },
|
|
@@ -200,9 +188,7 @@ export async function runDeploy(args) {
|
|
|
200
188
|
const batch = groupFiles.slice(i, i + 10);
|
|
201
189
|
const uploadDir = dir === '.' ? '.' : dir;
|
|
202
190
|
await uploadSiteFiles(
|
|
203
|
-
config
|
|
204
|
-
config.p12Path,
|
|
205
|
-
config.p12Password,
|
|
191
|
+
config,
|
|
206
192
|
siteId,
|
|
207
193
|
uploadDir,
|
|
208
194
|
batch.map((f) => f.absolutePath),
|
|
@@ -220,13 +206,7 @@ export async function runDeploy(args) {
|
|
|
220
206
|
// Count remote files recursively to match local recursive scan
|
|
221
207
|
let remoteCount = 0;
|
|
222
208
|
const countRemote = async (dirPath) => {
|
|
223
|
-
const { files: entries } = await fetchSiteFiles(
|
|
224
|
-
config.panelUrl,
|
|
225
|
-
config.p12Path,
|
|
226
|
-
config.p12Password,
|
|
227
|
-
siteId,
|
|
228
|
-
dirPath,
|
|
229
|
-
);
|
|
209
|
+
const { files: entries } = await fetchSiteFiles(config, siteId, dirPath);
|
|
230
210
|
for (const entry of entries) {
|
|
231
211
|
if (entry.type === 'directory') {
|
|
232
212
|
const subPath = dirPath === '.' ? entry.name : `${dirPath}/${entry.name}`;
|
package/src/commands/setup.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
+
import path from 'node:path';
|
|
5
6
|
import { Listr } from 'listr2';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
|
-
import { assertMacOS, CHISEL_BIN_DIR, LOGS_DIR } from '../lib/platform.js';
|
|
8
|
+
import { assertMacOS, CHISEL_BIN_DIR, LOGS_DIR, AGENT_DIR } from '../lib/platform.js';
|
|
8
9
|
import { loadAgentConfig, saveAgentConfig } from '../lib/config.js';
|
|
9
|
-
import { fetchHealth, fetchPlist, fetchTunnels } from '../lib/panel-api.js';
|
|
10
|
+
import { fetchHealth, fetchPlist, fetchTunnels, curlPostUnauthenticated } from '../lib/panel-api.js';
|
|
10
11
|
import { extractPemFromP12, cleanupPemFiles } from '../lib/ws-helpers.js';
|
|
11
12
|
import { installChisel } from '../lib/chisel.js';
|
|
12
13
|
import { rewritePlist, writePlistFile } from '../lib/plist.js';
|
|
13
14
|
import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/launchctl.js';
|
|
15
|
+
import { generateKeypairAndCSR, importIdentityToKeychain, secureDelete } from '../lib/keychain.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Prompt for user input via readline.
|
|
@@ -34,10 +36,283 @@ function prompt(question, defaultValue) {
|
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Parse --token and --panel-url flags from argv.
|
|
41
|
+
* @returns {{ token?: string, panelUrl?: string }}
|
|
42
|
+
*/
|
|
43
|
+
function parseSetupFlags() {
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
const flags = {};
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
if (args[i] === '--token' && args[i + 1]) {
|
|
48
|
+
flags.token = args[++i];
|
|
49
|
+
} else if (args[i] === '--panel-url' && args[i + 1]) {
|
|
50
|
+
flags.panelUrl = args[++i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return flags;
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
/**
|
|
38
57
|
* Run the interactive setup flow.
|
|
58
|
+
* If --token is provided, uses the hardware-bound enrollment flow.
|
|
39
59
|
*/
|
|
40
60
|
export async function runSetup() {
|
|
61
|
+
const flags = parseSetupFlags();
|
|
62
|
+
|
|
63
|
+
if (flags.token) {
|
|
64
|
+
return runTokenSetup(flags);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return runP12Setup();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hardware-bound enrollment flow using a one-time token.
|
|
72
|
+
* Generates a keypair locally, sends CSR to the panel, imports the signed
|
|
73
|
+
* certificate into macOS Keychain as a non-extractable identity.
|
|
74
|
+
*
|
|
75
|
+
* @param {{ token: string, panelUrl?: string }} flags
|
|
76
|
+
*/
|
|
77
|
+
async function runTokenSetup(flags) {
|
|
78
|
+
// Step 1: Verify macOS
|
|
79
|
+
assertMacOS();
|
|
80
|
+
|
|
81
|
+
// Check for existing config
|
|
82
|
+
const existingConfig = await loadAgentConfig();
|
|
83
|
+
if (existingConfig) {
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.yellow(' An existing agent configuration was found.'));
|
|
86
|
+
console.log(chalk.yellow(' Running setup again will overwrite it.'));
|
|
87
|
+
console.log('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.bold(' Portlama Agent Setup (Hardware-Bound Certificate)'));
|
|
92
|
+
console.log(chalk.dim(' Connect this Mac to your Portlama server using a Keychain-bound certificate.'));
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
let panelUrl = flags.panelUrl;
|
|
96
|
+
if (!panelUrl) {
|
|
97
|
+
panelUrl = await prompt('Panel URL (e.g. https://1.2.3.4:9292)', existingConfig?.panelUrl);
|
|
98
|
+
}
|
|
99
|
+
if (!panelUrl) {
|
|
100
|
+
throw new Error('Panel URL is required. Pass --panel-url <url> or enter interactively.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const normalizedUrl = panelUrl.replace(/\/+$/, '');
|
|
104
|
+
|
|
105
|
+
console.log('');
|
|
106
|
+
|
|
107
|
+
// Context shared across tasks
|
|
108
|
+
const ctx = {
|
|
109
|
+
panelUrl: normalizedUrl,
|
|
110
|
+
token: flags.token,
|
|
111
|
+
agentLabel: null,
|
|
112
|
+
keychainIdentity: null,
|
|
113
|
+
chiselVersion: null,
|
|
114
|
+
plistXml: null,
|
|
115
|
+
domain: null,
|
|
116
|
+
tunnels: [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const tasks = new Listr(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
title: 'Creating directories',
|
|
123
|
+
task: async () => {
|
|
124
|
+
await mkdir(AGENT_DIR, { recursive: true, mode: 0o700 });
|
|
125
|
+
await mkdir(CHISEL_BIN_DIR, { recursive: true });
|
|
126
|
+
await mkdir(LOGS_DIR, { recursive: true });
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
title: 'Generating keypair and CSR',
|
|
131
|
+
task: async (_ctx, task) => {
|
|
132
|
+
// We use a temporary label placeholder — the actual label comes from the token
|
|
133
|
+
// We'll pass 'pending' and fix after enrollment
|
|
134
|
+
ctx._keyData = await generateKeypairAndCSR('pending');
|
|
135
|
+
task.output = 'Keypair generated (4096-bit RSA)';
|
|
136
|
+
},
|
|
137
|
+
rendererOptions: { persistentOutput: true },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: 'Enrolling with panel',
|
|
141
|
+
task: async (_ctx, task) => {
|
|
142
|
+
const enrollUrl = `${ctx.panelUrl}/api/enroll`;
|
|
143
|
+
const result = await curlPostUnauthenticated(enrollUrl, {
|
|
144
|
+
token: ctx.token,
|
|
145
|
+
csr: ctx._keyData.csrPem,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
throw new Error(result.error || 'Enrollment failed');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ctx.agentLabel = result.label;
|
|
153
|
+
ctx._certPem = result.cert;
|
|
154
|
+
ctx._caCertPem = result.caCert;
|
|
155
|
+
task.output = `Enrolled as "${result.label}" (serial: ${result.serial})`;
|
|
156
|
+
},
|
|
157
|
+
rendererOptions: { persistentOutput: true },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
title: 'Importing certificate into Keychain',
|
|
161
|
+
task: async (_ctx, task) => {
|
|
162
|
+
// The server overrides the CSR subject with the correct CN=agent:<label>
|
|
163
|
+
// during signing, so the CSR placeholder subject doesn't matter.
|
|
164
|
+
const { identity } = await importIdentityToKeychain(
|
|
165
|
+
ctx._keyData.keyPath,
|
|
166
|
+
ctx._certPem,
|
|
167
|
+
ctx._caCertPem,
|
|
168
|
+
ctx.agentLabel,
|
|
169
|
+
console,
|
|
170
|
+
);
|
|
171
|
+
ctx.keychainIdentity = identity;
|
|
172
|
+
task.output = `Identity "${identity}" imported (non-extractable)`;
|
|
173
|
+
},
|
|
174
|
+
rendererOptions: { persistentOutput: true },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
title: 'Saving CA certificate',
|
|
178
|
+
task: async () => {
|
|
179
|
+
const caPath = path.join(AGENT_DIR, 'ca.crt');
|
|
180
|
+
await writeFile(caPath, ctx._caCertPem, { mode: 0o644 });
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: 'Verifying panel connectivity',
|
|
185
|
+
task: async (_ctx, task) => {
|
|
186
|
+
const config = {
|
|
187
|
+
panelUrl: ctx.panelUrl,
|
|
188
|
+
authMethod: 'keychain',
|
|
189
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
190
|
+
};
|
|
191
|
+
const health = await fetchHealth(config);
|
|
192
|
+
task.output = `Panel is reachable (status: ${health.status || 'ok'})`;
|
|
193
|
+
},
|
|
194
|
+
rendererOptions: { persistentOutput: true },
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: 'Installing Chisel',
|
|
198
|
+
task: async (_ctx, task) => {
|
|
199
|
+
const result = await installChisel();
|
|
200
|
+
ctx.chiselVersion = result.version;
|
|
201
|
+
if (result.skipped) {
|
|
202
|
+
task.output = `Already installed (${result.version})`;
|
|
203
|
+
} else {
|
|
204
|
+
task.output = `Installed ${result.version}`;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
rendererOptions: { persistentOutput: true },
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
title: 'Fetching tunnel configuration',
|
|
211
|
+
task: async (_ctx, task) => {
|
|
212
|
+
const config = {
|
|
213
|
+
panelUrl: ctx.panelUrl,
|
|
214
|
+
authMethod: 'keychain',
|
|
215
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
216
|
+
};
|
|
217
|
+
const data = await fetchPlist(config);
|
|
218
|
+
ctx.plistXml = data.plist;
|
|
219
|
+
|
|
220
|
+
const tunnelData = await fetchTunnels(config);
|
|
221
|
+
ctx.tunnels = tunnelData.tunnels || [];
|
|
222
|
+
task.output = `${ctx.tunnels.length} tunnel(s) configured`;
|
|
223
|
+
},
|
|
224
|
+
rendererOptions: { persistentOutput: true },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
title: 'Rewriting plist paths',
|
|
228
|
+
task: async () => {
|
|
229
|
+
ctx.plistXml = rewritePlist(ctx.plistXml);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
title: 'Writing plist file',
|
|
234
|
+
task: async () => {
|
|
235
|
+
await writePlistFile(ctx.plistXml);
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
title: 'Unloading previous agent',
|
|
240
|
+
skip: async () => {
|
|
241
|
+
const loaded = await isAgentLoaded();
|
|
242
|
+
return !loaded && 'No previous agent loaded';
|
|
243
|
+
},
|
|
244
|
+
task: async () => {
|
|
245
|
+
await unloadAgent();
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
title: 'Loading agent',
|
|
250
|
+
task: async () => {
|
|
251
|
+
await loadAgent();
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
title: 'Verifying agent is running',
|
|
256
|
+
task: async (_ctx, task) => {
|
|
257
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
258
|
+
const pid = await getAgentPid();
|
|
259
|
+
if (pid) {
|
|
260
|
+
task.output = `Agent running (PID ${pid})`;
|
|
261
|
+
} else {
|
|
262
|
+
const loaded = await isAgentLoaded();
|
|
263
|
+
if (loaded) {
|
|
264
|
+
task.output = 'Agent loaded (process starting...)';
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error('Agent failed to load. Check logs with: portlama-agent logs');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
rendererOptions: { persistentOutput: true },
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
title: 'Saving configuration',
|
|
274
|
+
task: async () => {
|
|
275
|
+
const domainMatch = ctx.plistXml.match(/wss:\/\/tunnel\.([^:]+):/);
|
|
276
|
+
ctx.domain = domainMatch ? domainMatch[1] : null;
|
|
277
|
+
|
|
278
|
+
await saveAgentConfig({
|
|
279
|
+
panelUrl: ctx.panelUrl,
|
|
280
|
+
authMethod: 'keychain',
|
|
281
|
+
keychainIdentity: ctx.keychainIdentity,
|
|
282
|
+
agentLabel: ctx.agentLabel,
|
|
283
|
+
domain: ctx.domain,
|
|
284
|
+
chiselVersion: ctx.chiselVersion,
|
|
285
|
+
setupAt: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
{
|
|
291
|
+
renderer: 'default',
|
|
292
|
+
rendererOptions: { collapseSubtasks: false },
|
|
293
|
+
exitOnError: true,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await tasks.run();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// Securely delete the temporary private key if it was generated but not
|
|
301
|
+
// yet imported into Keychain (importIdentityToKeychain handles its own cleanup).
|
|
302
|
+
if (ctx._keyData?.keyPath) {
|
|
303
|
+
await secureDelete(ctx._keyData.keyPath).catch(() => {});
|
|
304
|
+
}
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Print summary
|
|
309
|
+
printSetupSummary(ctx);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Traditional P12-based setup flow.
|
|
314
|
+
*/
|
|
315
|
+
async function runP12Setup() {
|
|
41
316
|
// Step 1: Verify macOS
|
|
42
317
|
assertMacOS();
|
|
43
318
|
|
|
@@ -206,6 +481,7 @@ export async function runSetup() {
|
|
|
206
481
|
|
|
207
482
|
await saveAgentConfig({
|
|
208
483
|
panelUrl: ctx.panelUrl,
|
|
484
|
+
authMethod: 'p12',
|
|
209
485
|
p12Path: ctx.p12Path,
|
|
210
486
|
p12Password: ctx.p12Password,
|
|
211
487
|
domain: ctx.domain,
|
package/src/commands/sites.js
CHANGED
|
@@ -87,7 +87,7 @@ async function runList(config) {
|
|
|
87
87
|
|
|
88
88
|
let sites;
|
|
89
89
|
try {
|
|
90
|
-
const data = await fetchSites(config
|
|
90
|
+
const data = await fetchSites(config);
|
|
91
91
|
sites = data.sites || [];
|
|
92
92
|
} catch {
|
|
93
93
|
console.log(` ${y('Could not reach panel to fetch site list.')}`);
|
|
@@ -163,7 +163,7 @@ async function runCreate(config, args) {
|
|
|
163
163
|
|
|
164
164
|
let result;
|
|
165
165
|
try {
|
|
166
|
-
result = await createSite(config
|
|
166
|
+
result = await createSite(config, body);
|
|
167
167
|
} catch (err) {
|
|
168
168
|
const detail = err.message || 'Unknown error';
|
|
169
169
|
console.error(`\n ${chalk.red(`Failed to create site: ${detail}`)}\n`);
|
|
@@ -207,7 +207,7 @@ async function runDelete(config, args) {
|
|
|
207
207
|
} else {
|
|
208
208
|
let data;
|
|
209
209
|
try {
|
|
210
|
-
data = await fetchSites(config
|
|
210
|
+
data = await fetchSites(config);
|
|
211
211
|
} catch (err) {
|
|
212
212
|
console.error(`\n ${chalk.red(`Failed to connect to panel: ${err.message}`)}\n`);
|
|
213
213
|
process.exit(1);
|
|
@@ -229,7 +229,7 @@ async function runDelete(config, args) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
try {
|
|
232
|
-
await deleteSite(config
|
|
232
|
+
await deleteSite(config, siteId);
|
|
233
233
|
} catch (err) {
|
|
234
234
|
const detail = err.message || 'Unknown error';
|
|
235
235
|
console.error(`\n ${chalk.red(`Failed to delete site: ${detail}`)}\n`);
|
package/src/commands/status.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function runStatus() {
|
|
|
69
69
|
console.log(d(' ─'.repeat(28)));
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
|
-
const data = await fetchTunnels(config
|
|
72
|
+
const data = await fetchTunnels(config);
|
|
73
73
|
const tunnels = data.tunnels || [];
|
|
74
74
|
|
|
75
75
|
if (tunnels.length === 0) {
|
package/src/commands/update.js
CHANGED
|
@@ -25,14 +25,10 @@ export async function runUpdate() {
|
|
|
25
25
|
{
|
|
26
26
|
title: 'Fetching updated tunnel configuration',
|
|
27
27
|
task: async (_ctx, task) => {
|
|
28
|
-
const data = await fetchPlist(config
|
|
28
|
+
const data = await fetchPlist(config);
|
|
29
29
|
ctx.plistXml = data.plist;
|
|
30
30
|
|
|
31
|
-
const tunnelData = await fetchTunnels(
|
|
32
|
-
config.panelUrl,
|
|
33
|
-
config.p12Path,
|
|
34
|
-
config.p12Password,
|
|
35
|
-
);
|
|
31
|
+
const tunnelData = await fetchTunnels(config);
|
|
36
32
|
ctx.tunnels = tunnelData.tunnels || [];
|
|
37
33
|
task.output = `${ctx.tunnels.length} tunnel(s) configured`;
|
|
38
34
|
},
|
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/config.js
CHANGED
|
@@ -4,12 +4,29 @@ import { CONFIG_PATH, AGENT_DIR } from './platform.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Load the agent config from ~/.portlama/agent.json.
|
|
6
6
|
* Returns null if the file does not exist.
|
|
7
|
+
*
|
|
8
|
+
* Config fields:
|
|
9
|
+
* - panelUrl: string — Panel URL
|
|
10
|
+
* - authMethod: 'p12' | 'keychain' — Authentication method (defaults to 'p12' if missing)
|
|
11
|
+
* - p12Path: string — Path to P12 file (when authMethod is 'p12')
|
|
12
|
+
* - p12Password: string — P12 password (when authMethod is 'p12')
|
|
13
|
+
* - keychainIdentity: string — Keychain identity name (when authMethod is 'keychain')
|
|
14
|
+
* - agentLabel: string — Agent label (when authMethod is 'keychain')
|
|
15
|
+
* - domain?: string
|
|
16
|
+
* - chiselVersion?: string
|
|
17
|
+
* - setupAt?: string
|
|
18
|
+
*
|
|
7
19
|
* @returns {Promise<object | null>}
|
|
8
20
|
*/
|
|
9
21
|
export async function loadAgentConfig() {
|
|
10
22
|
try {
|
|
11
23
|
const raw = await readFile(CONFIG_PATH, 'utf8');
|
|
12
|
-
|
|
24
|
+
const config = JSON.parse(raw);
|
|
25
|
+
// Default authMethod to 'p12' for backwards compatibility
|
|
26
|
+
if (config && !config.authMethod) {
|
|
27
|
+
config.authMethod = 'p12';
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
13
30
|
} catch {
|
|
14
31
|
return null;
|
|
15
32
|
}
|