@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 +2 -2
- package/src/commands/clean.ts +2 -2
- package/src/commands/setup.ts +5 -5
- package/src/commands/start.ts +2 -2
- package/src/commands/status.ts +4 -5
- package/src/commands/stop.ts +2 -2
- package/src/services/caddy.ts +16 -7
- package/src/services/hosts.ts +22 -8
- package/src/services/shell.ts +2 -2
- package/src/services/sudo.ts +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vlandoss/localproxy",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
30
|
+
"@vlandoss/loggy": "0.0.5-git-cd6be95.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
package/src/commands/clean.ts
CHANGED
|
@@ -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("
|
|
25
|
+
logger.success("Clean completed!");
|
|
26
26
|
});
|
|
27
27
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -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("
|
|
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
|
|
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("
|
|
86
|
+
logger.success("Setup completed!");
|
|
87
87
|
});
|
|
88
88
|
}
|
package/src/commands/start.ts
CHANGED
|
@@ -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("
|
|
18
|
+
logger.success("Start completed!");
|
|
19
19
|
});
|
|
20
20
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
-
.
|
|
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("
|
|
29
|
+
logger.success("`%s` is configured -> :%s", host, port);
|
|
31
30
|
} else {
|
|
32
|
-
logger.warn("
|
|
31
|
+
logger.warn("`%s` is not configured -> :%s", host, port);
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
});
|
package/src/commands/stop.ts
CHANGED
|
@@ -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("
|
|
18
|
+
logger.success("Stop completed!");
|
|
19
19
|
});
|
|
20
20
|
}
|
package/src/services/caddy.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
logger.start("Stopping Caddy");
|
|
75
84
|
|
|
76
85
|
await $`caddy stop -c ${this.#configPath}`;
|
|
77
86
|
this.#deleteCaddyPid();
|
|
78
87
|
|
|
79
|
-
|
|
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 =
|
|
100
|
+
const pid = this.#getPID();
|
|
92
101
|
|
|
93
|
-
debug("
|
|
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("
|
|
107
|
+
debug("caddy is %s", isRunning ? "running" : "stopped");
|
|
99
108
|
|
|
100
109
|
return isRunning;
|
|
101
110
|
}
|
package/src/services/hosts.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
58
|
+
const found = await this.findHost(host);
|
|
45
59
|
|
|
46
|
-
if (!
|
|
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
|
|
68
|
+
const found = await this.findHost(host);
|
|
55
69
|
|
|
56
|
-
if (!
|
|
70
|
+
if (!found) {
|
|
57
71
|
await $`sudo hosts remove ${host}`;
|
|
58
72
|
}
|
|
59
73
|
}
|
package/src/services/shell.ts
CHANGED
|
@@ -6,11 +6,11 @@ export const quietShell = createShellService({
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
export const silentShell = quietShell.child({
|
|
9
|
-
stdio:
|
|
9
|
+
stdio: "ignore",
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export const verboseShell = quietShell.child({
|
|
13
13
|
quiet: false,
|
|
14
14
|
verbose: true,
|
|
15
|
-
stdio:
|
|
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
|
+
}
|