@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 +3 -3
- package/src/commands/clean.ts +3 -8
- package/src/commands/setup.ts +5 -9
- 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 +12 -3
- package/src/services/hosts.ts +14 -19
- 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.2-git-
|
|
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/
|
|
30
|
-
"@vlandoss/
|
|
29
|
+
"@vlandoss/clibuddy": "0.0.5",
|
|
30
|
+
"@vlandoss/loggy": "0.0.5-git-62e4f21.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
package/src/commands/clean.ts
CHANGED
|
@@ -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(
|
|
23
|
+
await hostsService.clean(options);
|
|
29
24
|
|
|
30
|
-
logger.success("
|
|
25
|
+
logger.success("Clean completed!");
|
|
31
26
|
});
|
|
32
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 as editorPrompt
|
|
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();
|
|
@@ -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(
|
|
84
|
+
await hostsService.setup(options);
|
|
89
85
|
|
|
90
|
-
logger.success("
|
|
86
|
+
logger.success("Setup completed!");
|
|
91
87
|
});
|
|
92
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,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 =
|
|
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,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
|
|
23
|
-
|
|
24
|
-
throw new Error("Password is required");
|
|
25
|
-
}
|
|
24
|
+
async setup(options: SetupOptions) {
|
|
25
|
+
logger.start("Setting up hosts");
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
await this.#auth(options.password);
|
|
29
|
+
const { $ } = this.#shell(options);
|
|
36
30
|
|
|
37
|
-
|
|
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(
|
|
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
|
-
|
|
57
|
-
|
|
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
|
+
}
|