@lamalibre/portlama-agent 1.0.7 → 1.0.9

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/LICENSE.md CHANGED
@@ -10,7 +10,7 @@ You may use, copy, modify, and distribute this software for any **noncommercial*
10
10
 
11
11
  ## Commercial Use
12
12
 
13
- Commercial use of this software requires a commercial license, available by contacting licence@codelama.com.tr.
13
+ Commercial use of this software requires a commercial license, available by contacting license@codelama.com.tr.
14
14
 
15
15
  Commercial use includes, but is not limited to:
16
16
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @lamalibre/portlama-agent
2
2
 
3
- Mac tunnel agent for Portlama — installs a Chisel tunnel client and manages it
4
- as a macOS launchd agent.
3
+ Tunnel agent for Portlama — manages a Chisel tunnel client as a system service
4
+ on macOS (launchd) and Linux (systemd).
5
5
 
6
6
  ## Installation
7
7
 
@@ -10,16 +10,17 @@ npx @lamalibre/portlama-agent setup
10
10
  ```
11
11
 
12
12
  The setup command downloads the Chisel binary, configures the tunnel connection,
13
- installs a launchd plist, and starts the agent. The panel provides the
14
- connection details and an agent-scoped mTLS certificate.
13
+ installs a system service (launchd plist on macOS, systemd unit on Linux), and
14
+ starts the agent. The panel provides the connection details and an agent-scoped
15
+ mTLS certificate.
15
16
 
16
17
  ## Commands
17
18
 
18
19
  | Command | Description |
19
20
  | ---------------------------------- | ------------------------------------------ |
20
21
  | `setup` | Install Chisel and configure the tunnel |
21
- | `update` | Update Chisel binary to latest version |
22
- | `uninstall` | Remove Chisel, plist, and configuration |
22
+ | `update` | Re-fetch config from panel and restart |
23
+ | `uninstall` | Remove Chisel, service, and configuration |
23
24
  | `status` | Show tunnel connection status |
24
25
  | `logs` | Display recent tunnel logs |
25
26
  | `sites` | List all static sites |
@@ -102,11 +103,11 @@ portlama-agent deploy blog ./dist
102
103
 
103
104
  ## Requirements
104
105
 
105
- | Requirement | Details |
106
- | ----------- | ------------------------------- |
107
- | OS | macOS |
108
- | Node.js | >= 20.0.0 |
109
- | Access | User account (no root required) |
106
+ | Requirement | Details |
107
+ | ----------- | ----------------------------------------------- |
108
+ | OS | macOS or Ubuntu Linux (24.04 LTS) |
109
+ | Node.js | >= 20.0.0 |
110
+ | Access | User account on macOS; root/sudo on Linux |
110
111
 
111
112
  ## How It Works
112
113
 
@@ -115,8 +116,8 @@ certificate (not the admin certificate). It connects to the server's Chisel
115
116
  endpoint over WebSocket-over-HTTPS and exposes local ports as configured
116
117
  in the panel's tunnel settings.
117
118
 
118
- The launchd plist ensures the tunnel reconnects automatically after reboot
119
- or network changes.
119
+ The system service (launchd on macOS, systemd on Linux) ensures the tunnel
120
+ reconnects automatically after reboot or network changes.
120
121
 
121
122
  ## Further Reading
122
123
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lamalibre/portlama-agent",
3
- "version": "1.0.7",
4
- "description": "Mac agent for Portlama — installs Chisel tunnel client and manages launchd agent",
3
+ "version": "1.0.9",
4
+ "description": "Tunnel agent for Portlama — manages Chisel tunnel client as a system service",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "author": "Code Lama Software",
@@ -30,6 +30,9 @@
30
30
  "chisel",
31
31
  "macos",
32
32
  "launchd",
33
+ "linux",
34
+ "systemd",
35
+ "ubuntu",
33
36
  "agent"
34
37
  ],
35
38
  "dependencies": {
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { Listr } from 'listr2';
3
3
  import { readdir, stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { assertMacOS } from '../lib/platform.js';
5
+ import { assertSupportedPlatform } from '../lib/platform.js';
6
6
  import { requireAgentConfig } from '../lib/config.js';
7
7
  import { fetchSites, fetchSiteFiles, deleteSiteFile, uploadSiteFiles } from '../lib/panel-api.js';
8
8
  import { formatBytes } from '../lib/format.js';
@@ -40,7 +40,7 @@ async function scanDirectory(dir, base = '') {
40
40
  * @param {string[]} args
41
41
  */
42
42
  export async function runDeploy(args) {
43
- assertMacOS();
43
+ assertSupportedPlatform();
44
44
  const config = await requireAgentConfig();
45
45
 
46
46
  const target = args[0];
@@ -1,14 +1,14 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { execa } from 'execa';
3
3
  import chalk from 'chalk';
4
- import { assertMacOS, LOG_FILE, ERROR_LOG_FILE } from '../lib/platform.js';
4
+ import { assertSupportedPlatform, LOG_FILE, ERROR_LOG_FILE } from '../lib/platform.js';
5
5
 
6
6
  /**
7
7
  * Stream chisel logs to the terminal.
8
8
  * Tails both stdout and stderr log files.
9
9
  */
10
10
  export async function runLogs() {
11
- assertMacOS();
11
+ assertSupportedPlatform();
12
12
 
13
13
  const files = [];
14
14
  if (existsSync(LOG_FILE)) files.push(LOG_FILE);
@@ -3,7 +3,7 @@ import { execa } from 'execa';
3
3
  import { readFile, writeFile, rename, open, mkdir, rm } from 'node:fs/promises';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
- import { assertMacOS, AGENT_DIR } from '../lib/platform.js';
6
+ import { assertSupportedPlatform, AGENT_DIR } from '../lib/platform.js';
7
7
 
8
8
  const PLUGINS_FILE = path.join(AGENT_DIR, 'plugins.json');
9
9
 
@@ -225,7 +225,7 @@ async function showStatus() {
225
225
  * Entry point for the plugin subcommand.
226
226
  */
227
227
  export async function runPlugin(args) {
228
- assertMacOS();
228
+ assertSupportedPlatform();
229
229
 
230
230
  const subcommand = args[0];
231
231
  const target = args[1];
@@ -5,14 +5,15 @@ import { resolve } from 'node:path';
5
5
  import path from 'node:path';
6
6
  import { Listr } from 'listr2';
7
7
  import chalk from 'chalk';
8
- import { assertMacOS, CHISEL_BIN_DIR, LOGS_DIR, AGENT_DIR } from '../lib/platform.js';
8
+ import { assertSupportedPlatform, isDarwin, CHISEL_BIN_DIR, LOGS_DIR, AGENT_DIR } from '../lib/platform.js';
9
9
  import { loadAgentConfig, saveAgentConfig } from '../lib/config.js';
10
- import { fetchHealth, fetchPlist, fetchTunnels, curlPostUnauthenticated } from '../lib/panel-api.js';
10
+ import { fetchHealth, fetchAgentConfig, fetchTunnels, curlPostUnauthenticated } from '../lib/panel-api.js';
11
11
  import { extractPemFromP12, cleanupPemFiles } from '../lib/ws-helpers.js';
12
12
  import { installChisel } from '../lib/chisel.js';
13
- import { rewritePlist, writePlistFile } from '../lib/plist.js';
14
- import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/launchctl.js';
15
- import { generateKeypairAndCSR, importIdentityToKeychain, secureDelete } from '../lib/keychain.js';
13
+ import { generateServiceConfig, writeServiceConfigFile } from '../lib/service-config.js';
14
+ import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/service.js';
15
+ import { generateKeypairAndCSR, secureDelete } from '../lib/keychain.js';
16
+ import { storeEnrolledCert } from '../lib/cert-store.js';
16
17
 
17
18
  /**
18
19
  * Prompt for user input via readline.
@@ -38,6 +39,8 @@ function prompt(question, defaultValue) {
38
39
 
39
40
  /**
40
41
  * Parse --token and --panel-url flags from argv.
42
+ * Token can also be provided via PORTLAMA_ENROLLMENT_TOKEN env var
43
+ * to avoid exposure in process listings.
41
44
  * @returns {{ token?: string, panelUrl?: string }}
42
45
  */
43
46
  function parseSetupFlags() {
@@ -50,6 +53,10 @@ function parseSetupFlags() {
50
53
  flags.panelUrl = args[++i];
51
54
  }
52
55
  }
56
+ // Prefer env var over CLI arg to keep token out of process listings
57
+ if (process.env.PORTLAMA_ENROLLMENT_TOKEN) {
58
+ flags.token = process.env.PORTLAMA_ENROLLMENT_TOKEN;
59
+ }
53
60
  return flags;
54
61
  }
55
62
 
@@ -75,8 +82,8 @@ export async function runSetup() {
75
82
  * @param {{ token: string, panelUrl?: string }} flags
76
83
  */
77
84
  async function runTokenSetup(flags) {
78
- // Step 1: Verify macOS
79
- assertMacOS();
85
+ // Step 1: Verify supported platform
86
+ assertSupportedPlatform();
80
87
 
81
88
  // Check for existing config
82
89
  const existingConfig = await loadAgentConfig();
@@ -88,8 +95,10 @@ async function runTokenSetup(flags) {
88
95
  }
89
96
 
90
97
  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.'));
98
+ console.log(chalk.bold(' Portlama Agent Setup (Token-Based Enrollment)'));
99
+ console.log(chalk.dim(isDarwin()
100
+ ? ' Connect this Mac to your Portlama server using a Keychain-bound certificate.'
101
+ : ' Connect this machine to your Portlama server using a certificate.'));
93
102
  console.log('');
94
103
 
95
104
  let panelUrl = flags.panelUrl;
@@ -110,8 +119,10 @@ async function runTokenSetup(flags) {
110
119
  token: flags.token,
111
120
  agentLabel: null,
112
121
  keychainIdentity: null,
122
+ p12Path: null,
123
+ p12Password: null,
113
124
  chiselVersion: null,
114
- plistXml: null,
125
+ serviceConfig: null,
115
126
  domain: null,
116
127
  tunnels: [],
117
128
  };
@@ -157,19 +168,25 @@ async function runTokenSetup(flags) {
157
168
  rendererOptions: { persistentOutput: true },
158
169
  },
159
170
  {
160
- title: 'Importing certificate into Keychain',
171
+ title: isDarwin() ? 'Importing certificate into Keychain' : 'Storing certificate',
161
172
  task: async (_ctx, task) => {
162
173
  // The server overrides the CSR subject with the correct CN=agent:<label>
163
174
  // during signing, so the CSR placeholder subject doesn't matter.
164
- const { identity } = await importIdentityToKeychain(
175
+ const result = await storeEnrolledCert(
165
176
  ctx._keyData.keyPath,
166
177
  ctx._certPem,
167
178
  ctx._caCertPem,
168
179
  ctx.agentLabel,
169
180
  console,
170
181
  );
171
- ctx.keychainIdentity = identity;
172
- task.output = `Identity "${identity}" imported (non-extractable)`;
182
+ if (result.identity) {
183
+ ctx.keychainIdentity = result.identity;
184
+ task.output = `Identity "${result.identity}" imported (non-extractable)`;
185
+ } else {
186
+ ctx.p12Path = result.p12Path;
187
+ ctx.p12Password = result.p12Password;
188
+ task.output = `Certificate stored at ${result.p12Path}`;
189
+ }
173
190
  },
174
191
  rendererOptions: { persistentOutput: true },
175
192
  },
@@ -183,12 +200,10 @@ async function runTokenSetup(flags) {
183
200
  {
184
201
  title: 'Verifying panel connectivity',
185
202
  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);
203
+ const authConfig = ctx.keychainIdentity
204
+ ? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
205
+ : { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
206
+ const health = await fetchHealth(authConfig);
192
207
  task.output = `Panel is reachable (status: ${health.status || 'ok'})`;
193
208
  },
194
209
  rendererOptions: { persistentOutput: true },
@@ -209,30 +224,24 @@ async function runTokenSetup(flags) {
209
224
  {
210
225
  title: 'Fetching tunnel configuration',
211
226
  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;
227
+ const authConfig = ctx.keychainIdentity
228
+ ? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
229
+ : { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
219
230
 
220
- const tunnelData = await fetchTunnels(config);
231
+ const agentConfig = await fetchAgentConfig(authConfig);
232
+ ctx.domain = agentConfig.domain;
233
+ ctx.serviceConfig = generateServiceConfig(agentConfig.chiselArgs);
234
+
235
+ const tunnelData = await fetchTunnels(authConfig);
221
236
  ctx.tunnels = tunnelData.tunnels || [];
222
237
  task.output = `${ctx.tunnels.length} tunnel(s) configured`;
223
238
  },
224
239
  rendererOptions: { persistentOutput: true },
225
240
  },
226
241
  {
227
- title: 'Rewriting plist paths',
228
- task: async () => {
229
- ctx.plistXml = rewritePlist(ctx.plistXml);
230
- },
231
- },
232
- {
233
- title: 'Writing plist file',
242
+ title: 'Writing service config',
234
243
  task: async () => {
235
- await writePlistFile(ctx.plistXml);
244
+ await writeServiceConfigFile(ctx.serviceConfig);
236
245
  },
237
246
  },
238
247
  {
@@ -247,13 +256,16 @@ async function runTokenSetup(flags) {
247
256
  },
248
257
  {
249
258
  title: 'Loading agent',
259
+ skip: () => ctx.tunnels.length === 0 && 'No tunnels configured — run portlama-agent update after creating tunnels',
250
260
  task: async () => {
251
261
  await loadAgent();
252
262
  },
253
263
  },
254
264
  {
255
265
  title: 'Verifying agent is running',
266
+ skip: () => ctx.tunnels.length === 0 && 'No tunnels configured',
256
267
  task: async (_ctx, task) => {
268
+ // Give the service manager a moment to start the process
257
269
  await new Promise((r) => setTimeout(r, 2000));
258
270
  const pid = await getAgentPid();
259
271
  if (pid) {
@@ -272,18 +284,24 @@ async function runTokenSetup(flags) {
272
284
  {
273
285
  title: 'Saving configuration',
274
286
  task: async () => {
275
- const domainMatch = ctx.plistXml.match(/wss:\/\/tunnel\.([^:]+):/);
276
- ctx.domain = domainMatch ? domainMatch[1] : null;
277
-
278
- await saveAgentConfig({
287
+ const configData = {
279
288
  panelUrl: ctx.panelUrl,
280
- authMethod: 'keychain',
281
- keychainIdentity: ctx.keychainIdentity,
282
289
  agentLabel: ctx.agentLabel,
283
290
  domain: ctx.domain,
284
291
  chiselVersion: ctx.chiselVersion,
285
292
  setupAt: new Date().toISOString(),
286
- });
293
+ };
294
+
295
+ if (ctx.keychainIdentity) {
296
+ configData.authMethod = 'keychain';
297
+ configData.keychainIdentity = ctx.keychainIdentity;
298
+ } else {
299
+ configData.authMethod = 'p12';
300
+ configData.p12Path = ctx.p12Path;
301
+ configData.p12Password = ctx.p12Password;
302
+ }
303
+
304
+ await saveAgentConfig(configData);
287
305
  },
288
306
  },
289
307
  ],
@@ -298,7 +316,7 @@ async function runTokenSetup(flags) {
298
316
  await tasks.run();
299
317
  } catch (err) {
300
318
  // Securely delete the temporary private key if it was generated but not
301
- // yet imported into Keychain (importIdentityToKeychain handles its own cleanup).
319
+ // yet consumed by storeEnrolledCert (which handles its own cleanup).
302
320
  if (ctx._keyData?.keyPath) {
303
321
  await secureDelete(ctx._keyData.keyPath).catch(() => {});
304
322
  }
@@ -313,8 +331,8 @@ async function runTokenSetup(flags) {
313
331
  * Traditional P12-based setup flow.
314
332
  */
315
333
  async function runP12Setup() {
316
- // Step 1: Verify macOS
317
- assertMacOS();
334
+ // Step 1: Verify supported platform
335
+ assertSupportedPlatform();
318
336
 
319
337
  // Check for existing config
320
338
  const existingConfig = await loadAgentConfig();
@@ -328,7 +346,9 @@ async function runP12Setup() {
328
346
  // Step 2: Prompt credentials
329
347
  console.log('');
330
348
  console.log(chalk.bold(' Portlama Agent Setup'));
331
- console.log(chalk.dim(' Connect this Mac to your Portlama server.'));
349
+ console.log(chalk.dim(isDarwin()
350
+ ? ' Connect this Mac to your Portlama server.'
351
+ : ' Connect this machine to your Portlama server.'));
332
352
  console.log('');
333
353
  console.log(chalk.dim(' The admin must generate an agent certificate from the panel first:'));
334
354
  console.log(chalk.dim(' Panel → Certificates → Agent Certificates → Generate'));
@@ -363,7 +383,7 @@ async function runP12Setup() {
363
383
  p12Path,
364
384
  p12Password,
365
385
  chiselVersion: null,
366
- plistXml: null,
386
+ serviceConfig: null,
367
387
  domain: null,
368
388
  tunnels: [],
369
389
  };
@@ -415,8 +435,9 @@ async function runP12Setup() {
415
435
  {
416
436
  title: 'Fetching tunnel configuration',
417
437
  task: async (_ctx, task) => {
418
- const data = await fetchPlist(ctx.panelUrl, ctx.p12Path, ctx.p12Password);
419
- ctx.plistXml = data.plist;
438
+ const agentConfig = await fetchAgentConfig(ctx.panelUrl, ctx.p12Path, ctx.p12Password);
439
+ ctx.domain = agentConfig.domain;
440
+ ctx.serviceConfig = generateServiceConfig(agentConfig.chiselArgs);
420
441
 
421
442
  // Also fetch tunnel list for the summary
422
443
  const tunnelData = await fetchTunnels(ctx.panelUrl, ctx.p12Path, ctx.p12Password);
@@ -426,15 +447,9 @@ async function runP12Setup() {
426
447
  rendererOptions: { persistentOutput: true },
427
448
  },
428
449
  {
429
- title: 'Rewriting plist paths',
430
- task: async () => {
431
- ctx.plistXml = rewritePlist(ctx.plistXml);
432
- },
433
- },
434
- {
435
- title: 'Writing plist file',
450
+ title: 'Writing service config',
436
451
  task: async () => {
437
- await writePlistFile(ctx.plistXml);
452
+ await writeServiceConfigFile(ctx.serviceConfig);
438
453
  },
439
454
  },
440
455
  {
@@ -449,14 +464,16 @@ async function runP12Setup() {
449
464
  },
450
465
  {
451
466
  title: 'Loading agent',
467
+ skip: () => ctx.tunnels.length === 0 && 'No tunnels configured — run portlama-agent update after creating tunnels',
452
468
  task: async () => {
453
469
  await loadAgent();
454
470
  },
455
471
  },
456
472
  {
457
473
  title: 'Verifying agent is running',
474
+ skip: () => ctx.tunnels.length === 0 && 'No tunnels configured',
458
475
  task: async (_ctx, task) => {
459
- // Give launchd a moment to start the process
476
+ // Give the service manager a moment to start the process
460
477
  await new Promise((r) => setTimeout(r, 2000));
461
478
  const pid = await getAgentPid();
462
479
  if (pid) {
@@ -475,10 +492,6 @@ async function runP12Setup() {
475
492
  {
476
493
  title: 'Saving configuration',
477
494
  task: async () => {
478
- // Extract domain from the plist (look for wss://tunnel.<domain>)
479
- const domainMatch = ctx.plistXml.match(/wss:\/\/tunnel\.([^:]+):/);
480
- ctx.domain = domainMatch ? domainMatch[1] : null;
481
-
482
495
  await saveAgentConfig({
483
496
  panelUrl: ctx.panelUrl,
484
497
  authMethod: 'p12',
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import chalk from 'chalk';
3
- import { assertMacOS } from '../lib/platform.js';
3
+ import { assertSupportedPlatform } from '../lib/platform.js';
4
4
  import { requireAgentConfig } from '../lib/config.js';
5
5
  import { fetchSites, createSite, deleteSite } from '../lib/panel-api.js';
6
6
  import { formatBytes } from '../lib/format.js';
@@ -57,7 +57,7 @@ function parseFlags(args) {
57
57
  * @param {string[]} args
58
58
  */
59
59
  export async function runSites(args) {
60
- assertMacOS();
60
+ assertSupportedPlatform();
61
61
  const config = await requireAgentConfig();
62
62
  const sub = args[0];
63
63
 
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { assertMacOS, CHISEL_BIN_PATH, PLIST_PATH, LOG_FILE, AGENT_DIR } from '../lib/platform.js';
2
+ import { assertSupportedPlatform, CHISEL_BIN_PATH, SERVICE_CONFIG_PATH, LOG_FILE, AGENT_DIR } from '../lib/platform.js';
3
3
  import { loadAgentConfig } from '../lib/config.js';
4
- import { isAgentLoaded, getAgentPid } from '../lib/launchctl.js';
4
+ import { isAgentLoaded, getAgentPid } from '../lib/service.js';
5
5
  import { getInstalledVersion } from '../lib/chisel.js';
6
6
  import { fetchTunnels } from '../lib/panel-api.js';
7
7
  import { existsSync } from 'node:fs';
@@ -10,7 +10,7 @@ import { existsSync } from 'node:fs';
10
10
  * Print formatted status information about the agent.
11
11
  */
12
12
  export async function runStatus() {
13
- assertMacOS();
13
+ assertSupportedPlatform();
14
14
 
15
15
  const b = chalk.bold;
16
16
  const c = chalk.cyan;
@@ -52,7 +52,7 @@ export async function runStatus() {
52
52
  );
53
53
 
54
54
  // Files
55
- console.log(` ${b('Plist:')} ${existsSync(PLIST_PATH) ? g('present') : y('missing')}`);
55
+ console.log(` ${b('Service:')} ${existsSync(SERVICE_CONFIG_PATH) ? g('present') : y('missing')}`);
56
56
  console.log(` ${b('Config:')} ${existsSync(AGENT_DIR) ? g('present') : y('missing')}`);
57
57
  console.log(` ${b('Logs:')} ${d(LOG_FILE)}`);
58
58
 
@@ -2,14 +2,14 @@ import { rm } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { Listr } from 'listr2';
4
4
  import chalk from 'chalk';
5
- import { assertMacOS, AGENT_DIR, PLIST_PATH } from '../lib/platform.js';
6
- import { isAgentLoaded, unloadAgent } from '../lib/launchctl.js';
5
+ import { assertSupportedPlatform, AGENT_DIR, SERVICE_CONFIG_PATH, isLinux } from '../lib/platform.js';
6
+ import { isAgentLoaded, unloadAgent } from '../lib/service.js';
7
7
 
8
8
  /**
9
- * Unload the agent, remove the plist, chisel binary, and config.
9
+ * Unload the agent, remove the service config, chisel binary, and config.
10
10
  */
11
11
  export async function runUninstall() {
12
- assertMacOS();
12
+ assertSupportedPlatform();
13
13
 
14
14
  const tasks = new Listr(
15
15
  [
@@ -24,10 +24,14 @@ export async function runUninstall() {
24
24
  },
25
25
  },
26
26
  {
27
- title: 'Removing plist file',
28
- skip: () => !existsSync(PLIST_PATH) && 'Plist not found',
27
+ title: 'Removing service config',
28
+ skip: () => !existsSync(SERVICE_CONFIG_PATH) && 'Service config not found',
29
29
  task: async () => {
30
- await rm(PLIST_PATH);
30
+ await rm(SERVICE_CONFIG_PATH);
31
+ if (isLinux()) {
32
+ const { execa } = await import('execa');
33
+ await execa('systemctl', ['daemon-reload']);
34
+ }
31
35
  },
32
36
  },
33
37
  {
@@ -1,22 +1,22 @@
1
1
  import { Listr } from 'listr2';
2
2
  import chalk from 'chalk';
3
- import { assertMacOS } from '../lib/platform.js';
3
+ import { assertSupportedPlatform } from '../lib/platform.js';
4
4
  import { requireAgentConfig, saveAgentConfig } from '../lib/config.js';
5
- import { fetchPlist, fetchTunnels } from '../lib/panel-api.js';
6
- import { rewritePlist, writePlistFile } from '../lib/plist.js';
7
- import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/launchctl.js';
5
+ import { fetchAgentConfig, fetchTunnels } from '../lib/panel-api.js';
6
+ import { generateServiceConfig, writeServiceConfigFile } from '../lib/service-config.js';
7
+ import { isAgentLoaded, unloadAgent, loadAgent, getAgentPid } from '../lib/service.js';
8
8
 
9
9
  /**
10
- * Re-fetch the plist from the panel and restart the agent.
10
+ * Re-fetch tunnel config from the panel and restart the agent.
11
11
  * Used after adding/removing tunnels on the panel.
12
12
  */
13
13
  export async function runUpdate() {
14
- assertMacOS();
14
+ assertSupportedPlatform();
15
15
 
16
16
  const config = await requireAgentConfig();
17
17
 
18
18
  const ctx = {
19
- plistXml: null,
19
+ serviceConfig: null,
20
20
  tunnels: [],
21
21
  };
22
22
 
@@ -25,8 +25,8 @@ 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);
29
- ctx.plistXml = data.plist;
28
+ const agentConfig = await fetchAgentConfig(config);
29
+ ctx.serviceConfig = generateServiceConfig(agentConfig.chiselArgs);
30
30
 
31
31
  const tunnelData = await fetchTunnels(config);
32
32
  ctx.tunnels = tunnelData.tunnels || [];
@@ -35,15 +35,9 @@ export async function runUpdate() {
35
35
  rendererOptions: { persistentOutput: true },
36
36
  },
37
37
  {
38
- title: 'Rewriting plist paths',
38
+ title: 'Writing service config',
39
39
  task: async () => {
40
- ctx.plistXml = rewritePlist(ctx.plistXml);
41
- },
42
- },
43
- {
44
- title: 'Writing plist file',
45
- task: async () => {
46
- await writePlistFile(ctx.plistXml);
40
+ await writeServiceConfigFile(ctx.serviceConfig);
47
41
  },
48
42
  },
49
43
  {
package/src/index.js CHANGED
@@ -9,7 +9,7 @@ function printHelp() {
9
9
  const d = chalk.dim;
10
10
 
11
11
  console.log(`
12
- ${b('portlama-agent')} — Mac tunnel agent for Portlama
12
+ ${b('portlama-agent')} — tunnel agent for Portlama (macOS & Linux)
13
13
 
14
14
  ${b('USAGE')}
15
15
 
@@ -18,7 +18,7 @@ ${b('USAGE')}
18
18
  ${b('COMMANDS')}
19
19
 
20
20
  ${c('setup')} Interactive setup: install Chisel, fetch tunnel config, start agent
21
- ${c('update')} Re-fetch plist from panel after tunnel changes
21
+ ${c('update')} Re-fetch config from panel after tunnel changes
22
22
  ${c('uninstall')} Stop agent and remove all files
23
23
  ${c('status')} Show agent health, tunnel list, connection status
24
24
  ${c('logs')} Stream Chisel log output (tail -f)
@@ -28,9 +28,12 @@ ${b('COMMANDS')}
28
28
 
29
29
  ${b('EXAMPLES')}
30
30
 
31
- ${d('# First-time setup')}
31
+ ${d('# First-time setup (interactive)')}
32
32
  ${c('npx @lamalibre/portlama-agent setup')}
33
33
 
34
+ ${d('# Token-based setup (non-interactive)')}
35
+ ${c('PORTLAMA_ENROLLMENT_TOKEN=<token> portlama-agent setup --panel-url https://1.2.3.4:9292')}
36
+
34
37
  ${d('# After adding a tunnel on the panel')}
35
38
  ${c('portlama-agent update')}
36
39
 
@@ -52,9 +55,9 @@ ${b('EXAMPLES')}
52
55
 
53
56
  ${b('PREREQUISITES')}
54
57
 
55
- ${d('•')} macOS (arm64 or x64)
56
- ${d('•')} Agent certificate (.p12) generated from your Portlama panel
57
- (Panel → Certificates → Agent Certificates → Generate)
58
+ ${d('•')} macOS (arm64 or x64) or Ubuntu Linux (arm64 or x64)
59
+ ${d('•')} Agent certificate (.p12) or enrollment token from your Portlama panel
60
+ (Panel → Certificates → Agent Certificates → Generate / Enroll)
58
61
  ${d('•')} Panel URL (e.g. https://1.2.3.4:9292)
59
62
  `);
60
63
  process.exit(0);