@testrigor/testrigor-cli 0.0.42-beta
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/README.md +33 -0
- package/bin/dev +18 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/dist/command-base.d.ts +27 -0
- package/dist/command-base.js +110 -0
- package/dist/commands/authenticate.d.ts +6 -0
- package/dist/commands/authenticate.js +59 -0
- package/dist/commands/test-suite/config.d.ts +14 -0
- package/dist/commands/test-suite/config.js +97 -0
- package/dist/commands/test-suite/run.d.ts +46 -0
- package/dist/commands/test-suite/run.js +728 -0
- package/dist/config/environments.d.ts +7 -0
- package/dist/config/environments.js +10 -0
- package/dist/flags/chrome-extension.d.ts +4 -0
- package/dist/flags/chrome-extension.js +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.js +2 -0
- package/dist/util/config.d.ts +33 -0
- package/dist/util/config.js +87 -0
- package/dist/util/parser.d.ts +4 -0
- package/dist/util/parser.js +83 -0
- package/dist/util/request-builder.d.ts +11 -0
- package/dist/util/request-builder.js +49 -0
- package/dist/util/trtc.d.ts +1 -0
- package/dist/util/trtc.js +109 -0
- package/dist/util/tunnel.d.ts +13 -0
- package/dist/util/tunnel.js +102 -0
- package/dist/util/utils.d.ts +16 -0
- package/dist/util/utils.js +97 -0
- package/oclif.manifest.json +265 -0
- package/package.json +98 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.environments = void 0;
|
|
4
|
+
const environments = {
|
|
5
|
+
prod: {
|
|
6
|
+
TESTRIGOR_WEB_URL: "https://app.testrigor.com",
|
|
7
|
+
TESTRIGOR_API_URL: "https://api2.testrigor.com",
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
exports.environments = environments;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChromeExtensionFlag = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const utils_1 = require("../util/utils");
|
|
8
|
+
let totalChromeExtensionSize = 0;
|
|
9
|
+
exports.ChromeExtensionFlag = core_1.Flags.custom({
|
|
10
|
+
description: 'Chrome extension: "name=EXT_NAME file=PATH_TO_CRX"',
|
|
11
|
+
multiple: true,
|
|
12
|
+
parse: async (input) => {
|
|
13
|
+
// The 'file=' acts as a delimiter for the name value
|
|
14
|
+
// Supports quoted paths: file="path with spaces.crx" or file='path.crx'
|
|
15
|
+
const fileMatch = input.match(/\bfile=(?:["'](.+?)["']|(.+))$/i);
|
|
16
|
+
const nameMatch = input.match(/^name=(.+?)\s+file=/i);
|
|
17
|
+
if (!nameMatch || !nameMatch[1]) {
|
|
18
|
+
throw new Error("--chrome-extension requires: name=EXT_NAME");
|
|
19
|
+
}
|
|
20
|
+
if (!fileMatch) {
|
|
21
|
+
throw new Error("--chrome-extension requires: file=PATH_TO_CRX");
|
|
22
|
+
}
|
|
23
|
+
const name = nameMatch[1].trim();
|
|
24
|
+
if (!name) {
|
|
25
|
+
throw new Error("--chrome-extension requires a non-empty name");
|
|
26
|
+
}
|
|
27
|
+
// Extract file path from either quoted (group 1) or unquoted (group 2) match
|
|
28
|
+
const file = (fileMatch[1] || fileMatch[2] || "").trim();
|
|
29
|
+
if (!file.toLowerCase().endsWith(".crx")) {
|
|
30
|
+
throw new Error(`File '${path.basename(file)}': invalid extension. Allowed: .crx`);
|
|
31
|
+
}
|
|
32
|
+
if (!fs.existsSync(file)) {
|
|
33
|
+
throw new Error(`Chrome extension file not found: ${file}`);
|
|
34
|
+
}
|
|
35
|
+
const fileSize = fs.statSync(file).size;
|
|
36
|
+
if (fileSize > utils_1.MAX_FILE_SIZE_100_MB) {
|
|
37
|
+
throw new Error(`File '${path.basename(file)}': size exceeds maximum allowed (100 MB)`);
|
|
38
|
+
}
|
|
39
|
+
totalChromeExtensionSize += fileSize;
|
|
40
|
+
if (totalChromeExtensionSize > utils_1.MAX_FILE_SIZE_100_MB) {
|
|
41
|
+
throw new Error("Chrome extensions combined size exceeds maximum allowed (100 MB)");
|
|
42
|
+
}
|
|
43
|
+
return { name, file: path.resolve(file) };
|
|
44
|
+
},
|
|
45
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from "@oclif/core";
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { ReadStream } from "fs";
|
|
3
|
+
export type RetestResponse = {
|
|
4
|
+
result: string;
|
|
5
|
+
msg: string;
|
|
6
|
+
errName?: string;
|
|
7
|
+
taskId?: string;
|
|
8
|
+
queueId?: string;
|
|
9
|
+
};
|
|
10
|
+
export type StatusResponse = {
|
|
11
|
+
detailsUrl?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
};
|
|
14
|
+
export type RetestSignedUrlApi2Request = {
|
|
15
|
+
fileName: string;
|
|
16
|
+
};
|
|
17
|
+
export type Api2Wrapper = {
|
|
18
|
+
status: number;
|
|
19
|
+
message: string;
|
|
20
|
+
errors?: string[];
|
|
21
|
+
path?: string;
|
|
22
|
+
data?: object;
|
|
23
|
+
};
|
|
24
|
+
export type RetestDataApi2Response = {
|
|
25
|
+
taskId?: string;
|
|
26
|
+
queueId?: string;
|
|
27
|
+
};
|
|
28
|
+
export type RetestApi2Response = Api2Wrapper & {
|
|
29
|
+
data: RetestDataApi2Response;
|
|
30
|
+
};
|
|
31
|
+
export type SignedUrlDataApi2Response = {
|
|
32
|
+
url: string;
|
|
33
|
+
fileName: string;
|
|
34
|
+
};
|
|
35
|
+
export type SignedUrlApi2Response = Api2Wrapper & {
|
|
36
|
+
data: SignedUrlDataApi2Response;
|
|
37
|
+
};
|
|
38
|
+
export type FileProperties = {
|
|
39
|
+
fileName: string;
|
|
40
|
+
fileSize: number;
|
|
41
|
+
};
|
|
42
|
+
export type TestCase = {
|
|
43
|
+
testCaseUuid?: string;
|
|
44
|
+
customSteps: string;
|
|
45
|
+
description: string;
|
|
46
|
+
labels?: string[];
|
|
47
|
+
datasetName?: string;
|
|
48
|
+
};
|
|
49
|
+
export type Rule = {
|
|
50
|
+
steps: string;
|
|
51
|
+
name: string;
|
|
52
|
+
labels: string[];
|
|
53
|
+
};
|
|
54
|
+
export type ChromeExtensionInput = {
|
|
55
|
+
name: string;
|
|
56
|
+
file: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Represents a single chrome extension override with its binary file.
|
|
60
|
+
*/
|
|
61
|
+
export type ChromeExtensionOverrideItem = {
|
|
62
|
+
name: string;
|
|
63
|
+
file: ReadStream;
|
|
64
|
+
};
|
|
65
|
+
export type ChromeExtensionOverridesRequest = {
|
|
66
|
+
chromeExtensions: ChromeExtensionOverrideItem[];
|
|
67
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type TestRigorConfig = {
|
|
2
|
+
authToken?: string;
|
|
3
|
+
defaultTestSuiteId?: string;
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
};
|
|
6
|
+
export type ConfigPath = {
|
|
7
|
+
homeDir: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Get the paths for testRigor configuration
|
|
12
|
+
*/
|
|
13
|
+
export declare function getConfigPath(): ConfigPath;
|
|
14
|
+
/**
|
|
15
|
+
* Read testRigor configuration from ~/.testrigor/testrigor.yml
|
|
16
|
+
*/
|
|
17
|
+
export declare function readConfig(): Promise<TestRigorConfig>;
|
|
18
|
+
/**
|
|
19
|
+
* Write testRigor configuration to ~/.testrigor/testrigor.yml
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeConfig(config: TestRigorConfig): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Read a specific property from testRigor configuration
|
|
24
|
+
*/
|
|
25
|
+
export declare function readConfigProperty<T extends keyof TestRigorConfig>(property: T): Promise<TestRigorConfig[T] | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Update a specific property in testRigor configuration
|
|
28
|
+
*/
|
|
29
|
+
export declare function updateConfigProperty<T extends keyof TestRigorConfig>(property: T, value: TestRigorConfig[T]): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Delete a specific property from testRigor configuration
|
|
32
|
+
*/
|
|
33
|
+
export declare function deleteConfigProperty<T extends keyof TestRigorConfig>(property: T): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteConfigProperty = exports.updateConfigProperty = exports.readConfigProperty = exports.writeConfig = exports.readConfig = exports.getConfigPath = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const Fs = require("@supercharge/fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const yaml_1 = require("yaml");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
/**
|
|
10
|
+
* Get the paths for testRigor configuration
|
|
11
|
+
*/
|
|
12
|
+
function getConfigPath() {
|
|
13
|
+
const homeDir = (0, utils_1.getHomeDir)();
|
|
14
|
+
const configPath = path.join(homeDir, "testrigor.yml");
|
|
15
|
+
return { homeDir, configPath };
|
|
16
|
+
}
|
|
17
|
+
exports.getConfigPath = getConfigPath;
|
|
18
|
+
/**
|
|
19
|
+
* Read testRigor configuration from ~/.testrigor/testrigor.yml
|
|
20
|
+
*/
|
|
21
|
+
async function readConfig() {
|
|
22
|
+
const { configPath } = getConfigPath();
|
|
23
|
+
if (!(await Fs.pathExists(configPath))) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const content = await Fs.content(configPath);
|
|
28
|
+
return (0, yaml_1.parse)(content.toString()) || {};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
core_1.ux.warn(`Warning: Found testrigor.yml at "${configPath}" but could not read/parse it.\n` +
|
|
32
|
+
`Error: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
33
|
+
"Please check your configuration file.");
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.readConfig = readConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Write testRigor configuration to ~/.testrigor/testrigor.yml
|
|
40
|
+
*/
|
|
41
|
+
async function writeConfig(config) {
|
|
42
|
+
const { homeDir, configPath } = getConfigPath();
|
|
43
|
+
// Ensure the config directory exists
|
|
44
|
+
if (!(await Fs.pathExists(homeDir))) {
|
|
45
|
+
await Fs.ensureDir(homeDir);
|
|
46
|
+
}
|
|
47
|
+
// Avoid writing empty objects as "{}"
|
|
48
|
+
const configContent = Object.keys(config).length > 0 ? (0, yaml_1.stringify)(config) : "";
|
|
49
|
+
if (configContent.trim() === "") {
|
|
50
|
+
await Fs.writeFile(configPath, "");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await Fs.writeFile(configPath, configContent);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.writeConfig = writeConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Read a specific property from testRigor configuration
|
|
59
|
+
*/
|
|
60
|
+
async function readConfigProperty(property) {
|
|
61
|
+
const config = await readConfig();
|
|
62
|
+
return config[property] || null;
|
|
63
|
+
}
|
|
64
|
+
exports.readConfigProperty = readConfigProperty;
|
|
65
|
+
/**
|
|
66
|
+
* Update a specific property in testRigor configuration
|
|
67
|
+
*/
|
|
68
|
+
async function updateConfigProperty(property, value) {
|
|
69
|
+
const config = await readConfig();
|
|
70
|
+
if (value === null || value === undefined) {
|
|
71
|
+
delete config[property];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
config[property] = value;
|
|
75
|
+
}
|
|
76
|
+
await writeConfig(config);
|
|
77
|
+
}
|
|
78
|
+
exports.updateConfigProperty = updateConfigProperty;
|
|
79
|
+
/**
|
|
80
|
+
* Delete a specific property from testRigor configuration
|
|
81
|
+
*/
|
|
82
|
+
async function deleteConfigProperty(property) {
|
|
83
|
+
const config = await readConfig();
|
|
84
|
+
delete config[property];
|
|
85
|
+
await writeConfig(config);
|
|
86
|
+
}
|
|
87
|
+
exports.deleteConfigProperty = deleteConfigProperty;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSettings = exports.parseRules = exports.parseTestCases = void 0;
|
|
4
|
+
const Fs = require("@supercharge/fs");
|
|
5
|
+
const glob_1 = require("glob");
|
|
6
|
+
const yaml_1 = require("yaml");
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
async function parseTestCases(path) {
|
|
9
|
+
const result = [];
|
|
10
|
+
const files = await (0, glob_1.glob)(path, { withFileTypes: true });
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
if (file.isFile()) {
|
|
13
|
+
const content = await Fs.content(file.fullpath());
|
|
14
|
+
if ((0, utils_1.isYamlFile)(file.name)) {
|
|
15
|
+
const json = (0, yaml_1.parse)(content.toString());
|
|
16
|
+
const testCase = {
|
|
17
|
+
customSteps: json.customSteps,
|
|
18
|
+
description: (0, utils_1.removeExtension)(file.name),
|
|
19
|
+
};
|
|
20
|
+
if (json.testCaseUuid) {
|
|
21
|
+
testCase.testCaseUuid = json.testCaseUuid;
|
|
22
|
+
}
|
|
23
|
+
if (json.labels) {
|
|
24
|
+
testCase.labels = json.labels;
|
|
25
|
+
}
|
|
26
|
+
if (json.datasetName) {
|
|
27
|
+
testCase.datasetName = json.datasetName;
|
|
28
|
+
}
|
|
29
|
+
result.push(testCase);
|
|
30
|
+
}
|
|
31
|
+
if ((0, utils_1.isTextFile)(file.name)) {
|
|
32
|
+
const testCase = {
|
|
33
|
+
customSteps: content,
|
|
34
|
+
description: (0, utils_1.removeExtension)(file.name),
|
|
35
|
+
};
|
|
36
|
+
result.push(testCase);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
exports.parseTestCases = parseTestCases;
|
|
43
|
+
async function parseRules(path) {
|
|
44
|
+
const result = [];
|
|
45
|
+
const files = await (0, glob_1.glob)(path, { withFileTypes: true });
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (file.isFile()) {
|
|
48
|
+
const content = await Fs.content(file.fullpath());
|
|
49
|
+
if ((0, utils_1.isYamlFile)(file.name)) {
|
|
50
|
+
const json = (0, yaml_1.parse)(content.toString());
|
|
51
|
+
const ruleName = (0, utils_1.removeExtension)(file.name);
|
|
52
|
+
const rule = {
|
|
53
|
+
steps: json.steps,
|
|
54
|
+
name: ruleName,
|
|
55
|
+
labels: json.labels?.length ? json.labels : [json.name],
|
|
56
|
+
};
|
|
57
|
+
result.push(rule);
|
|
58
|
+
}
|
|
59
|
+
if ((0, utils_1.isTextFile)(file.name)) {
|
|
60
|
+
const ruleName = (0, utils_1.removeExtension)(file.name);
|
|
61
|
+
const rule = {
|
|
62
|
+
steps: content,
|
|
63
|
+
name: ruleName,
|
|
64
|
+
labels: [ruleName],
|
|
65
|
+
};
|
|
66
|
+
result.push(rule);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
exports.parseRules = parseRules;
|
|
73
|
+
async function parseSettings(path) {
|
|
74
|
+
const content = await Fs.content(path);
|
|
75
|
+
if ((0, utils_1.isYamlFile)(path)) {
|
|
76
|
+
return (0, yaml_1.parse)(content.toString());
|
|
77
|
+
}
|
|
78
|
+
if ((0, utils_1.isJsonFile)(path)) {
|
|
79
|
+
return JSON.parse(content.toString());
|
|
80
|
+
}
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
exports.parseSettings = parseSettings;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as FormData from "form-data";
|
|
2
|
+
import { ChromeExtensionInput } from "../types";
|
|
3
|
+
export interface ChromeExtensionFormDataResult {
|
|
4
|
+
form: FormData;
|
|
5
|
+
cleanup: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Converts ChromeExtensionInput[] -> ChromeExtensionOverridesRequest -> FormData
|
|
9
|
+
* Returns both the FormData and a cleanup function to close streams.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildChromeExtensionFormData(chromeExtensions: ChromeExtensionInput[]): ChromeExtensionFormDataResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildChromeExtensionFormData = void 0;
|
|
4
|
+
const FormData = require("form-data");
|
|
5
|
+
const Fs = require("@supercharge/fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
function toChromeExtensionOverrideItem(input) {
|
|
8
|
+
try {
|
|
9
|
+
return {
|
|
10
|
+
name: input.name,
|
|
11
|
+
file: Fs.createReadStream(input.file),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new Error(`Failed to read chrome extension file "${input.file}": ${error instanceof Error ? error.message : String(error)}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function toChromeExtensionOverridesRequest(extensionInputs) {
|
|
19
|
+
const chromeExtensions = extensionInputs.map(toChromeExtensionOverrideItem);
|
|
20
|
+
const streams = chromeExtensions.map((item) => item.file);
|
|
21
|
+
return {
|
|
22
|
+
request: { chromeExtensions },
|
|
23
|
+
streams,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Converts ChromeExtensionInput[] -> ChromeExtensionOverridesRequest -> FormData
|
|
28
|
+
* Returns both the FormData and a cleanup function to close streams.
|
|
29
|
+
*/
|
|
30
|
+
function buildChromeExtensionFormData(chromeExtensions) {
|
|
31
|
+
const { request, streams } = toChromeExtensionOverridesRequest(chromeExtensions);
|
|
32
|
+
const form = new FormData();
|
|
33
|
+
request.chromeExtensions.forEach((ext, index) => {
|
|
34
|
+
form.append(`chromeExtensions[${index}].name`, ext.name);
|
|
35
|
+
const fileName = path.basename(chromeExtensions[index].file);
|
|
36
|
+
form.append(`chromeExtensions[${index}].file`, ext.file, {
|
|
37
|
+
filename: fileName,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
streams.forEach((stream) => {
|
|
42
|
+
if (!stream.destroyed) {
|
|
43
|
+
stream.destroy();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
return { form, cleanup };
|
|
48
|
+
}
|
|
49
|
+
exports.buildChromeExtensionFormData = buildChromeExtensionFormData;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function download(dir: string): Promise<string | null>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.download = void 0;
|
|
4
|
+
const Fs = require("@supercharge/fs");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const node_fetch_commonjs_1 = require("node-fetch-commonjs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const tar = require("tar-stream");
|
|
10
|
+
const unzip = require("unzip-stream");
|
|
11
|
+
const zlib = require("zlib");
|
|
12
|
+
const core_1 = require("@oclif/core");
|
|
13
|
+
const FILE_EXTENSION = {
|
|
14
|
+
darwin: "tar.gz",
|
|
15
|
+
linux: "tar.gz",
|
|
16
|
+
win32: "zip",
|
|
17
|
+
};
|
|
18
|
+
const FILE_NAME = {
|
|
19
|
+
darwin: "darwin",
|
|
20
|
+
linux: "linux",
|
|
21
|
+
win32: "windows",
|
|
22
|
+
};
|
|
23
|
+
const ARCHITECTURE_MAP = {
|
|
24
|
+
x64: "amd64",
|
|
25
|
+
arm64: "arm64",
|
|
26
|
+
};
|
|
27
|
+
const VERSION = "2.3.15";
|
|
28
|
+
function getFolderName() {
|
|
29
|
+
const platform = os.platform();
|
|
30
|
+
const architecture = os.arch();
|
|
31
|
+
const fileName = FILE_NAME[platform];
|
|
32
|
+
const arch = ARCHITECTURE_MAP[architecture];
|
|
33
|
+
if (arch && fileName) {
|
|
34
|
+
return `trtc_${VERSION}_${fileName}_${arch}`;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Unsupported platform ${platform} or architecture ${architecture}`);
|
|
37
|
+
}
|
|
38
|
+
function getUrl() {
|
|
39
|
+
const platform = os.platform();
|
|
40
|
+
const architecture = os.arch();
|
|
41
|
+
const extension = FILE_EXTENSION[platform];
|
|
42
|
+
const folderName = getFolderName();
|
|
43
|
+
if (folderName && extension) {
|
|
44
|
+
return `https://testrigor-storage.s3.amazonaws.com/tunneling-client/${VERSION}/${folderName}.${extension}`;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unsupported platform ${platform} or architecture ${architecture}`);
|
|
47
|
+
}
|
|
48
|
+
async function extractZip(dir, stream) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
stream
|
|
51
|
+
.pipe(unzip.Extract({ path: dir }))
|
|
52
|
+
.on("close", () => resolve(path.join(dir, getFolderName())))
|
|
53
|
+
.on("error", reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function extractTar(dir, stream) {
|
|
57
|
+
const extract = tar.extract();
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
extract.on("error", reject);
|
|
60
|
+
extract.on("entry", (header, stream, next) => {
|
|
61
|
+
const entryPath = path.join(dir, header.name);
|
|
62
|
+
const opts = { mode: header.mode };
|
|
63
|
+
switch (header.type) {
|
|
64
|
+
case "directory":
|
|
65
|
+
Fs.ensureDir(entryPath, opts);
|
|
66
|
+
next();
|
|
67
|
+
break;
|
|
68
|
+
case "file": {
|
|
69
|
+
const writeStream = fs.createWriteStream(entryPath);
|
|
70
|
+
stream.pipe(writeStream);
|
|
71
|
+
writeStream.on("close", () => {
|
|
72
|
+
try {
|
|
73
|
+
fs.chmodSync(entryPath, header.mode || 0o644);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
core_1.ux.warn(`Could not set permissions for ${entryPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "symlink":
|
|
82
|
+
// ignore symlinks since they will not work on windows
|
|
83
|
+
next();
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(header.type);
|
|
87
|
+
}
|
|
88
|
+
stream.resume();
|
|
89
|
+
stream.on("end", next);
|
|
90
|
+
});
|
|
91
|
+
extract.on("finish", () => resolve(path.join(dir, getFolderName())));
|
|
92
|
+
stream.pipe(zlib.createGunzip()).pipe(extract);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function download(dir) {
|
|
96
|
+
const response = await (0, node_fetch_commonjs_1.default)(getUrl());
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`unexpected response ${response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
const stream = response.body;
|
|
101
|
+
if (stream == null) {
|
|
102
|
+
throw new Error("unexpected null response body");
|
|
103
|
+
}
|
|
104
|
+
if (os.platform() === "win32") {
|
|
105
|
+
return extractZip(dir, stream);
|
|
106
|
+
}
|
|
107
|
+
return extractTar(dir, stream);
|
|
108
|
+
}
|
|
109
|
+
exports.download = download;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default class Tunnel {
|
|
2
|
+
private static readonly BINARY_DIRECTORY;
|
|
3
|
+
private running;
|
|
4
|
+
private process;
|
|
5
|
+
private port;
|
|
6
|
+
private binaryPath?;
|
|
7
|
+
constructor();
|
|
8
|
+
initialize(): Promise<void>;
|
|
9
|
+
start(minPort: number, maxPort: number): Promise<void>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
isRunning(): boolean;
|
|
12
|
+
getPort(): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const Fs = require("@supercharge/fs");
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const trtc_1 = require("./trtc");
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
function randomPort(min, max) {
|
|
10
|
+
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
11
|
+
}
|
|
12
|
+
async function updateConfig(dir, port) {
|
|
13
|
+
const parentDir = path.dirname(dir);
|
|
14
|
+
const configPath = `${parentDir}/config.toml`;
|
|
15
|
+
const newConfigContent = `[[proxies]]
|
|
16
|
+
name = "http_proxy_${port}"
|
|
17
|
+
type = "tcp"
|
|
18
|
+
remotePort = ${port}
|
|
19
|
+
[proxies.plugin]
|
|
20
|
+
type = "http_proxy"`;
|
|
21
|
+
return Fs.writeFile(configPath, newConfigContent);
|
|
22
|
+
}
|
|
23
|
+
async function startTrtc(trtc, command) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const binaryPath = path.join(path.dirname(trtc), command);
|
|
26
|
+
const workingDir = path.dirname(trtc); // This is where config.toml is written
|
|
27
|
+
if (!Fs.pathExistsSync(binaryPath)) {
|
|
28
|
+
reject(new Error(`Binary not found at ${binaryPath}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const tunnelProcess = childProcess.spawn(binaryPath, [], {
|
|
32
|
+
cwd: workingDir,
|
|
33
|
+
});
|
|
34
|
+
tunnelProcess.stdout.on("data", (data) => {
|
|
35
|
+
const dataString = data.toString();
|
|
36
|
+
if (dataString.includes("start proxy success")) {
|
|
37
|
+
resolve(tunnelProcess);
|
|
38
|
+
}
|
|
39
|
+
else if (dataString.includes("port already used")) {
|
|
40
|
+
tunnelProcess.kill("SIGINT");
|
|
41
|
+
reject(new Error("Port already used"));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
tunnelProcess.stderr.on("data", (data) => {
|
|
45
|
+
reject(new Error(`TRTC error: ${data.toString()}`));
|
|
46
|
+
});
|
|
47
|
+
tunnelProcess.on("error", (error) => {
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
tunnelProcess.on("exit", (code, signal) => {
|
|
51
|
+
if (code !== 0) {
|
|
52
|
+
reject(new Error(`TRTC exited with code ${code}`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
class Tunnel {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.running = false;
|
|
60
|
+
this.process = null;
|
|
61
|
+
this.port = 0;
|
|
62
|
+
}
|
|
63
|
+
async initialize() {
|
|
64
|
+
const directory = await (0, utils_1.createDirectory)(Tunnel.BINARY_DIRECTORY);
|
|
65
|
+
this.binaryPath = await (0, trtc_1.download)(directory);
|
|
66
|
+
}
|
|
67
|
+
async start(minPort, maxPort) {
|
|
68
|
+
if (this.binaryPath == null) {
|
|
69
|
+
throw new Error("Could not download the trtc binary");
|
|
70
|
+
}
|
|
71
|
+
const command = os.platform() === "win32" ? "trtc.exe" : "trtc";
|
|
72
|
+
let maxTries = 40;
|
|
73
|
+
while (maxTries > 0) {
|
|
74
|
+
try {
|
|
75
|
+
this.port = randomPort(minPort, maxPort);
|
|
76
|
+
await updateConfig(this.binaryPath, this.port);
|
|
77
|
+
this.process = await startTrtc(this.binaryPath, command);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
maxTries--;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (maxTries === 0) {
|
|
85
|
+
throw new Error("Could not start trtc process");
|
|
86
|
+
}
|
|
87
|
+
this.running = true;
|
|
88
|
+
}
|
|
89
|
+
async stop() {
|
|
90
|
+
if (this.isRunning()) {
|
|
91
|
+
this.process?.kill("SIGINT");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
isRunning() {
|
|
95
|
+
return this.running;
|
|
96
|
+
}
|
|
97
|
+
getPort() {
|
|
98
|
+
return this.port;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.default = Tunnel;
|
|
102
|
+
Tunnel.BINARY_DIRECTORY = "trtc";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MAX_FILE_SIZE_100_MB: number;
|
|
2
|
+
export declare const UPLOAD_TIMEOUT_MS: number;
|
|
3
|
+
export declare function createDirectory(name: string): Promise<string>;
|
|
4
|
+
export declare function getHomeDir(): string;
|
|
5
|
+
export declare function pathExists(name: string | undefined): Promise<boolean>;
|
|
6
|
+
export declare function isRunningOnCi(): any;
|
|
7
|
+
export declare function rendererToUse(): "default" | "verbose";
|
|
8
|
+
export declare function isYamlFile(fileName: string): boolean;
|
|
9
|
+
export declare function isTextFile(fileName: string): boolean;
|
|
10
|
+
export declare function isJsonFile(fileName: string): boolean;
|
|
11
|
+
export declare function isValidFileExtensionToUpload(fileName: string): boolean;
|
|
12
|
+
export declare function getExtensionLength(fileName: string): 0 | 4 | 5;
|
|
13
|
+
export declare function removeExtension(fileName: string): string;
|
|
14
|
+
export declare function extractUrlWithoutQueryParams(urlString: string): string;
|
|
15
|
+
export declare function extractRunIdFromDetailsUrl(detailsUrl: string | undefined): string;
|
|
16
|
+
export declare function isValidUrl(url: string): boolean;
|