bdy 1.22.85-dev-buddy-auth-me-869dq96dc → 1.22.85-dev
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/distTs/README.md +181 -0
- package/distTs/package.json +1 -1
- package/distTs/src/agent/socket/client.js +2 -2
- package/distTs/src/api/client.js +4 -3
- package/distTs/src/command/login.js +11 -2
- package/distTs/src/command/project/get.js +18 -0
- package/distTs/src/command/project/set.js +31 -0
- package/distTs/src/command/sandbox/get/yaml.js +30 -0
- package/distTs/src/command/vt/scrape.js +193 -0
- package/distTs/src/tunnel/api/buddy.js +1 -1
- package/distTs/src/tunnel/config.js +241 -0
- package/distTs/src/tunnel/http/auth.js +404 -0
- package/distTs/src/tunnel/http/jwks.js +84 -0
- package/distTs/src/tunnel/tunnel.js +130 -742
- package/distTs/src/utils.js +8 -0
- package/package.json +1 -1
- package/distTs/detect-rules.json +0 -351
- package/distTs/src/command/pipeline/run/apply.js +0 -62
- package/distTs/src/command/yaml/actions/detect.js +0 -268
- package/distTs/src/command/yaml/actions/info.js +0 -56
- package/distTs/src/command/yaml/actions/list.js +0 -70
- package/distTs/src/command/yaml/actions/schema.js +0 -100
- package/distTs/src/command/yaml/actions.js +0 -13
- package/distTs/src/command/yaml/agents.js +0 -88
- package/distTs/src/command/yaml/cache.js +0 -98
- package/distTs/src/command/yaml/init.js +0 -110
- package/distTs/src/command/yaml/pipeline.js +0 -42
- package/distTs/src/command/yaml/render.js +0 -83
- package/distTs/src/command/yaml/schemaUtils.js +0 -138
- package/distTs/src/command/yaml/validate.js +0 -259
- package/distTs/src/command/yaml.js +0 -18
- package/distTs/src/diskCache.js +0 -82
package/distTs/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Buddy CLI (`bdy`)
|
|
2
|
+
|
|
3
|
+
Work seamlessly with [Buddy](https://buddy.works) from the command line.
|
|
4
|
+
|
|
5
|
+
`bdy` is Buddy's official command-line interface for managing CI/CD pipelines, agents, tests, artifacts, sandboxes, distributions, and more — straight from your terminal.
|
|
6
|
+
|
|
7
|
+
📚 Full documentation: https://buddy.works/docs/cli/getting-started
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### npm (recommended)
|
|
12
|
+
|
|
13
|
+
Requires Node.js 14+.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# macOS / Linux
|
|
17
|
+
sudo npm i -g bdy
|
|
18
|
+
|
|
19
|
+
# Windows
|
|
20
|
+
npm i -g bdy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Homebrew (macOS)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
brew tap buddy/bdy
|
|
27
|
+
brew install bdy
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### APT (Linux x64 / ARM64)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
sudo apt-get update && sudo apt-get install -y software-properties-common
|
|
34
|
+
sudo gpg --homedir /tmp --no-default-keyring --keyring /usr/share/keyrings/buddy.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys eb39332e766364ca6220e8dc631c5a16310cc0ad
|
|
35
|
+
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/buddy.gpg] https://es.buddy.works/bdy/apt-repo prod main" | sudo tee /etc/apt/sources.list.d/buddy.list > /dev/null
|
|
36
|
+
sudo apt-get update && sudo apt-get install -y bdy
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> On ARM64, replace `arch=amd64` with `arch=arm64` in the `echo` command above.
|
|
40
|
+
|
|
41
|
+
### Chocolatey (Windows)
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
choco install bdy --pre
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Direct download
|
|
48
|
+
|
|
49
|
+
Prebuilt binaries are available for macOS (Apple Silicon), Linux (x64/ARM64), and Windows (x64) on the [installation page](https://buddy.works/docs/cli/getting-started).
|
|
50
|
+
|
|
51
|
+
### Verify & update
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bdy version # show current and latest version
|
|
55
|
+
sudo npm i -g bdy # update (or use your package manager's upgrade command)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Authentication
|
|
59
|
+
|
|
60
|
+
A [Buddy account](https://buddy.works) is required to use the CLI.
|
|
61
|
+
|
|
62
|
+
Log in interactively:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bdy login
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
New to Buddy? Create an account from the CLI:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bdy register
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Linking a project
|
|
75
|
+
|
|
76
|
+
Once logged in, link a directory to a Buddy project:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mkdir my-proj
|
|
80
|
+
cd my-proj
|
|
81
|
+
bdy proj link
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
After linking, every `bdy` command run from this directory is executed against the linked project — no need to pass the project name each time.
|
|
85
|
+
|
|
86
|
+
Check who you're logged in as, or log out:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bdy whoami
|
|
90
|
+
bdy logout
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Tokens & non-interactive login
|
|
94
|
+
|
|
95
|
+
For CI/CD pipelines and non-browser environments, authenticate with a [personal access token](https://buddy.works/docs/api/getting-started/oauth2/personal-access-token):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bdy login --token <token>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The login command accepts the following options:
|
|
102
|
+
|
|
103
|
+
| Option | Description | Environment variable |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `--token <token>` | Personal access token | `BUDDY_TOKEN` |
|
|
106
|
+
| `--api <url>` | API URL for On-Premises installations | `BUDDY_API_ENDPOINT` |
|
|
107
|
+
| `--region <region>` | Region: `us`, `eu`, or `as` | `BUDDY_REGION` |
|
|
108
|
+
| `-w, --workspace <domain>` | Workspace URL handle | `BUDDY_WORKSPACE` |
|
|
109
|
+
|
|
110
|
+
Each option can be supplied as a flag or via its environment variable — handy for pipelines and headless setups:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export BUDDY_TOKEN=<token>
|
|
114
|
+
export BUDDY_WORKSPACE=<your-workspace>
|
|
115
|
+
bdy login
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Agent integration (plugin & skills)
|
|
119
|
+
|
|
120
|
+
The [Buddy Plugin](https://github.com/buddy/buddy-plugin) lets coding agents deploy applications, expose local services, and manage infrastructure on Buddy. It bundles a consolidated skill covering sandboxes, artifacts, tunnels, domains, distributions, and pipelines, plus two commands:
|
|
121
|
+
|
|
122
|
+
- `/deploy [name] [path]` — deploy a static site or server application
|
|
123
|
+
- `/expose [port]` — open a tunnel to a locally running service
|
|
124
|
+
|
|
125
|
+
Make sure the CLI is installed first (`sudo npm install -g bdy`), then:
|
|
126
|
+
|
|
127
|
+
**Claude Code** — install the plugin (commands + skills):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
claude plugin marketplace add buddy/buddy-plugin
|
|
131
|
+
claude plugin install buddy@buddy-plugin
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Other agents** — install the skills only:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx skills add buddy/buddy-plugin
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then authenticate and link your project:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
bdy login
|
|
144
|
+
cd your-project
|
|
145
|
+
bdy proj link
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The plugin auto-detects your project type: static sites generate versioned artifacts with public URLs, while server applications deploy to sandboxes with HTTPS endpoints. Monorepos can deploy multiple applications, each getting its own URL and endpoint.
|
|
149
|
+
|
|
150
|
+
## Commands
|
|
151
|
+
|
|
152
|
+
| Command | Description |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `bdy login` / `sign-in` | Log in to Buddy |
|
|
155
|
+
| `bdy register` / `sign-up` | Register a new Buddy account |
|
|
156
|
+
| `bdy whoami` | Check login information |
|
|
157
|
+
| `bdy logout` | Log out from Buddy |
|
|
158
|
+
| `bdy workspace` / `ws` | Manage workspaces |
|
|
159
|
+
| `bdy project` / `proj` | Manage projects |
|
|
160
|
+
| `bdy pipeline` / `pip` | Interact with the pipeline service |
|
|
161
|
+
| `bdy artifact` / `art` | Interact with the artifact service |
|
|
162
|
+
| `bdy sandbox` / `sb` | Interact with sandboxes |
|
|
163
|
+
| `bdy agent` | Install and run `bdy` as an OS service (Windows, macOS, Linux) |
|
|
164
|
+
| `bdy tunnel` | Manage tunnel configuration |
|
|
165
|
+
| `bdy crawl` | Manage web crawls (Markdown, HTML, PNG/JPEG screenshots; suite linking) |
|
|
166
|
+
| `bdy tests` | Manage unit tests and visual regression (Storybook, URL captures, runners) |
|
|
167
|
+
| `bdy domain` | Manage zones used in distribution routes |
|
|
168
|
+
| `bdy distro` | Manage distributions |
|
|
169
|
+
| `bdy api` | Contact the Buddy API directly |
|
|
170
|
+
| `bdy update` / `version` | Show version and update the CLI |
|
|
171
|
+
|
|
172
|
+
Run any command with `-h` / `--help` for detailed usage:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
bdy --help
|
|
176
|
+
bdy pipeline --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Feedback
|
|
180
|
+
|
|
181
|
+
Found a bug or have a feature request? Let us know through [Buddy support](https://buddy.works/support) or the [documentation](https://buddy.works/docs/cli/getting-started).
|
package/distTs/package.json
CHANGED
|
@@ -139,10 +139,10 @@ class AgentSocketClient extends events_1.default {
|
|
|
139
139
|
forceBase64: true,
|
|
140
140
|
transports: ['websocket'],
|
|
141
141
|
reconnection: false,
|
|
142
|
-
rejectUnauthorized:
|
|
142
|
+
rejectUnauthorized: !(0, utils_1.isLocalHost)(host),
|
|
143
143
|
// @ts-ignore
|
|
144
144
|
agent: new https_1.Agent({
|
|
145
|
-
rejectUnauthorized:
|
|
145
|
+
rejectUnauthorized: !(0, utils_1.isLocalHost)(host),
|
|
146
146
|
}),
|
|
147
147
|
maxHttpBufferSize: 3e6,
|
|
148
148
|
extraHeaders: {
|
package/distTs/src/api/client.js
CHANGED
|
@@ -26,7 +26,7 @@ class ApiClient {
|
|
|
26
26
|
this.clientToken = clientToken;
|
|
27
27
|
this.client = new undici_1.Pool(baseUrl, {
|
|
28
28
|
connect: {
|
|
29
|
-
rejectUnauthorized:
|
|
29
|
+
rejectUnauthorized: !(0, utils_1.isLocalHost)(baseUrl.hostname),
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
32
|
}
|
|
@@ -706,7 +706,7 @@ class ApiClient {
|
|
|
706
706
|
client_name: name,
|
|
707
707
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
708
708
|
response_types: ['code'],
|
|
709
|
-
token_endpoint_auth_method: '
|
|
709
|
+
token_endpoint_auth_method: 'client_secret_post',
|
|
710
710
|
token_expires_in: 3600,
|
|
711
711
|
},
|
|
712
712
|
parseResponseBody: true,
|
|
@@ -732,7 +732,7 @@ class ApiClient {
|
|
|
732
732
|
parseResponseBody: true,
|
|
733
733
|
});
|
|
734
734
|
}
|
|
735
|
-
async exchangeAppToken(code, clientId, clientSecret, redirectUrl) {
|
|
735
|
+
async exchangeAppToken(code, clientId, clientSecret, redirectUrl, codeVerifier) {
|
|
736
736
|
return await this.request({
|
|
737
737
|
method: 'POST',
|
|
738
738
|
path: '/oauth2/token',
|
|
@@ -742,6 +742,7 @@ class ApiClient {
|
|
|
742
742
|
client_id: clientId,
|
|
743
743
|
client_secret: clientSecret,
|
|
744
744
|
redirect_uri: redirectUrl,
|
|
745
|
+
code_verifier: codeVerifier,
|
|
745
746
|
},
|
|
746
747
|
parseResponseBody: true,
|
|
747
748
|
httpUrlEncoded: true,
|
|
@@ -7,6 +7,7 @@ exports.authorize = authorize;
|
|
|
7
7
|
const cfg_1 = __importDefault(require("../tunnel/cfg"));
|
|
8
8
|
const output_1 = __importDefault(require("../output"));
|
|
9
9
|
const node_http_1 = __importDefault(require("node:http"));
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
11
|
const texts_1 = require("../texts");
|
|
11
12
|
const utils_1 = require("../utils");
|
|
12
13
|
const input_1 = __importDefault(require("../input"));
|
|
@@ -37,6 +38,13 @@ const OAUTH_CLIENT_APP_SCOPES = [
|
|
|
37
38
|
function getRedirectUrl(api) {
|
|
38
39
|
return `https://${api}/oauth2/cli`;
|
|
39
40
|
}
|
|
41
|
+
function generatePkcePair() {
|
|
42
|
+
const codeVerifier = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
43
|
+
const codeChallenge = (0, node_crypto_1.createHash)('sha256')
|
|
44
|
+
.update(codeVerifier)
|
|
45
|
+
.digest('base64url');
|
|
46
|
+
return { codeVerifier, codeChallenge };
|
|
47
|
+
}
|
|
40
48
|
async function waitForOpen(isRegister, abortCode, seconds = 3) {
|
|
41
49
|
output_1.default.normal(isRegister ? texts_1.TXT_REGISTER_OPENING : texts_1.TXT_LOGIN_OPENING);
|
|
42
50
|
let star = true;
|
|
@@ -66,11 +74,12 @@ async function oauthServer(api, clientId, clientSecret, forceRegisterEmail, forc
|
|
|
66
74
|
}, OAUTH_TIMEOUT);
|
|
67
75
|
return new Promise((resolve) => {
|
|
68
76
|
const state = (0, uuid_1.v4)();
|
|
77
|
+
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
69
78
|
const abortCode = new AbortController();
|
|
70
79
|
const exchange = async (urlCode, urlState, res) => {
|
|
71
80
|
const client = new ApiClient(new URL(`https://${api}`));
|
|
72
81
|
try {
|
|
73
|
-
const response = await client.exchangeAppToken(urlCode, clientId, clientSecret, redirectUrl);
|
|
82
|
+
const response = await client.exchangeAppToken(urlCode, clientId, clientSecret, redirectUrl, codeVerifier);
|
|
74
83
|
if (res)
|
|
75
84
|
res.end(urlState);
|
|
76
85
|
s.close();
|
|
@@ -111,7 +120,7 @@ async function oauthServer(api, clientId, clientSecret, forceRegisterEmail, forc
|
|
|
111
120
|
output_1.default.exitError(texts_1.ERR_LOGIN_HTTP_SERVER_PORT_TAKEN);
|
|
112
121
|
});
|
|
113
122
|
s.listen(OAUTH_CLIENT_APP_PORT, OAUTH_CLIENT_APP_HOST, async () => {
|
|
114
|
-
let url = `https://${api}/oauth2/authorize?type=web_server&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&response_type=code&scope=${encodeURIComponent(OAUTH_CLIENT_APP_SCOPES.join(' '))}&state=${encodeURIComponent(state)}`;
|
|
123
|
+
let url = `https://${api}/oauth2/authorize?type=web_server&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&response_type=code&scope=${encodeURIComponent(OAUTH_CLIENT_APP_SCOPES.join(' '))}&state=${encodeURIComponent(state)}&code_challenge=${encodeURIComponent(codeChallenge)}&code_challenge_method=S256`;
|
|
115
124
|
if (isRegister) {
|
|
116
125
|
url += `®ister_email=${encodeURIComponent(forceRegisterEmail)}®ister_name=${encodeURIComponent(forceRegisterName)}`;
|
|
117
126
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const cfg_1 = __importDefault(require("../../tunnel/cfg"));
|
|
7
|
+
const output_1 = __importDefault(require("../../output"));
|
|
8
|
+
const texts_1 = require("../../texts");
|
|
9
|
+
const utils_1 = require("../../utils");
|
|
10
|
+
const commandProjectGet = (0, utils_1.newCommand)('get', texts_1.DESC_COMMAND_PROJECT_GET);
|
|
11
|
+
commandProjectGet.action(async () => {
|
|
12
|
+
const project = cfg_1.default.getProject();
|
|
13
|
+
if (!project) {
|
|
14
|
+
output_1.default.exitError(texts_1.TXT_PROJECT_NONE);
|
|
15
|
+
}
|
|
16
|
+
output_1.default.exitNormal(project);
|
|
17
|
+
});
|
|
18
|
+
exports.default = commandProjectGet;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const cfg_1 = __importDefault(require("../../tunnel/cfg"));
|
|
7
|
+
const output_1 = __importDefault(require("../../output"));
|
|
8
|
+
const texts_1 = require("../../texts");
|
|
9
|
+
const utils_1 = require("../../utils");
|
|
10
|
+
const input_1 = __importDefault(require("../../input"));
|
|
11
|
+
const commandProjectSet = (0, utils_1.newCommand)('set', texts_1.DESC_COMMAND_PROJECT_SET);
|
|
12
|
+
commandProjectSet.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
|
|
13
|
+
commandProjectSet.argument('[project]', texts_1.ARG_COMMAND_PROJECT_NAME);
|
|
14
|
+
commandProjectSet.action(async (project, options) => {
|
|
15
|
+
output_1.default.handleSignals();
|
|
16
|
+
const workspace = input_1.default.restApiWorkspace(options.workspace);
|
|
17
|
+
const client = input_1.default.restApiTokenClient();
|
|
18
|
+
if (project) {
|
|
19
|
+
await client.getProject(workspace, project);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const response = await client.getProjects(workspace);
|
|
23
|
+
project = await output_1.default.selectProject(response.projects);
|
|
24
|
+
}
|
|
25
|
+
cfg_1.default.setProject(project);
|
|
26
|
+
if (!project)
|
|
27
|
+
output_1.default.exitSuccess(texts_1.TXT_PROJECT_SET_CLEARED);
|
|
28
|
+
else
|
|
29
|
+
output_1.default.exitSuccess((0, texts_1.TXT_PROJECT_SET_SUCCESS)(project));
|
|
30
|
+
});
|
|
31
|
+
exports.default = commandProjectSet;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const utils_1 = require("../../../utils");
|
|
7
|
+
const texts_1 = require("../../../texts");
|
|
8
|
+
const input_1 = __importDefault(require("../../../input"));
|
|
9
|
+
const output_1 = __importDefault(require("../../../output"));
|
|
10
|
+
const commandSandboxGetYaml = (0, utils_1.newCommand)('yaml', texts_1.DESC_COMMAND_SANDBOX_GET_YAML);
|
|
11
|
+
commandSandboxGetYaml.hideVersionUpdate = true;
|
|
12
|
+
commandSandboxGetYaml.alias('yml');
|
|
13
|
+
commandSandboxGetYaml.option('-w, --workspace <domain>', texts_1.OPTION_REST_API_WORKSPACE);
|
|
14
|
+
commandSandboxGetYaml.option('-p, --project <name>', texts_1.OPTION_REST_API_PROJECT);
|
|
15
|
+
commandSandboxGetYaml.argument('<identifier>', texts_1.OPTION_SANDBOX_IDENTIFIER);
|
|
16
|
+
commandSandboxGetYaml.action(async (identifier, options) => {
|
|
17
|
+
const workspace = input_1.default.restApiWorkspace(options.workspace);
|
|
18
|
+
const project = input_1.default.restApiProject(options.project);
|
|
19
|
+
const client = input_1.default.restApiTokenClient();
|
|
20
|
+
let result = await client.listSandboxes(workspace, project);
|
|
21
|
+
const sandboxes = result.sandboxes || [];
|
|
22
|
+
const found = sandboxes.find((s) => s.identifier === identifier);
|
|
23
|
+
if (!found) {
|
|
24
|
+
output_1.default.exitError(texts_1.ERR_SANDBOX_NOT_FOUND);
|
|
25
|
+
}
|
|
26
|
+
const sandboxId = found.id;
|
|
27
|
+
result = await client.getSandboxYaml(workspace, sandboxId);
|
|
28
|
+
output_1.default.exitNormal(Buffer.from(result.yaml, 'base64').toString('utf8'));
|
|
29
|
+
});
|
|
30
|
+
exports.default = commandSandboxGetYaml;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const utils_1 = require("../../utils");
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const texts_1 = require("../../texts");
|
|
9
|
+
const validation_1 = require("../../visualTest/validation");
|
|
10
|
+
const output_1 = __importDefault(require("../../output"));
|
|
11
|
+
const requests_1 = require("../../visualTest/requests");
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const node_zlib_1 = require("node:zlib");
|
|
14
|
+
const tar_stream_1 = __importDefault(require("tar-stream"));
|
|
15
|
+
const promises_1 = require("node:stream/promises");
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const promises_2 = require("node:fs/promises");
|
|
19
|
+
const commandScrape = (0, utils_1.newCommand)('scrape', texts_1.DESC_COMMAND_VT_SCRAPE);
|
|
20
|
+
commandScrape.argument('<url>', texts_1.OPTION_SCRAPE_URL);
|
|
21
|
+
commandScrape.option('--follow', texts_1.OPTION_SCRAPE_FOLLOW, false);
|
|
22
|
+
commandScrape.addOption(new commander_1.Option('--outputType <type>', texts_1.OPTION_SCRAPE_OUTPUT_TYPE)
|
|
23
|
+
.choices(['jpeg', 'png', 'md', 'html'])
|
|
24
|
+
.makeOptionMandatory());
|
|
25
|
+
commandScrape.option('--quality <quality>', texts_1.OPTION_SCRAPE_QUALITY);
|
|
26
|
+
commandScrape.option('--fullPage', texts_1.OPTION_SCRAPE_FULL_PAGE, false);
|
|
27
|
+
commandScrape.option('--cssSelector <selector>', texts_1.OPTION_SCRAPE_CSS_SELECTOR);
|
|
28
|
+
commandScrape.option('--xpathSelector <selector>', texts_1.OPTION_SCRAPE_XPATH_SELECTOR);
|
|
29
|
+
commandScrape.addOption(new commander_1.Option('--browser <browser>', texts_1.OPTION_SCRAPE_BROWSER)
|
|
30
|
+
.choices(['chrome', 'firefox', 'safari'])
|
|
31
|
+
.default('chrome'));
|
|
32
|
+
commandScrape.option('--viewport <viewport>', texts_1.OPTION_SCRAPE_VIEWPORT, '1920x1080');
|
|
33
|
+
commandScrape.option('--devicePixelRatio <ratio>', texts_1.OPTION_SCRAPE_DEVICE_PIXEL_RATIO, '1');
|
|
34
|
+
commandScrape.option('--waitForElement <selector>', texts_1.OPTION_SCRAPE_WAIT_FOR_ELEMENT);
|
|
35
|
+
commandScrape.option('--darkMode', texts_1.OPTION_SCRAPE_DARK_MODE, false);
|
|
36
|
+
commandScrape.option('--delay <delay>', texts_1.OPTION_SCRAPE_DELAY, '0');
|
|
37
|
+
commandScrape.option('--outputDir <dir>', texts_1.OPTION_SCRAPE_OUTPUT_DIR, '.');
|
|
38
|
+
commandScrape.action(async (inputUrl, options) => {
|
|
39
|
+
if (!(0, validation_1.checkToken)()) {
|
|
40
|
+
output_1.default.exitError(texts_1.ERR_MISSING_VT_TOKEN);
|
|
41
|
+
}
|
|
42
|
+
const { url, follow, outputType, quality, outputDir, fullPage, cssSelector, xpathSelector, browser, viewport, devicePixelRatio, darkMode, delay, waitForElement, } = validateInputAndOptions(inputUrl, options);
|
|
43
|
+
try {
|
|
44
|
+
const { buildId } = await (0, requests_1.sendScrap)(url, outputType, follow, quality, fullPage, cssSelector, xpathSelector, browser, viewport, devicePixelRatio, darkMode, delay, waitForElement);
|
|
45
|
+
output_1.default.normal('Starting scrape session');
|
|
46
|
+
const status = await watchSessionStatus(buildId);
|
|
47
|
+
if (!status.ok) {
|
|
48
|
+
output_1.default.exitError(`Unexpected error while watching session status: ${status.error}`);
|
|
49
|
+
}
|
|
50
|
+
output_1.default.normal('Downloading scrape package');
|
|
51
|
+
const scrapPackageStream = await (0, requests_1.downloadScrapPackage)(buildId);
|
|
52
|
+
const brotliDecompressor = (0, node_zlib_1.createBrotliDecompress)();
|
|
53
|
+
const unpack = tar_stream_1.default.extract();
|
|
54
|
+
unpack.on('entry', async (header, stream, next) => {
|
|
55
|
+
const currentDir = process.cwd();
|
|
56
|
+
const preparedOutputDir = outputDir.startsWith('.')
|
|
57
|
+
? node_path_1.default.join(currentDir, outputDir)
|
|
58
|
+
: outputDir;
|
|
59
|
+
const newFilePath = node_path_1.default.join(preparedOutputDir, header.name);
|
|
60
|
+
try {
|
|
61
|
+
if (header.type === 'file') {
|
|
62
|
+
await (0, promises_2.mkdir)(node_path_1.default.dirname(newFilePath), { recursive: true });
|
|
63
|
+
const fileWriteStream = (0, node_fs_1.createWriteStream)(newFilePath);
|
|
64
|
+
await (0, promises_1.pipeline)(stream, fileWriteStream);
|
|
65
|
+
next();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
stream.resume();
|
|
69
|
+
next();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (entryError) {
|
|
73
|
+
output_1.default.error(`Error processing entry ${header.name}: ${entryError}`);
|
|
74
|
+
next(entryError);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await (0, promises_1.pipeline)(scrapPackageStream, brotliDecompressor, unpack);
|
|
78
|
+
output_1.default.exitSuccess('Downloading scrape package finished');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
output_1.default.exitError(`${error}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
function validateInputAndOptions(input, options) {
|
|
85
|
+
const urlSchema = zod_1.z.string().url();
|
|
86
|
+
const optionsSchema = zod_1.z.object({
|
|
87
|
+
follow: zod_1.z.boolean(),
|
|
88
|
+
outputType: zod_1.z.enum(['jpeg', 'png', 'md', 'html']),
|
|
89
|
+
quality: zod_1.z.coerce.number().min(1).max(100).optional(),
|
|
90
|
+
outputDir: zod_1.z.string().default('.'),
|
|
91
|
+
fullPage: zod_1.z.boolean().optional(),
|
|
92
|
+
cssSelector: zod_1.z.string().optional(),
|
|
93
|
+
xpathSelector: zod_1.z.string().optional(),
|
|
94
|
+
browser: zod_1.z.enum(['chrome', 'firefox', 'safari']),
|
|
95
|
+
viewport: zod_1.z
|
|
96
|
+
.string()
|
|
97
|
+
.refine((value) => {
|
|
98
|
+
const [width, height] = value.split('x');
|
|
99
|
+
return (width &&
|
|
100
|
+
height &&
|
|
101
|
+
!isNaN(Number(width)) &&
|
|
102
|
+
!isNaN(Number(height)) &&
|
|
103
|
+
Number(width) > 0 &&
|
|
104
|
+
Number(height) > 0);
|
|
105
|
+
}, 'Invalid viewport format, example: 1920x1080')
|
|
106
|
+
.transform((value) => {
|
|
107
|
+
const [width, height] = value.split('x');
|
|
108
|
+
return {
|
|
109
|
+
width: Number(width),
|
|
110
|
+
height: Number(height),
|
|
111
|
+
};
|
|
112
|
+
}),
|
|
113
|
+
devicePixelRatio: zod_1.z.coerce.number().min(1).max(4),
|
|
114
|
+
darkMode: zod_1.z.boolean(),
|
|
115
|
+
delay: zod_1.z.coerce.number().min(0).max(10000),
|
|
116
|
+
waitForElement: zod_1.z.string().optional(),
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const url = urlSchema.parse(input);
|
|
120
|
+
const { follow, outputType, quality, outputDir, fullPage, cssSelector, xpathSelector, browser, viewport, devicePixelRatio, darkMode, delay, waitForElement, } = optionsSchema.parse(options);
|
|
121
|
+
if (typeof quality === 'number' && outputType !== 'jpeg') {
|
|
122
|
+
output_1.default.exitError('Quality is only supported for jpeg output type, use --outputType jpeg');
|
|
123
|
+
}
|
|
124
|
+
if (cssSelector && xpathSelector) {
|
|
125
|
+
output_1.default.exitError('Only one of --cssSelector or --xpathSelector can be used');
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
url,
|
|
129
|
+
follow,
|
|
130
|
+
outputType,
|
|
131
|
+
quality,
|
|
132
|
+
outputDir,
|
|
133
|
+
fullPage,
|
|
134
|
+
cssSelector,
|
|
135
|
+
xpathSelector,
|
|
136
|
+
browser,
|
|
137
|
+
viewport,
|
|
138
|
+
devicePixelRatio,
|
|
139
|
+
darkMode,
|
|
140
|
+
delay,
|
|
141
|
+
waitForElement,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof zod_1.ZodError) {
|
|
146
|
+
output_1.default.exitError(error.errors.map((e) => `${e.path}: ${e.message}`).join(', '));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function watchSessionStatus(buildId) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const eventSource = (0, requests_1.connectToScrapSession)(buildId);
|
|
156
|
+
eventSource.addEventListener('SESSION_STATUS', (event) => {
|
|
157
|
+
const data = JSON.parse(event.data);
|
|
158
|
+
if (data.status === 'GATHER_URLS_COMPLETED') {
|
|
159
|
+
output_1.default.normal(`Gathering URLs completed, found ${data.text} URLs`);
|
|
160
|
+
}
|
|
161
|
+
else if (data.status === 'GATHER_URLS_FAILED') {
|
|
162
|
+
output_1.default.error('Gathering URLs failed');
|
|
163
|
+
}
|
|
164
|
+
else if (data.status === 'SCRAPE_URL_COMPLETED') {
|
|
165
|
+
output_1.default.normal(`Scraping ${data.text} completed`);
|
|
166
|
+
}
|
|
167
|
+
else if (data.status === 'SCRAPE_URL_FAILED') {
|
|
168
|
+
output_1.default.error(`Scraping ${data.text} failed`);
|
|
169
|
+
}
|
|
170
|
+
else if (data.status === 'CREATE_PACKAGE_COMPLETED') {
|
|
171
|
+
output_1.default.normal('Package created');
|
|
172
|
+
}
|
|
173
|
+
else if (data.status === 'CREATE_PACKAGE_FAILED') {
|
|
174
|
+
output_1.default.error('Package creation failed');
|
|
175
|
+
}
|
|
176
|
+
else if (data.status === 'FINISHED') {
|
|
177
|
+
eventSource.close();
|
|
178
|
+
output_1.default.normal('Scrape session finished');
|
|
179
|
+
resolve({ ok: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
eventSource.addEventListener('error', (event) => {
|
|
183
|
+
if (event.code) {
|
|
184
|
+
eventSource.close();
|
|
185
|
+
if (event.code === 410) {
|
|
186
|
+
output_1.default.normal('Scrape session finished');
|
|
187
|
+
}
|
|
188
|
+
resolve({ ok: event.code === 410, error: event.code });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
exports.default = commandScrape;
|
|
@@ -26,7 +26,7 @@ const makeRequest = async (host, path, body, method = 'POST', respAsJson = true,
|
|
|
26
26
|
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
27
27
|
method,
|
|
28
28
|
signal: c.signal,
|
|
29
|
-
dispatcher: output_1.default.getUndiciUnauthorizedAgent(),
|
|
29
|
+
dispatcher: (0, utils_1.isLocalHost)(host) ? output_1.default.getUndiciUnauthorizedAgent() : undefined,
|
|
30
30
|
});
|
|
31
31
|
if (respAsJson)
|
|
32
32
|
data = await response.json();
|