@youcan/cli-kit 2.4.0 → 2.4.3
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/dist/node/filesystem.d.ts +1 -0
- package/dist/node/filesystem.js +4 -1
- package/dist/node/session.js +1 -1
- package/dist/node/system.d.ts +0 -1
- package/dist/node/worker.d.ts +2 -0
- package/dist/node/worker.js +2 -0
- package/dist/services/cloudflared.d.ts +1 -1
- package/dist/services/cloudflared.js +20 -7
- package/dist/services/cloudflared.test.d.ts +1 -0
- package/dist/services/cloudflared.test.js +114 -0
- package/package.json +3 -1
|
@@ -29,6 +29,7 @@ export declare function archived(path: string, name: string, glob?: string): Pro
|
|
|
29
29
|
export declare function unlink(path: string): Promise<void>;
|
|
30
30
|
export declare function readdir(path: string): Promise<string[]>;
|
|
31
31
|
export declare function stat(path: string): Promise<Stats>;
|
|
32
|
+
export declare function chmod(path: string, mode: number): Promise<void>;
|
|
32
33
|
export declare const watch: typeof import("chokidar").watch;
|
|
33
34
|
export declare function decompressGzip(file: string, destination: string, mode?: number): Promise<void>;
|
|
34
35
|
export declare function extractTar(file: string, cwd: string, mode: number): Promise<void>;
|
package/dist/node/filesystem.js
CHANGED
|
@@ -121,6 +121,9 @@ async function readdir(path) {
|
|
|
121
121
|
async function stat(path) {
|
|
122
122
|
return await FilesystemPromises.stat(path);
|
|
123
123
|
}
|
|
124
|
+
async function chmod(path, mode) {
|
|
125
|
+
return await FilesystemPromises.chmod(path, mode);
|
|
126
|
+
}
|
|
124
127
|
const watch = chokidar.watch;
|
|
125
128
|
async function decompressGzip(file, destination, mode = 0o755) {
|
|
126
129
|
const unzip = createGunzip();
|
|
@@ -132,4 +135,4 @@ async function extractTar(file, cwd, mode) {
|
|
|
132
135
|
await tar.extract({ cwd, file, mode });
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
export { archived, decompressGzip, exists, extractTar, glob, isDirectory, isExecutable, mkdir, move, readFile, readJsonFile, readdir, rm, stat, tapIntoTmp, unlink, watch, writeFile, writeJsonFile };
|
|
138
|
+
export { archived, chmod, decompressGzip, exists, extractTar, glob, isDirectory, isExecutable, mkdir, move, readFile, readJsonFile, readdir, rm, stat, tapIntoTmp, unlink, watch, writeFile, writeJsonFile };
|
package/dist/node/session.js
CHANGED
|
@@ -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);
|
package/dist/node/system.d.ts
CHANGED
package/dist/node/worker.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/node/worker.js
CHANGED
|
@@ -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;
|
|
@@ -8,7 +8,7 @@ import 'change-case';
|
|
|
8
8
|
import '../node/cli.js';
|
|
9
9
|
import 'conf';
|
|
10
10
|
import 'env-paths';
|
|
11
|
-
import { isExecutable, tapIntoTmp, mkdir, decompressGzip, extractTar, move } from '../node/filesystem.js';
|
|
11
|
+
import { isExecutable, tapIntoTmp, mkdir, decompressGzip, extractTar, move, chmod } from '../node/filesystem.js';
|
|
12
12
|
import 'formdata-node';
|
|
13
13
|
import 'formdata-node/file-from-path';
|
|
14
14
|
import 'simple-git';
|
|
@@ -85,6 +85,7 @@ async function installForLinux(url, destination) {
|
|
|
85
85
|
const parentDir = dirname(destination);
|
|
86
86
|
await mkdir(parentDir);
|
|
87
87
|
await downloadFromRelease(url, destination);
|
|
88
|
+
await chmod(destination, 0o755);
|
|
88
89
|
}
|
|
89
90
|
async function installForWindows(url, destination) {
|
|
90
91
|
const parentDir = dirname(destination);
|
|
@@ -146,6 +147,9 @@ class OutputStream extends Writable {
|
|
|
146
147
|
clearBuffer() {
|
|
147
148
|
this.buffer = '';
|
|
148
149
|
}
|
|
150
|
+
getBuffer() {
|
|
151
|
+
return this.buffer;
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
154
|
class Cloudflared {
|
|
151
155
|
bin;
|
|
@@ -163,10 +167,10 @@ class Cloudflared {
|
|
|
163
167
|
this.bin = resolveBinaryPath(platform);
|
|
164
168
|
this.system = { platform, arch };
|
|
165
169
|
}
|
|
166
|
-
async tunnel(port, host = 'localhost') {
|
|
170
|
+
async tunnel(port, host = 'localhost', signal) {
|
|
167
171
|
await this.install();
|
|
168
172
|
const { bin, args } = this.composeTunnelingCommand(port, host);
|
|
169
|
-
this.exec(bin, args);
|
|
173
|
+
this.exec(bin, args, 3, signal);
|
|
170
174
|
}
|
|
171
175
|
async install() {
|
|
172
176
|
if (await isExecutable(this.bin)) {
|
|
@@ -174,6 +178,9 @@ class Cloudflared {
|
|
|
174
178
|
}
|
|
175
179
|
const downloadUrl = composeDownloadUrl(this.system.platform, this.system.arch);
|
|
176
180
|
await install(this.system.platform, downloadUrl, this.bin);
|
|
181
|
+
if (!(await isExecutable(this.bin))) {
|
|
182
|
+
throw new Error(`Failed to install executable cloudflared binary at ${this.bin}. Check file permissions and platform compatibility.`);
|
|
183
|
+
}
|
|
177
184
|
}
|
|
178
185
|
composeTunnelingCommand(port, host = 'localhost') {
|
|
179
186
|
return {
|
|
@@ -181,19 +188,25 @@ class Cloudflared {
|
|
|
181
188
|
args: ['tunnel', `--url=${host}:${port}`, '--no-autoupdate'],
|
|
182
189
|
};
|
|
183
190
|
}
|
|
184
|
-
async exec(bin, args, maxRetries = 3) {
|
|
191
|
+
async exec(bin, args, maxRetries = 3, signal) {
|
|
185
192
|
if (this.getUrl()) {
|
|
186
193
|
return;
|
|
187
194
|
}
|
|
188
195
|
if (maxRetries === 0) {
|
|
189
|
-
|
|
196
|
+
const extractedError = this.output.extractError();
|
|
197
|
+
const errorMessage = extractedError
|
|
198
|
+
? `cloudflared failed: ${extractedError}`
|
|
199
|
+
: `cloudflared failed for unknown reason. Binary: ${bin}, Args: ${args.join(' ')}, Buffer: ${this.output.getBuffer()}`;
|
|
200
|
+
throw new Error(errorMessage);
|
|
190
201
|
}
|
|
191
202
|
this.output.clearBuffer();
|
|
192
203
|
await exec(bin, args, {
|
|
193
204
|
// Weird choice of cloudflared to write to stderr.
|
|
194
205
|
stderr: this.output,
|
|
195
|
-
|
|
196
|
-
|
|
206
|
+
signal,
|
|
207
|
+
errorHandler: async (error) => {
|
|
208
|
+
console.error(`cloudflared execution error (retries left: ${maxRetries - 1}):`, error);
|
|
209
|
+
await this.exec(bin, args, maxRetries - 1, signal);
|
|
197
210
|
},
|
|
198
211
|
});
|
|
199
212
|
}
|
|
@@ -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.
|
|
4
|
+
"version": "2.4.3",
|
|
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
|
}
|