@youcan/cli-kit 2.4.0 → 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,7 +3,7 @@ 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>;
6
+ tunnel(port: number, host?: string, signal?: AbortSignal): Promise<void>;
7
7
  private install;
8
8
  private composeTunnelingCommand;
9
9
  private exec;
@@ -163,10 +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
167
  await this.install();
168
168
  const { bin, args } = this.composeTunnelingCommand(port, host);
169
- this.exec(bin, args);
169
+ this.exec(bin, args, 3, signal);
170
170
  }
171
171
  async install() {
172
172
  if (await isExecutable(this.bin)) {
@@ -181,7 +181,7 @@ class Cloudflared {
181
181
  args: ['tunnel', `--url=${host}:${port}`, '--no-autoupdate'],
182
182
  };
183
183
  }
184
- async exec(bin, args, maxRetries = 3) {
184
+ async exec(bin, args, maxRetries = 3, signal) {
185
185
  if (this.getUrl()) {
186
186
  return;
187
187
  }
@@ -192,8 +192,9 @@ class Cloudflared {
192
192
  await exec(bin, args, {
193
193
  // Weird choice of cloudflared to write to stderr.
194
194
  stderr: this.output,
195
+ signal,
195
196
  errorHandler: async () => {
196
- await this.exec(bin, args, maxRetries - 1);
197
+ await this.exec(bin, args, maxRetries - 1, signal);
197
198
  },
198
199
  });
199
200
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -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.4.0",
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,6 +60,8 @@
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
66
  "type-check": "tsc --noEmit"
65
67
  }