@lamalibre/create-portlama 1.0.35 → 1.0.37

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.
Files changed (23) hide show
  1. package/README.md +20 -0
  2. package/package.json +3 -2
  3. package/src/index.js +146 -52
  4. package/vendor/panel-client/dist/assets/{index-BKznW-YZ.js → index-BU662GVt.js} +21 -21
  5. package/vendor/panel-client/dist/docs/00-introduction/what-is-portlama.md +2 -0
  6. package/vendor/panel-client/dist/docs/01-concepts/mtls.md +1 -0
  7. package/vendor/panel-client/dist/docs/01-concepts/security-model.md +1 -0
  8. package/vendor/panel-client/dist/docs/01-concepts/tunneling.md +12 -0
  9. package/vendor/panel-client/dist/docs/02-guides/desktop-app-setup.md +12 -10
  10. package/vendor/panel-client/dist/docs/02-guides/first-tunnel.md +2 -0
  11. package/vendor/panel-client/dist/docs/03-architecture/management-flow.md +3 -0
  12. package/vendor/panel-client/dist/docs/03-architecture/overview.md +2 -0
  13. package/vendor/panel-client/dist/docs/03-architecture/panel-client.md +4 -1
  14. package/vendor/panel-client/dist/docs/03-architecture/panel-server.md +4 -1
  15. package/vendor/panel-client/dist/docs/03-architecture/system-overview.md +1 -0
  16. package/vendor/panel-client/dist/docs/04-api-reference/certificates.md +1 -1
  17. package/vendor/panel-client/dist/docs/04-api-reference/tunnels.md +9 -0
  18. package/vendor/panel-client/dist/docs/06-reference/config-files.md +49 -2
  19. package/vendor/panel-client/dist/index.html +1 -1
  20. package/vendor/panel-server/package.json +1 -1
  21. package/vendor/panel-server/src/lib/mtls.js +1 -0
  22. package/vendor/panel-server/src/lib/nginx.js +164 -0
  23. package/vendor/panel-server/src/routes/management/tunnels.js +277 -13
package/README.md CHANGED
@@ -17,6 +17,26 @@ the full Portlama stack with zero prompts. When finished it prints:
17
17
 
18
18
  Import the certificate into your browser and open the URL to begin onboarding.
19
19
 
20
+ ### JSON Mode
21
+
22
+ For programmatic use (e.g. from the Portlama desktop app), pass `--json` to
23
+ replace the interactive Listr2 terminal UI with NDJSON progress lines on stdout:
24
+
25
+ ```bash
26
+ npx @lamalibre/create-portlama --json
27
+ ```
28
+
29
+ The `--json` flag implies `--yes` (no prompts) and `--dev` (accept private IPs).
30
+ Each line is a JSON object with one of these shapes:
31
+
32
+ ```jsonl
33
+ {"event":"step","step":"check_environment","status":"running"}
34
+ {"event":"step","step":"harden_system","status":"complete"}
35
+ {"event":"step","step":"install_node","status":"skipped"}
36
+ {"event":"complete","server":{"ip":"...","panelUrl":"...","p12Path":"...","p12PasswordPath":"..."}}
37
+ {"event":"error","message":"...","recoverable":false}
38
+ ```
39
+
20
40
  ## What It Provisions
21
41
 
22
42
  - **OS hardening** — swap, UFW, fail2ban, SSH lockdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/create-portlama",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "One-command setup for secure reverse tunnels with a management dashboard",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -15,8 +15,9 @@
15
15
  "vendor"
16
16
  ],
17
17
  "scripts": {
18
+ "build": "node scripts/bundle-vendor.js",
18
19
  "lint": "eslint .",
19
- "prepublishOnly": "npm run lint && node scripts/bundle-vendor.js"
20
+ "prepublishOnly": "npm run lint && npm run build"
20
21
  },
21
22
  "repository": {
22
23
  "type": "git",
package/src/index.js CHANGED
@@ -15,7 +15,7 @@ import { redeployTasks } from './tasks/redeploy.js';
15
15
 
16
16
  /**
17
17
  * Parse minimal CLI flags from process.argv.
18
- * @returns {{ skipHarden: boolean, uninstall: boolean, dev: boolean, help: boolean, yes: boolean }}
18
+ * @returns {{ skipHarden: boolean, uninstall: boolean, dev: boolean, help: boolean, yes: boolean, json: boolean, forceFull: boolean }}
19
19
  */
20
20
  function parseFlags() {
21
21
  const args = process.argv.slice(2);
@@ -26,9 +26,19 @@ function parseFlags() {
26
26
  help: args.includes('--help') || args.includes('-h'),
27
27
  yes: args.includes('--yes') || args.includes('-y'),
28
28
  forceFull: args.includes('--force-full'),
29
+ json: args.includes('--json'),
29
30
  };
30
31
  }
31
32
 
33
+ /**
34
+ * Emit a single NDJSON line to stdout. Used when --json mode is active
35
+ * so that callers (e.g. Tauri desktop app) can parse structured progress.
36
+ * @param {Record<string, unknown>} obj
37
+ */
38
+ function emitJson(obj) {
39
+ process.stdout.write(JSON.stringify(obj) + '\n');
40
+ }
41
+
32
42
  /**
33
43
  * Print help message describing Portlama and what the installer does,
34
44
  * then exit with code 0.
@@ -61,7 +71,7 @@ ${b('SYSTEM MODIFICATIONS')}
61
71
  • Creates a 1GB swap file
62
72
 
63
73
  ${y('Firewall & Security')}
64
- • Resets UFW firewall (allows only ports 22, 443, 9292)
74
+ • Resets UFW firewall (allows only ports 22, 80, 443, 9292)
65
75
  • Installs fail2ban with SSH and nginx jails
66
76
  • Hardens SSH (disables password authentication)
67
77
 
@@ -94,6 +104,7 @@ ${b('FLAGS')}
94
104
  ${c('--yes')}, ${c('-y')} Skip the confirmation prompt
95
105
  ${c('--skip-harden')} Skip OS hardening (swap, UFW, fail2ban, SSH)
96
106
  ${c('--dev')} Allow private/non-routable IP addresses
107
+ ${c('--json')} Output NDJSON progress lines instead of terminal UI
97
108
  ${c('--force-full')} Run full installation even on existing installs
98
109
  ${c('--uninstall')} Show manual removal guide for Portlama
99
110
  `);
@@ -371,9 +382,37 @@ ${chalk.cyan.bold('└───────────────────
371
382
  });
372
383
  }
373
384
 
385
+ /**
386
+ * Run a single Listr task step in JSON mode: emit running/complete/skipped
387
+ * NDJSON events around each step so the caller gets per-step progress.
388
+ * @param {Record<string, unknown>} ctx - Shared installer context.
389
+ * @param {{ key: string, title: string, fn: Function, skip?: () => boolean }} step
390
+ */
391
+ async function runJsonStep(ctx, step) {
392
+ if (step.skip && step.skip()) {
393
+ emitJson({ event: 'step', step: step.key, status: 'skipped' });
394
+ return;
395
+ }
396
+ emitJson({ event: 'step', step: step.key, status: 'running' });
397
+ const taskList = new Listr(
398
+ [{ title: step.title, task: (_c, t) => step.fn(ctx, t) }],
399
+ { renderer: 'silent', exitOnError: true },
400
+ );
401
+ try {
402
+ await taskList.run();
403
+ } catch (error) {
404
+ emitJson({ event: 'step', step: step.key, status: 'failed' });
405
+ throw error;
406
+ }
407
+ emitJson({ event: 'step', step: step.key, status: 'complete' });
408
+ }
409
+
374
410
  /**
375
411
  * Main installer orchestrator. Creates a shared context, runs all installation
376
412
  * tasks through Listr2, and prints a summary on completion.
413
+ *
414
+ * When --json is active, outputs NDJSON progress lines instead of the
415
+ * interactive Listr2 terminal UI. The --json flag implies --yes and --dev.
377
416
  */
378
417
  export async function main() {
379
418
  const flags = parseFlags();
@@ -386,6 +425,14 @@ export async function main() {
386
425
  printUninstallGuide();
387
426
  }
388
427
 
428
+ // --json implies --yes (no interactive prompts) and --dev (accept private IPs)
429
+ if (flags.json) {
430
+ flags.yes = true;
431
+ flags.dev = true;
432
+ }
433
+
434
+ const renderer = flags.json ? 'silent' : 'default';
435
+
389
436
  const ctx = {
390
437
  ip: null,
391
438
  osRelease: null,
@@ -437,44 +484,7 @@ export async function main() {
437
484
  },
438
485
  ],
439
486
  {
440
- renderer: 'default',
441
- rendererOptions: { collapseSubtasks: false },
442
- exitOnError: true,
443
- },
444
- );
445
-
446
- // Phase 2: Installation tasks
447
- const installTasks = new Listr(
448
- [
449
- {
450
- title: 'Hardening operating system',
451
- task: (_ctx, task) => hardenTasks(ctx, task),
452
- },
453
- {
454
- title: 'Installing Node.js 20',
455
- task: (_ctx, task) => nodeTasks(ctx, task),
456
- },
457
- {
458
- title: 'Generating mTLS certificates',
459
- task: (_ctx, task) => mtlsTasks(ctx, task),
460
- },
461
- {
462
- title: 'Configuring nginx',
463
- task: (_ctx, task) => nginxTasks(ctx, task),
464
- },
465
- {
466
- title: 'Deploying Portlama panel',
467
- task: (_ctx, task) => panelTasks(ctx, task),
468
- },
469
- {
470
- title: 'Installation complete',
471
- task: async () => {
472
- // Summary will be printed after Listr finishes
473
- },
474
- },
475
- ],
476
- {
477
- renderer: 'default',
487
+ renderer,
478
488
  rendererOptions: { collapseSubtasks: false },
479
489
  exitOnError: true,
480
490
  },
@@ -482,7 +492,13 @@ export async function main() {
482
492
 
483
493
  try {
484
494
  // Run environment checks first
495
+ if (flags.json) {
496
+ emitJson({ event: 'step', step: 'check_environment', status: 'running' });
497
+ }
485
498
  await envTasks.run();
499
+ if (flags.json) {
500
+ emitJson({ event: 'step', step: 'check_environment', status: 'complete' });
501
+ }
486
502
 
487
503
  // Detect existing system state for the confirmation banner
488
504
  const existingState = await detectExistingState();
@@ -490,30 +506,96 @@ export async function main() {
490
506
  // Determine mode: redeploy (fast update) or full install
491
507
  const isRedeploy = existingState.portlamaExists && !flags.forceFull;
492
508
 
493
- // Show confirmation banner and wait for user input
494
- await confirmInstallation(flags, isRedeploy, existingState);
509
+ // Show confirmation banner and wait for user input (skipped in JSON mode)
510
+ if (!flags.json) {
511
+ await confirmInstallation(flags, isRedeploy, existingState);
512
+ }
495
513
 
496
514
  if (isRedeploy) {
497
- // Fast path: only update panel files and restart
498
- const redeployTaskList = new Listr(
515
+ if (flags.json) {
516
+ // JSON mode: emit step-level progress for redeploy
517
+ await runJsonStep(ctx, {
518
+ key: 'redeploy_panel',
519
+ title: 'Redeploying Portlama panel',
520
+ fn: redeployTasks,
521
+ });
522
+ } else {
523
+ // Fast path: only update panel files and restart
524
+ const redeployTaskList = new Listr(
525
+ [
526
+ {
527
+ title: 'Redeploying Portlama panel',
528
+ task: (_ctx, task) => redeployTasks(ctx, task),
529
+ },
530
+ ],
531
+ {
532
+ renderer,
533
+ rendererOptions: { collapseSubtasks: false },
534
+ exitOnError: true,
535
+ },
536
+ );
537
+ await redeployTaskList.run();
538
+ }
539
+ } else if (flags.json) {
540
+ // JSON mode: run each install step individually with NDJSON progress
541
+ const installSteps = [
542
+ { key: 'harden_system', title: 'Hardening operating system', fn: hardenTasks, skip: () => ctx.skipHarden },
543
+ { key: 'install_node', title: 'Installing Node.js 20', fn: nodeTasks },
544
+ { key: 'generate_certs', title: 'Generating mTLS certificates', fn: mtlsTasks },
545
+ { key: 'configure_nginx', title: 'Configuring nginx', fn: nginxTasks },
546
+ { key: 'deploy_panel', title: 'Deploying Portlama panel', fn: panelTasks },
547
+ ];
548
+ for (const step of installSteps) {
549
+ await runJsonStep(ctx, step);
550
+ }
551
+ } else {
552
+ // Full install path with interactive Listr2 rendering
553
+ const installTasks = new Listr(
499
554
  [
500
555
  {
501
- title: 'Redeploying Portlama panel',
502
- task: (_ctx, task) => redeployTasks(ctx, task),
556
+ title: 'Hardening operating system',
557
+ task: (_ctx, task) => hardenTasks(ctx, task),
558
+ },
559
+ {
560
+ title: 'Installing Node.js 20',
561
+ task: (_ctx, task) => nodeTasks(ctx, task),
562
+ },
563
+ {
564
+ title: 'Generating mTLS certificates',
565
+ task: (_ctx, task) => mtlsTasks(ctx, task),
566
+ },
567
+ {
568
+ title: 'Configuring nginx',
569
+ task: (_ctx, task) => nginxTasks(ctx, task),
570
+ },
571
+ {
572
+ title: 'Deploying Portlama panel',
573
+ task: (_ctx, task) => panelTasks(ctx, task),
574
+ },
575
+ {
576
+ title: 'Installation complete',
577
+ task: async () => {
578
+ // Summary will be printed after Listr finishes
579
+ },
503
580
  },
504
581
  ],
505
582
  {
506
- renderer: 'default',
583
+ renderer,
507
584
  rendererOptions: { collapseSubtasks: false },
508
585
  exitOnError: true,
509
586
  },
510
587
  );
511
- await redeployTaskList.run();
512
- } else {
513
- // Full install path
514
588
  await installTasks.run();
515
589
  }
516
590
  } catch (error) {
591
+ if (flags.json) {
592
+ emitJson({
593
+ event: 'error',
594
+ message: error.message || 'Unknown error',
595
+ recoverable: false,
596
+ });
597
+ process.exit(1);
598
+ }
517
599
  console.error('\n');
518
600
  console.error(' ┌─────────────────────────────────────────────┐');
519
601
  console.error(' │ Portlama installation failed. │');
@@ -526,6 +608,18 @@ export async function main() {
526
608
  process.exit(1);
527
609
  }
528
610
 
529
- // Print summary after Listr2 finishes rendering
530
- await printSummary(ctx);
611
+ if (flags.json) {
612
+ emitJson({
613
+ event: 'complete',
614
+ server: {
615
+ ip: ctx.ip,
616
+ panelUrl: `https://${ctx.ip}:9292`,
617
+ p12Path: `${ctx.pkiDir}/client.p12`,
618
+ p12PasswordPath: `${ctx.pkiDir}/.p12-password`,
619
+ },
620
+ });
621
+ } else {
622
+ // Print summary after Listr2 finishes rendering
623
+ await printSummary(ctx);
624
+ }
531
625
  }