@vlandoss/localproxy 0.0.1 → 0.0.2-git-cd6be95.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.1",
3
+ "version": "0.0.2-git-cd6be95.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": {
@@ -27,7 +27,7 @@
27
27
  "@inquirer/prompts": "^7.8.4",
28
28
  "commander": "13.1.0",
29
29
  "@vlandoss/clibuddy": "0.0.5",
30
- "@vlandoss/loggy": "0.0.4"
30
+ "@vlandoss/loggy": "0.0.5-git-cd6be95.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -12,7 +12,7 @@ export function createCleanCommand({ caddyfilePath }: Context) {
12
12
  return createCommand("clean")
13
13
  .description("clean up config files")
14
14
  .option("--verbose", "verbose mode, show background output", false)
15
- .action(async (options: CommandOptions) => {
15
+ .action(async function cleanAction(options: CommandOptions) {
16
16
  const caddyService = new CaddyService(caddyfilePath);
17
17
  await caddyService.stop(options);
18
18
 
@@ -22,6 +22,6 @@ export function createCleanCommand({ caddyfilePath }: Context) {
22
22
  const hostsService = new HostsService(hosts);
23
23
  await hostsService.clean(options);
24
24
 
25
- logger.success("localproxy clean completed");
25
+ logger.success("Clean completed!");
26
26
  });
27
27
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { editor } 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();
@@ -65,7 +65,7 @@ export function createSetupCommand({ binDir, installDir, caddyfilePath }: Contex
65
65
  ? await fs.readFile(caddyfilePath, "utf-8")
66
66
  : await fs.readFile(exampleCaddyFilePath, "utf-8");
67
67
 
68
- const fileContent = await editor({
68
+ const fileContent = await editorPrompt({
69
69
  message: "Caddyfile",
70
70
  default: defaultContent,
71
71
  });
@@ -83,6 +83,6 @@ export function createSetupCommand({ binDir, installDir, caddyfilePath }: Contex
83
83
  const hostsService = new HostsService(hosts);
84
84
  await hostsService.setup(options);
85
85
 
86
- logger.success("localproxy setup completed");
86
+ logger.success("Setup completed!");
87
87
  });
88
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,14 +57,19 @@ 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 {
59
- debug("Starting Caddy...");
68
+ logger.start("Starting Caddy");
60
69
 
61
70
  await $`caddy start -c ${this.#configPath} --pidfile ${this.#pidFilePath} > /dev/null 2>&1`;
62
71
 
63
- debug("Caddy started");
72
+ logger.success("Caddy started");
64
73
  } catch {
65
74
  logger.error("Can't start Caddy");
66
75
  process.exit(1);
@@ -71,12 +80,12 @@ export class CaddyService {
71
80
  const { $ } = this.#shell({ verbose });
72
81
 
73
82
  try {
74
- debug("Stopping Caddy...");
83
+ logger.start("Stopping Caddy");
75
84
 
76
85
  await $`caddy stop -c ${this.#configPath}`;
77
86
  this.#deleteCaddyPid();
78
87
 
79
- debug("Caddy stopped");
88
+ logger.success("Caddy stopped");
80
89
  } catch {
81
90
  logger.error("Can't stop Caddy");
82
91
  process.exit(1);
@@ -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,5 +1,6 @@
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;
@@ -9,9 +10,11 @@ const debug = logger.subdebug("hosts");
9
10
 
10
11
  export class HostsService {
11
12
  #hosts: string[];
13
+ #sudo: SudoService;
12
14
 
13
15
  constructor(domains: string[]) {
14
16
  this.#hosts = domains;
17
+ this.#sudo = new SudoService();
15
18
  }
16
19
 
17
20
  #shell({ verbose }: SetupOptions) {
@@ -19,31 +22,42 @@ export class HostsService {
19
22
  }
20
23
 
21
24
  async setup(options: SetupOptions) {
22
- await verboseShell.$`sudo hosts backups create`;
25
+ logger.start("Setting up hosts");
26
+
27
+ await this.#sudo.auth();
28
+
29
+ const { $ } = this.#shell(options);
30
+
31
+ await $`sudo hosts backups create`;
23
32
 
24
33
  for (const host of this.#hosts) {
25
34
  await this.addHost(host, options);
26
35
  }
36
+
37
+ logger.success("Hosts ready");
27
38
  }
28
39
 
29
40
  async clean(options: SetupOptions) {
41
+ await this.#sudo.auth();
42
+
30
43
  for (const host of this.#hosts) {
31
44
  await this.removeHost(host, options);
32
45
  }
33
46
  }
34
47
 
35
48
  async findHost(host: string) {
36
- const currentHost = await quietShell.$`hosts show "${host}"`.text();
37
- debug("Host %s is %s", host, currentHost ? "present" : "absent");
38
- return currentHost;
49
+ const { exitCode } = await quietShell.$`hosts show "${host}"`.nothrow();
50
+ const found = exitCode === 0;
51
+ debug("Host %s is %s", host, found ? "present" : "absent");
52
+ return found;
39
53
  }
40
54
 
41
55
  async addHost(host: string, options: SetupOptions) {
42
56
  const { $ } = this.#shell(options);
43
57
 
44
- const currentHost = await this.findHost(host);
58
+ const found = await this.findHost(host);
45
59
 
46
- if (!currentHost) {
60
+ if (!found) {
47
61
  await $`sudo hosts add 127.0.0.1 ${host}`;
48
62
  }
49
63
  }
@@ -51,9 +65,9 @@ export class HostsService {
51
65
  async removeHost(host: string, options: SetupOptions) {
52
66
  const { $ } = this.#shell(options);
53
67
 
54
- const currentHost = await this.findHost(host);
68
+ const found = await this.findHost(host);
55
69
 
56
- if (!currentHost) {
70
+ if (!found) {
57
71
  await $`sudo hosts remove ${host}`;
58
72
  }
59
73
  }
@@ -6,11 +6,11 @@ export const quietShell = createShellService({
6
6
  });
7
7
 
8
8
  export const silentShell = quietShell.child({
9
- stdio: ["ignore", "ignore", "ignore"],
9
+ stdio: "ignore",
10
10
  });
11
11
 
12
12
  export const verboseShell = quietShell.child({
13
13
  quiet: false,
14
14
  verbose: true,
15
- stdio: ["inherit", "inherit", "inherit"],
15
+ stdio: "inherit",
16
16
  });
@@ -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
+ }