@vlandoss/localproxy 0.0.2-git-b8d71ee.0 → 0.0.2-git-62e4f21.0

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": "@vlandoss/localproxy",
3
- "version": "0.0.2-git-b8d71ee.0",
3
+ "version": "0.0.2-git-62e4f21.0",
4
4
  "description": "Simple local development proxy automation",
5
5
  "homepage": "https://github.com/variableland/dx/tree/main/packages/localproxy#readme",
6
6
  "bugs": {
@@ -26,8 +26,8 @@
26
26
  "dependencies": {
27
27
  "@inquirer/prompts": "^7.8.4",
28
28
  "commander": "13.1.0",
29
- "@vlandoss/loggy": "0.0.4",
30
- "@vlandoss/clibuddy": "0.0.5"
29
+ "@vlandoss/clibuddy": "0.0.5",
30
+ "@vlandoss/loggy": "0.0.5-git-62e4f21.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -1,4 +1,3 @@
1
- import { password as passwordPrompt } from "@inquirer/prompts";
2
1
  import { createCommand } from "commander";
3
2
  import { CaddyService } from "~/services/caddy";
4
3
  import { HostsService } from "~/services/hosts";
@@ -13,20 +12,16 @@ export function createCleanCommand({ caddyfilePath }: Context) {
13
12
  return createCommand("clean")
14
13
  .description("clean up config files")
15
14
  .option("--verbose", "verbose mode, show background output", false)
16
- .action(async (options: CommandOptions) => {
15
+ .action(async function cleanAction(options: CommandOptions) {
17
16
  const caddyService = new CaddyService(caddyfilePath);
18
17
  await caddyService.stop(options);
19
18
 
20
19
  const localDomains = caddyService.getLocalDomains();
21
20
  const hosts = localDomains.map((d) => d.host);
22
21
 
23
- const password = await passwordPrompt({
24
- message: "sudo password to manage hosts",
25
- });
26
-
27
22
  const hostsService = new HostsService(hosts);
28
- await hostsService.clean({ ...options, password });
23
+ await hostsService.clean(options);
29
24
 
30
- logger.success("localproxy clean completed");
25
+ logger.success("Clean completed!");
31
26
  });
32
27
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { editor as editorPrompt, password as passwordPrompt } from "@inquirer/prompts";
3
+ import { editor as editorPrompt } from "@inquirer/prompts";
4
4
  import { createCommand } from "commander";
5
5
  import { CaddyService } from "~/services/caddy";
6
6
  import { HostsService } from "~/services/hosts";
@@ -33,7 +33,7 @@ async function checkInternalTools() {
33
33
  process.exit(1);
34
34
  }
35
35
 
36
- debug("Caddy version: %s", caddyVersion.stdout.trim());
36
+ debug("caddy version: %s", caddyVersion.stdout.trim());
37
37
 
38
38
  const hostsVersion = await $`hosts --version`.nothrow();
39
39
 
@@ -51,7 +51,7 @@ export function createSetupCommand({ binDir, installDir, caddyfilePath }: Contex
51
51
  return createCommand("setup")
52
52
  .description("setup config files")
53
53
  .option("--verbose", "verbose mode, show background output", false)
54
- .action(async (options: CommandOptions) => {
54
+ .action(async function setupAction(options: CommandOptions) {
55
55
  debug("setup command options %o", options);
56
56
 
57
57
  await checkInternalTools();
@@ -80,13 +80,9 @@ export function createSetupCommand({ binDir, installDir, caddyfilePath }: Contex
80
80
  const localDomains = caddyService.getLocalDomains();
81
81
  const hosts = localDomains.map((d) => d.host);
82
82
 
83
- const password = await passwordPrompt({
84
- message: "sudo password to manage hosts",
85
- });
86
-
87
83
  const hostsService = new HostsService(hosts);
88
- await hostsService.setup({ ...options, password });
84
+ await hostsService.setup(options);
89
85
 
90
- logger.success("localproxy setup completed");
86
+ logger.success("Setup completed!");
91
87
  });
92
88
  }
@@ -11,10 +11,10 @@ export function createStartCommand({ caddyfilePath }: Context) {
11
11
  return createCommand("start")
12
12
  .description("start caddy server")
13
13
  .option("--verbose", "verbose mode, show background output", false)
14
- .action(async (options: CommandOptions) => {
14
+ .action(async function startAction(options: CommandOptions) {
15
15
  const caddyService = new CaddyService(caddyfilePath);
16
16
  await caddyService.start(options);
17
17
 
18
- logger.success("localproxy start completed");
18
+ logger.success("Start completed!");
19
19
  });
20
20
  }
@@ -7,14 +7,13 @@ import type { Context } from "~/types";
7
7
  export function createStatusCommand({ caddyfilePath }: Context) {
8
8
  return createCommand("status")
9
9
  .description("show configured localhosts")
10
- .option("--verbose", "verbose mode, show background output", false)
11
- .action(async () => {
10
+ .action(async function statusAction() {
12
11
  const caddyService = new CaddyService(caddyfilePath);
13
12
 
14
13
  if (await caddyService.isRunning()) {
15
14
  logger.info("Caddy is running");
16
15
  } else {
17
- logger.warn("Caddy is not running");
16
+ logger.warn("Caddy is not running. Use `localp start` to start it.");
18
17
  }
19
18
 
20
19
  const localDomains = caddyService.getLocalDomains();
@@ -27,9 +26,9 @@ export function createStatusCommand({ caddyfilePath }: Context) {
27
26
  const found = await hostsService.findHost(host);
28
27
 
29
28
  if (found) {
30
- logger.success("%s is configured -> :%s", host, port);
29
+ logger.success("`%s` is configured -> :%s", host, port);
31
30
  } else {
32
- logger.warn("%s is not configured -> :%s", host, port);
31
+ logger.warn("`%s` is not configured -> :%s", host, port);
33
32
  }
34
33
  }
35
34
  });
@@ -11,10 +11,10 @@ export function createStopCommand({ caddyfilePath }: Context) {
11
11
  return createCommand("stop")
12
12
  .description("stop caddy server")
13
13
  .option("--verbose", "verbose mode, show background output", false)
14
- .action(async (options: CommandOptions) => {
14
+ .action(async function stopAction(options: CommandOptions) {
15
15
  const caddyService = new CaddyService(caddyfilePath);
16
16
  await caddyService.stop(options);
17
17
 
18
- logger.success("localproxy stop completed");
18
+ logger.success("Stop completed!");
19
19
  });
20
20
  }
@@ -44,6 +44,10 @@ export class CaddyService {
44
44
  return verbose ? verboseShell : silentShell;
45
45
  }
46
46
 
47
+ #getPID() {
48
+ return this.#hasCaddyPid() ? fs.readFileSync(this.#pidFilePath, "utf-8").trim() : null;
49
+ }
50
+
47
51
  async reboot(options: ExecOption) {
48
52
  const isRunning = await this.isRunning();
49
53
  if (isRunning) {
@@ -53,6 +57,11 @@ export class CaddyService {
53
57
  }
54
58
 
55
59
  async start({ verbose }: ExecOption) {
60
+ if (await this.isRunning()) {
61
+ logger.warn("Caddy is already running. PID: %d", this.#getPID());
62
+ return;
63
+ }
64
+
56
65
  const { $ } = this.#shell({ verbose });
57
66
 
58
67
  try {
@@ -88,14 +97,14 @@ export class CaddyService {
88
97
  return false;
89
98
  }
90
99
 
91
- const pid = (await quietShell.$`cat ${this.#pidFilePath}`.text()).trim();
100
+ const pid = this.#getPID();
92
101
 
93
- debug("Caddy PID: %d", pid);
102
+ debug("caddy PID: %d", pid);
94
103
 
95
104
  const { exitCode } = await quietShell.$`kill -0 ${pid}`.nothrow();
96
105
  const isRunning = exitCode === 0;
97
106
 
98
- debug("Caddy is %s", isRunning ? "running" : "stopped");
107
+ debug("caddy is %s", isRunning ? "running" : "stopped");
99
108
 
100
109
  return isRunning;
101
110
  }
@@ -1,50 +1,44 @@
1
1
  import { logger } from "./logger";
2
2
  import { quietShell, silentShell, verboseShell } from "./shell";
3
+ import { SudoService } from "./sudo";
3
4
 
4
5
  type SetupOptions = {
5
6
  verbose: boolean;
6
- password: string;
7
7
  };
8
8
 
9
9
  const debug = logger.subdebug("hosts");
10
10
 
11
11
  export class HostsService {
12
12
  #hosts: string[];
13
+ #sudo: SudoService;
13
14
 
14
15
  constructor(domains: string[]) {
15
16
  this.#hosts = domains;
17
+ this.#sudo = new SudoService();
16
18
  }
17
19
 
18
20
  #shell({ verbose }: SetupOptions) {
19
21
  return verbose ? verboseShell : silentShell;
20
22
  }
21
23
 
22
- async #auth(password: string) {
23
- if (!password) {
24
- throw new Error("Password is required");
25
- }
24
+ async setup(options: SetupOptions) {
25
+ logger.start("Setting up hosts");
26
26
 
27
- // Asking sudo directly in JS is unreliable,
28
- // so we do it through a shell command
29
- await silentShell.child({
30
- stdio: ["ignore", "ignore", "pipe"],
31
- }).$`echo "${password}" | sudo -S -v`;
32
- }
27
+ await this.#sudo.auth();
33
28
 
34
- async setup(options: SetupOptions) {
35
- await this.#auth(options.password);
29
+ const { $ } = this.#shell(options);
36
30
 
37
- const { stdout } = await verboseShell.$`sudo hosts backups create`;
38
- const backupPath = stdout.match(/(\/[^\s]+)/)?.[1];
39
- debug("Backup created at %s", backupPath);
31
+ await $`sudo hosts backups create`;
40
32
 
41
33
  for (const host of this.#hosts) {
42
34
  await this.addHost(host, options);
43
35
  }
36
+
37
+ logger.success("Hosts ready");
44
38
  }
45
39
 
46
40
  async clean(options: SetupOptions) {
47
- await this.#auth(options.password);
41
+ await this.#sudo.auth();
48
42
 
49
43
  for (const host of this.#hosts) {
50
44
  await this.removeHost(host, options);
@@ -53,8 +47,9 @@ export class HostsService {
53
47
 
54
48
  async findHost(host: string) {
55
49
  const { exitCode } = await quietShell.$`hosts show "${host}"`.nothrow();
56
- debug("Host %s is %s", host, !exitCode ? "present" : "absent");
57
- return !exitCode;
50
+ const found = exitCode === 0;
51
+ debug("Host %s is %s", host, found ? "present" : "absent");
52
+ return found;
58
53
  }
59
54
 
60
55
  async addHost(host: string, options: SetupOptions) {
@@ -0,0 +1,58 @@
1
+ import { password as passwordPrompt } from "@inquirer/prompts";
2
+ import type { ShellService } from "@vlandoss/clibuddy";
3
+ import { logger } from "./logger";
4
+ import { silentShell } from "./shell";
5
+
6
+ const debug = logger.subdebug("sudo");
7
+
8
+ export class SudoService {
9
+ #shell: ShellService;
10
+
11
+ constructor() {
12
+ this.#shell = silentShell.child({
13
+ stdio: ["ignore", "ignore", "pipe"],
14
+ });
15
+ }
16
+
17
+ async auth() {
18
+ if (!(await this.isAuthorized())) {
19
+ await this.authenticate();
20
+ }
21
+ }
22
+
23
+ async isAuthorized() {
24
+ const { exitCode } = await this.#shell.$`sudo -v -n`.nothrow();
25
+ return exitCode === 0;
26
+ }
27
+
28
+ async authenticate() {
29
+ let intent = 1;
30
+ let exitCode = null;
31
+
32
+ await passwordPrompt({
33
+ message: "Enter sudo password to manage hosts",
34
+ mask: true,
35
+ validate: async (value) => {
36
+ debug("Attempting sudo authentication %o", { intent });
37
+
38
+ const output = await this.#shell.$`echo "${value}" | sudo -S -v`.nothrow();
39
+ exitCode = output.exitCode;
40
+
41
+ debug("Sudo authentication exitCode(%d)", exitCode);
42
+
43
+ if (intent >= 3) {
44
+ return true;
45
+ }
46
+
47
+ intent++;
48
+
49
+ return output.exitCode === 0 ? true : "Invalid password";
50
+ },
51
+ });
52
+
53
+ if (exitCode !== 0) {
54
+ logger.error("`sudo` authentication failed");
55
+ process.exit(1);
56
+ }
57
+ }
58
+ }