@fedify/cli 2.0.0-pr.458.1788 → 2.0.0-pr.458.1796

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/cli",
3
- "version": "2.0.0-pr.458.1788+db30ba33",
3
+ "version": "2.0.0-pr.458.1796+f091195d",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "imports": {
package/dist/deno.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  //#region deno.json
5
5
  var name = "@fedify/cli";
6
- var version = "2.0.0-pr.458.1788+db30ba33";
6
+ var version = "2.0.0-pr.458.1796+f091195d";
7
7
  var license = "MIT";
8
8
  var exports = "./src/mod.ts";
9
9
  var imports = {
@@ -8,12 +8,12 @@ import { message } from "@optique/core/message";
8
8
  import process from "node:process";
9
9
 
10
10
  //#region src/generate-vocab/action.ts
11
- async function runGenerateVocab({ schemaPath, generatedPath }) {
12
- if (!(await stat(schemaPath)).isDirectory()) {
13
- printError(message`${schemaPath} is not a directory.`);
11
+ async function runGenerateVocab({ schemaDir, generatedPath }) {
12
+ if (!(await stat(schemaDir)).isDirectory()) {
13
+ printError(message`${schemaDir} is not a directory.`);
14
14
  process.exit(1);
15
15
  }
16
- await generateVocab(schemaPath, generatedPath);
16
+ await generateVocab(schemaDir, generatedPath);
17
17
  }
18
18
 
19
19
  //#endregion
@@ -5,13 +5,21 @@ import { command, constant, message, object, option } from "@optique/core";
5
5
  import { path } from "@optique/run";
6
6
 
7
7
  //#region src/generate-vocab/command.ts
8
- const schemaPath = option("-i", "--input", path({ metavar: "SCHEMA FILES PATH" }));
9
- const generatedPath = option("-o", "--output", path({ metavar: "GENERATED CODE PATH" }));
8
+ const schemaDir = option("-i", "--input", path({
9
+ metavar: "DIR",
10
+ type: "directory",
11
+ mustExist: true
12
+ }));
13
+ const generatedPath = option("-o", "--output", path({
14
+ metavar: "PATH",
15
+ type: "file",
16
+ allowCreate: true
17
+ }));
10
18
  const generateVocabCommand = command("generate-vocab", object({
11
19
  command: constant("generate-vocab"),
12
- schemaPath,
20
+ schemaDir,
13
21
  generatedPath
14
- }), { description: message`Generate Vocabulary Classes from Schema Files` });
22
+ }), { description: message`Generate vocabulary classes from schema files.` });
15
23
  var command_default = generateVocabCommand;
16
24
 
17
25
  //#endregion
package/dist/mod.js CHANGED
@@ -30,7 +30,7 @@ async function main() {
30
30
  if (result.command === "webfinger") await runWebFinger(result);
31
31
  if (result.command === "inbox") runInbox(result);
32
32
  if (result.command === "nodeinfo") runNodeInfo(result);
33
- if (result.command === "tunnel") runTunnel(result);
33
+ if (result.command === "tunnel") await runTunnel(result);
34
34
  if (result.command === "generate-vocab") await runGenerateVocab(result);
35
35
  }
36
36
  await main();
package/dist/tunnel.js CHANGED
@@ -1,20 +1,54 @@
1
1
 
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
 
4
- import { debugOption } from "./globals.js";
5
- import { argument, command, constant, integer, merge, message, object } from "@optique/core";
4
+ import { configureLogging, debugOption } from "./globals.js";
5
+ import { argument, command, constant, integer, merge, message, object, option, optional } from "@optique/core";
6
+ import { print, printError } from "@optique/run";
7
+ import process from "node:process";
8
+ import ora from "ora";
9
+ import { openTunnel } from "@hongminhee/localtunnel";
10
+ import { choice as choice$1 } from "@optique/core/valueparser";
6
11
 
7
12
  //#region src/tunnel.ts
8
- const tunnelCommand = command("tunnel", merge(object({
9
- command: constant("tunnel"),
13
+ const tunnelCommand = command("tunnel", merge("Tunnel options", object({ command: constant("tunnel") }), object({
10
14
  port: argument(integer({
11
15
  metavar: "PORT",
12
16
  min: 0,
13
17
  max: 65535
14
- }))
15
- }), debugOption), { description: message`Expose a local HTTP server to the public internet using a secure tunnel.` });
16
- function runTunnel(command$1) {
17
- console.debug(command$1);
18
+ }), { description: message`The local port number to expose.` }),
19
+ service: optional(option("-s", "--service", choice$1([
20
+ "localhost.run",
21
+ "serveo.net",
22
+ "pinggy.io"
23
+ ]), { description: message`The localtunnel service to use.` }))
24
+ }), debugOption), { description: message`Expose a local HTTP server to the public internet using a secure tunnel.\nNote that the HTTP requests through the tunnel have X-Forwarded-* headers.` });
25
+ async function runTunnel(command$1, deps = {
26
+ openTunnel,
27
+ ora,
28
+ exit: process.exit
29
+ }) {
30
+ if (command$1.debug) await configureLogging();
31
+ const spinner = deps.ora({
32
+ text: "Creating a secure tunnel...",
33
+ discardStdin: false
34
+ }).start();
35
+ let tunnel;
36
+ try {
37
+ tunnel = await deps.openTunnel({
38
+ port: command$1.port,
39
+ service: command$1.service
40
+ });
41
+ } catch (error) {
42
+ if (command$1.debug) printError(message`${String(error)}`);
43
+ spinner.fail("Failed to create a secure tunnel.");
44
+ deps.exit(1);
45
+ }
46
+ spinner.succeed(`Your local server at ${command$1.port} is now publicly accessible:\n`);
47
+ print(message`${tunnel.url.href}`);
48
+ print(message`\nPress ^C to close the tunnel.`);
49
+ process.on("SIGINT", async () => {
50
+ await tunnel.close();
51
+ });
18
52
  }
19
53
 
20
54
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/cli",
3
- "version": "2.0.0-pr.458.1788+db30ba33",
3
+ "version": "2.0.0-pr.458.1796+f091195d",
4
4
  "description": "CLI toolchain for Fedify and debugging ActivityPub",
5
5
  "keywords": [
6
6
  "fedify",
@@ -71,9 +71,10 @@
71
71
  "ora": "^8.2.0",
72
72
  "shiki": "^1.6.4",
73
73
  "srvx": "^0.8.7",
74
- "@fedify/sqlite": "2.0.0-pr.458.1788+db30ba33",
75
- "@fedify/vocab-runtime": "2.0.0-pr.458.1788+db30ba33",
76
- "@fedify/fedify": "2.0.0-pr.458.1788+db30ba33"
74
+ "@fedify/fedify": "2.0.0-pr.458.1796+f091195d",
75
+ "@fedify/sqlite": "2.0.0-pr.458.1796+f091195d",
76
+ "@fedify/vocab-runtime": "2.0.0-pr.458.1796+f091195d",
77
+ "@fedify/vocab-tools": "2.0.0-pr.458.1796+f091195d"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@types/bun": "^1.2.23",
@@ -6,12 +6,12 @@ import process from "node:process";
6
6
  import type { GenerateVocabCommand } from "./command.ts";
7
7
 
8
8
  export default async function runGenerateVocab(
9
- { schemaPath, generatedPath }: GenerateVocabCommand,
9
+ { schemaDir, generatedPath }: GenerateVocabCommand,
10
10
  ) {
11
- if (!(await stat(schemaPath)).isDirectory()) {
12
- printError(message`${schemaPath} is not a directory.`);
11
+ if (!(await stat(schemaDir)).isDirectory()) {
12
+ printError(message`${schemaDir} is not a directory.`);
13
13
  process.exit(1);
14
14
  }
15
15
 
16
- await generateVocab(schemaPath, generatedPath);
16
+ await generateVocab(schemaDir, generatedPath);
17
17
  }
@@ -8,26 +8,26 @@ import {
8
8
  } from "@optique/core";
9
9
  import { path } from "@optique/run";
10
10
 
11
- const schemaPath = option(
11
+ const schemaDir = option(
12
12
  "-i",
13
13
  "--input",
14
- path({ metavar: "SCHEMA FILES PATH" }),
14
+ path({ metavar: "DIR", type: "directory", mustExist: true }),
15
15
  );
16
16
  const generatedPath = option(
17
17
  "-o",
18
18
  "--output",
19
- path({ metavar: "GENERATED CODE PATH" }),
19
+ path({ metavar: "PATH", type: "file", allowCreate: true }),
20
20
  );
21
21
 
22
22
  const generateVocabCommand = command(
23
23
  "generate-vocab",
24
24
  object({
25
25
  command: constant("generate-vocab"),
26
- schemaPath,
26
+ schemaDir,
27
27
  generatedPath,
28
28
  }),
29
29
  {
30
- description: message`Generate Vocabulary Classes from Schema Files`,
30
+ description: message`Generate vocabulary classes from schema files.`,
31
31
  },
32
32
  );
33
33
 
package/src/globals.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { message, object, option } from "@optique/core";
2
- import { configure, getConsoleSink } from "@logtape/logtape";
3
1
  import { getFileSink } from "@logtape/file";
4
- import { recordingSink } from "./log.ts";
2
+ import { configure, getConsoleSink } from "@logtape/logtape";
3
+ import { message, object, option } from "@optique/core";
5
4
  import { AsyncLocalStorage } from "node:async_hooks";
6
5
  import process from "node:process";
6
+ import { recordingSink } from "./log.ts";
7
7
 
8
8
  export const debugOption = object("Global options", {
9
9
  debug: option("-d", "--debug", {
package/src/mod.ts CHANGED
@@ -43,7 +43,7 @@ async function main() {
43
43
  runNodeInfo(result);
44
44
  }
45
45
  if (result.command === "tunnel") {
46
- runTunnel(result);
46
+ await runTunnel(result);
47
47
  }
48
48
  if (result.command === "generate-vocab") {
49
49
  await runGenerateVocab(result);
@@ -0,0 +1,157 @@
1
+ import type { Tunnel, TunnelOptions } from "@hongminhee/localtunnel";
2
+ import { run } from "@optique/run";
3
+ import { deepEqual, rejects } from "node:assert/strict";
4
+ import test from "node:test";
5
+ import type { Ora } from "ora";
6
+ import { runTunnel, tunnelCommand } from "./tunnel.ts";
7
+
8
+ test("tunnel command structure", () => {
9
+ const testCommandWithOptions = run(tunnelCommand, {
10
+ args: ["tunnel", "3001", "-s", "pinggy.io", "-d"],
11
+ });
12
+ const testCommandWithoutOptions = run(tunnelCommand, {
13
+ args: ["tunnel", "3000"],
14
+ });
15
+
16
+ deepEqual(testCommandWithOptions.command, "tunnel");
17
+ deepEqual(testCommandWithOptions.port, 3001);
18
+ deepEqual(testCommandWithOptions.service, "pinggy.io");
19
+ deepEqual(testCommandWithOptions.debug, true);
20
+
21
+ deepEqual(testCommandWithoutOptions.port, 3000);
22
+ deepEqual(testCommandWithoutOptions.service, undefined);
23
+ deepEqual(testCommandWithoutOptions.debug, false);
24
+ });
25
+
26
+ test("tunnel successfully creates and manages tunnel", async () => {
27
+ const mockCommand = {
28
+ command: "tunnel" as const,
29
+ port: 3001,
30
+ service: "pinggy.io" as const,
31
+ debug: true,
32
+ };
33
+
34
+ const mockTunnel: Tunnel = {
35
+ url: new URL("https://droar-218-152-125-59.a.free.pinggy.link/"),
36
+ localPort: 3001,
37
+ pid: 123,
38
+ close: () => Promise.resolve(),
39
+ };
40
+
41
+ let openTunnelCalled = false;
42
+ let openTunnelPort;
43
+ let openTunnelService;
44
+ let spinnerCalled = false;
45
+ let openTunnelSucceed = false;
46
+ let openTunnelFailed = false;
47
+ let spinnerMsg;
48
+
49
+ const mockDeps = {
50
+ openTunnel: (args: TunnelOptions) => {
51
+ openTunnelCalled = true;
52
+ openTunnelPort = args.port;
53
+ openTunnelService = args.service;
54
+ return Promise.resolve(mockTunnel);
55
+ },
56
+ ora: () =>
57
+ ({
58
+ start() {
59
+ spinnerCalled = true;
60
+ return this;
61
+ },
62
+ fail(msg: string) {
63
+ openTunnelFailed = true;
64
+ spinnerMsg = msg;
65
+ return this;
66
+ },
67
+ succeed(msg: string) {
68
+ openTunnelSucceed = true;
69
+ spinnerMsg = msg;
70
+ return this;
71
+ },
72
+ }) as unknown as Ora,
73
+ exit: (): never => {
74
+ throw new Error();
75
+ },
76
+ };
77
+
78
+ try {
79
+ await runTunnel(mockCommand, mockDeps);
80
+ } finally {
81
+ deepEqual(openTunnelCalled, true);
82
+ deepEqual(openTunnelPort, 3001);
83
+ deepEqual(openTunnelService, "pinggy.io");
84
+ deepEqual(openTunnelSucceed, true);
85
+ deepEqual(openTunnelFailed, false);
86
+ deepEqual(spinnerCalled, true);
87
+ deepEqual(
88
+ spinnerMsg,
89
+ `Your local server at ${mockTunnel.localPort} is now publicly accessible:\n`,
90
+ );
91
+ }
92
+ });
93
+
94
+ test("tunnel fails to create a secure tunnel and handles error", async () => {
95
+ const mockCommand = {
96
+ command: "tunnel" as const,
97
+ port: 3001,
98
+ service: undefined,
99
+ debug: false,
100
+ };
101
+
102
+ let openTunnelCalled = false;
103
+ let openTunnelPort;
104
+ let openTunnelService;
105
+ let spinnerCalled = false;
106
+ let openTunnelSucceed = false;
107
+ let openTunnelFailed = false;
108
+ let spinnerMsg;
109
+
110
+ const mockDeps = {
111
+ openTunnel: (args: TunnelOptions) => {
112
+ openTunnelCalled = true;
113
+ openTunnelPort = args.port;
114
+ openTunnelService = args.service;
115
+ return Promise.reject();
116
+ },
117
+ ora: () =>
118
+ ({
119
+ start() {
120
+ spinnerCalled = true;
121
+ return this;
122
+ },
123
+ fail(msg: string) {
124
+ openTunnelFailed = true;
125
+ spinnerMsg = msg;
126
+ return this;
127
+ },
128
+ succeed(msg: string) {
129
+ openTunnelSucceed = true;
130
+ spinnerMsg = msg;
131
+ return this;
132
+ },
133
+ }) as unknown as Ora,
134
+ exit: (): never => {
135
+ throw new Error("Process exit called");
136
+ },
137
+ };
138
+
139
+ try {
140
+ await rejects(
141
+ () => runTunnel(mockCommand, mockDeps),
142
+ Error,
143
+ "Process exit called",
144
+ );
145
+ } finally {
146
+ deepEqual(openTunnelCalled, true);
147
+ deepEqual(openTunnelPort, 3001);
148
+ deepEqual(openTunnelService, undefined);
149
+ deepEqual(openTunnelSucceed, false);
150
+ deepEqual(openTunnelFailed, true);
151
+ deepEqual(spinnerCalled, true);
152
+ deepEqual(
153
+ spinnerMsg,
154
+ "Failed to create a secure tunnel.",
155
+ );
156
+ }
157
+ });
package/src/tunnel.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { openTunnel, type Tunnel } from "@hongminhee/localtunnel";
1
2
  import {
2
3
  argument,
3
4
  command,
@@ -7,26 +8,83 @@ import {
7
8
  merge,
8
9
  message,
9
10
  object,
11
+ option,
12
+ optional,
10
13
  } from "@optique/core";
11
- import { debugOption } from "./globals.ts";
14
+ import { choice } from "@optique/core/valueparser";
15
+ import { print, printError } from "@optique/run";
16
+ import process from "node:process";
17
+ import ora from "ora";
18
+ import { configureLogging, debugOption } from "./globals.ts";
12
19
 
13
20
  export const tunnelCommand = command(
14
21
  "tunnel",
15
22
  merge(
23
+ "Tunnel options",
16
24
  object({
17
25
  command: constant("tunnel"),
18
- port: argument(integer({ metavar: "PORT", min: 0, max: 65_535 })),
26
+ }),
27
+ object({
28
+ port: argument(integer({ metavar: "PORT", min: 0, max: 65535 }), {
29
+ description: message`The local port number to expose.`,
30
+ }),
31
+ service: optional(
32
+ option(
33
+ "-s",
34
+ "--service",
35
+ choice(["localhost.run", "serveo.net", "pinggy.io"]),
36
+ {
37
+ description: message`The localtunnel service to use.`,
38
+ },
39
+ ),
40
+ ),
19
41
  }),
20
42
  debugOption,
21
43
  ),
22
44
  {
23
45
  description:
24
- message`Expose a local HTTP server to the public internet using a secure tunnel.`,
46
+ message`Expose a local HTTP server to the public internet using a secure tunnel.\nNote that the HTTP requests through the tunnel have X-Forwarded-* headers.`,
25
47
  },
26
48
  );
27
49
 
28
- export function runTunnel(
50
+ export async function runTunnel(
29
51
  command: InferValue<typeof tunnelCommand>,
52
+ deps: {
53
+ openTunnel: typeof openTunnel;
54
+ ora: typeof ora;
55
+ exit: typeof process.exit;
56
+ } = {
57
+ openTunnel,
58
+ ora,
59
+ exit: process.exit,
60
+ },
30
61
  ) {
31
- console.debug(command);
62
+ if (command.debug) {
63
+ await configureLogging();
64
+ }
65
+ const spinner = deps.ora({
66
+ text: "Creating a secure tunnel...",
67
+ discardStdin: false,
68
+ }).start();
69
+ let tunnel: Tunnel;
70
+ try {
71
+ tunnel = await deps.openTunnel({
72
+ port: command.port,
73
+ service: command.service,
74
+ });
75
+ } catch (error) {
76
+ if (command.debug) {
77
+ printError(message`${String(error)}`);
78
+ }
79
+ spinner.fail("Failed to create a secure tunnel.");
80
+ deps.exit(1);
81
+ }
82
+ spinner.succeed(
83
+ `Your local server at ${command.port} is now publicly accessible:\n`,
84
+ );
85
+ print(message`${tunnel.url.href}`);
86
+ print(message`\nPress ^C to close the tunnel.`);
87
+ process.on("SIGINT", async () => {
88
+ await tunnel.close();
89
+ });
32
90
  }