@youcan/cli-kit 2.3.3 → 2.4.1

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.
@@ -73,7 +73,7 @@ async function authorize(command, state = randomHex(30)) {
73
73
  code_challenge: challenge,
74
74
  code_challenge_method: 'S256',
75
75
  };
76
- await command.output.anykey('Press any key to open the login page on your browser..');
76
+ await command.output.anykey('Press any key to open the login page on your browser');
77
77
  const url = `http://${AUTHORIZATION_URL}/admin/oauth/authorize?${new URLSearchParams(params).toString()}`;
78
78
  open(url);
79
79
  const result = await listen(command, LS_HOST, LS_PORT, url);
@@ -1,5 +1,4 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
2
  import type { ExecaError } from 'execa';
4
3
  import type { Readable, Writable } from 'node:stream';
5
4
  export interface ExecOptions {
@@ -3,6 +3,7 @@ import { Writable } from 'node:stream';
3
3
  export interface Interface {
4
4
  run: () => Promise<void>;
5
5
  boot: () => Promise<void>;
6
+ cleanup: () => Promise<void>;
6
7
  }
7
8
  export declare class Logger extends Writable {
8
9
  private readonly type;
@@ -13,4 +14,5 @@ export declare class Logger extends Writable {
13
14
  export declare abstract class Abstract implements Interface {
14
15
  abstract run(): Promise<void>;
15
16
  abstract boot(): Promise<void>;
17
+ cleanup(): Promise<void>;
16
18
  }
@@ -46,6 +46,8 @@ class Logger extends Writable {
46
46
  }
47
47
  }
48
48
  class Abstract {
49
+ async cleanup() {
50
+ }
49
51
  }
50
52
 
51
53
  export { Abstract, Logger };
@@ -3,8 +3,8 @@ export declare class Cloudflared {
3
3
  private readonly system;
4
4
  private readonly output;
5
5
  constructor();
6
- tunnel(port: number, host?: string): Promise<void>;
7
- install(): Promise<void>;
6
+ tunnel(port: number, host?: string, signal?: AbortSignal): Promise<void>;
7
+ private install;
8
8
  private composeTunnelingCommand;
9
9
  private exec;
10
10
  getUrl(): string | null;
@@ -163,9 +163,10 @@ class Cloudflared {
163
163
  this.bin = resolveBinaryPath(platform);
164
164
  this.system = { platform, arch };
165
165
  }
166
- async tunnel(port, host = 'localhost') {
166
+ async tunnel(port, host = 'localhost', signal) {
167
+ await this.install();
167
168
  const { bin, args } = this.composeTunnelingCommand(port, host);
168
- this.exec(bin, args);
169
+ this.exec(bin, args, 3, signal);
169
170
  }
170
171
  async install() {
171
172
  if (await isExecutable(this.bin)) {
@@ -180,7 +181,7 @@ class Cloudflared {
180
181
  args: ['tunnel', `--url=${host}:${port}`, '--no-autoupdate'],
181
182
  };
182
183
  }
183
- async exec(bin, args, maxRetries = 3) {
184
+ async exec(bin, args, maxRetries = 3, signal) {
184
185
  if (this.getUrl()) {
185
186
  return;
186
187
  }
@@ -191,8 +192,9 @@ class Cloudflared {
191
192
  await exec(bin, args, {
192
193
  // Weird choice of cloudflared to write to stderr.
193
194
  stderr: this.output,
195
+ signal,
194
196
  errorHandler: async () => {
195
- await this.exec(bin, args, maxRetries - 1);
197
+ await this.exec(bin, args, maxRetries - 1, signal);
196
198
  },
197
199
  });
198
200
  }
@@ -0,0 +1,114 @@
1
+ import { vi, describe, beforeEach, afterAll, it, expect } from 'vitest';
2
+ import 'change-case';
3
+ import '../node/cli.js';
4
+ import 'conf';
5
+ import 'env-paths';
6
+ import { isExecutable } from '../node/filesystem.js';
7
+ import 'formdata-node';
8
+ import 'formdata-node/file-from-path';
9
+ import 'simple-git';
10
+ import { exec } from '../node/system.js';
11
+ import 'node-fetch';
12
+ import 'ramda';
13
+ import 'kleur';
14
+ import 'dayjs';
15
+ import { Cloudflared } from './cloudflared.js';
16
+ import '../ui/components/DevOutput.js';
17
+ import 'react';
18
+ import 'ink';
19
+
20
+ vi.mock('..', () => ({
21
+ System: {
22
+ exec: vi.fn(),
23
+ },
24
+ Filesystem: {
25
+ isExecutable: vi.fn(),
26
+ },
27
+ Path: {
28
+ join: vi.fn().mockReturnValue('/mock/path/cloudflared'),
29
+ },
30
+ }));
31
+ const originalPlatform = process.platform;
32
+ const originalArch = process.arch;
33
+ describe('cloudflared', () => {
34
+ let cloudflared;
35
+ let mockAbortController;
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ Object.defineProperty(process, 'platform', {
39
+ value: 'darwin',
40
+ configurable: true,
41
+ });
42
+ Object.defineProperty(process, 'arch', {
43
+ value: 'arm64',
44
+ configurable: true,
45
+ });
46
+ vi.mocked(isExecutable).mockResolvedValue(true);
47
+ mockAbortController = new AbortController();
48
+ cloudflared = new Cloudflared();
49
+ });
50
+ afterAll(() => {
51
+ Object.defineProperty(process, 'platform', {
52
+ value: originalPlatform,
53
+ configurable: true,
54
+ });
55
+ Object.defineProperty(process, 'arch', {
56
+ value: originalArch,
57
+ configurable: true,
58
+ });
59
+ });
60
+ describe('tunnel', () => {
61
+ it('should pass abort signal to System.exec', async () => {
62
+ vi.mocked(exec).mockResolvedValue();
63
+ await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal);
64
+ expect(exec).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
65
+ signal: mockAbortController.signal,
66
+ }));
67
+ });
68
+ it('should use correct cloudflared arguments', async () => {
69
+ vi.mocked(exec).mockResolvedValue();
70
+ await cloudflared.tunnel(3001, 'localhost', mockAbortController.signal);
71
+ expect(exec).toHaveBeenCalledWith(expect.any(String), ['tunnel', '--url=localhost:3001', '--no-autoupdate'], expect.any(Object));
72
+ });
73
+ it('should work without abort signal', async () => {
74
+ vi.mocked(exec).mockResolvedValue();
75
+ await expect(cloudflared.tunnel(3000)).resolves.toBeUndefined();
76
+ expect(exec).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
77
+ signal: undefined,
78
+ }));
79
+ });
80
+ it('should use default host when not provided', async () => {
81
+ vi.mocked(exec).mockResolvedValue();
82
+ await cloudflared.tunnel(3000, undefined, mockAbortController.signal);
83
+ expect(exec).toHaveBeenCalledWith(expect.any(String), ['tunnel', '--url=localhost:3000', '--no-autoupdate'], expect.any(Object));
84
+ });
85
+ it('should check if cloudflared is executable before running', async () => {
86
+ vi.mocked(exec).mockResolvedValue();
87
+ await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal);
88
+ expect(isExecutable).toHaveBeenCalledWith('/mock/path/cloudflared');
89
+ });
90
+ });
91
+ describe('exec retry logic with signal', () => {
92
+ it('should retry with the same signal on failure', async () => {
93
+ let callCount = 0;
94
+ vi.mocked(exec).mockImplementation(async (bin, args, options) => {
95
+ callCount++;
96
+ if (callCount < 2) {
97
+ if (options?.errorHandler) {
98
+ await options.errorHandler(new Error('Connection failed'));
99
+ }
100
+ }
101
+ });
102
+ await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal);
103
+ expect(exec).toHaveBeenCalledTimes(2);
104
+ expect(exec).toHaveBeenNthCalledWith(1, expect.any(String), expect.any(Array), expect.objectContaining({ signal: mockAbortController.signal }));
105
+ expect(exec).toHaveBeenNthCalledWith(2, expect.any(String), expect.any(Array), expect.objectContaining({ signal: mockAbortController.signal }));
106
+ });
107
+ it('should handle errors without crashing', async () => {
108
+ vi.mocked(exec).mockResolvedValue();
109
+ await expect(cloudflared.tunnel(3000, 'localhost', mockAbortController.signal))
110
+ .resolves
111
+ .toBeUndefined();
112
+ });
113
+ });
114
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/cli-kit",
3
3
  "type": "module",
4
- "version": "2.3.3",
4
+ "version": "2.4.1",
5
5
  "description": "Utilities for the YouCan CLI",
6
6
  "author": "YouCan <contact@youcan.shop> (https://youcan.shop)",
7
7
  "license": "MIT",
@@ -60,8 +60,9 @@
60
60
  "scripts": {
61
61
  "build": "shx rm -rf dist && tsc --noEmit && rollup --config rollup.config.js",
62
62
  "dev": "shx rm -rf dist && rollup --config rollup.config.js --watch",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
63
65
  "release": "pnpm publish --access public",
64
- "type-check": "tsc --noEmit",
65
- "postinstall": "node -e \"import('fs').then(fs => { if (fs.existsSync('./dist/scripts/install-cloudflared.js')) import('./dist/scripts/install-cloudflared.js'); });\""
66
+ "type-check": "tsc --noEmit"
66
67
  }
67
68
  }
@@ -1,25 +0,0 @@
1
- import 'change-case';
2
- import '../node/cli.js';
3
- import 'conf';
4
- import 'env-paths';
5
- import '../node/filesystem.js';
6
- import 'formdata-node';
7
- import 'formdata-node/file-from-path';
8
- import 'simple-git';
9
- import 'execa';
10
- import 'find-process';
11
- import 'get-port';
12
- import 'tcp-port-used';
13
- import 'node-fetch';
14
- import 'ramda';
15
- import 'kleur';
16
- import 'dayjs';
17
- import { Cloudflared } from '../services/cloudflared.js';
18
- import '../ui/components/DevOutput.js';
19
- import 'react';
20
- import 'ink';
21
-
22
- const cloudflared = new Cloudflared();
23
- console.log('Installing Cloudflared...');
24
- await cloudflared.install();
25
- console.log('Successfully Installed Cloudflared');