@lazco/cli 0.1.0 → 0.2.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.
- package/CHANGELOG.md +20 -0
- package/README.md +230 -85
- package/dist/cli.js +354 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All releases of `@lazco/cli` are in this file.
|
|
4
4
|
The newest release is at the top.
|
|
5
5
|
|
|
6
|
+
## [0.2.1]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- `lazco auth login` with the browser exits after it prints the sign-in.
|
|
11
|
+
It kept running when the browser held a spare connection open to the CLI, though the sign-in was already stored.
|
|
12
|
+
|
|
13
|
+
## [0.2.0]
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `lazco labs artifacts publish <path>` and `lazco labs artifacts update <artifact-id> <path>`, and the MCP tools `labs_artifacts_publish` and `labs_artifacts_update`, publish a Lazco Artifact from one HTML or Markdown file, or from a directory with one entry file and the images, CSS and fonts it uses.
|
|
18
|
+
In a directory, every file or directory whose name starts with a dot is skipped, so a `.env` is never published.
|
|
19
|
+
The command refuses a file type that an artifact may not hold, more than 100 files, and more than 5 MiB, before it sends anything.
|
|
20
|
+
An update reads the current version first and sends it as `If-Match`, unless `--if-match` gives it, so a change made since then fails instead of being overwritten.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The browser page that ends an SSO sign-in uses the Lazco design, in the dark theme.
|
|
25
|
+
|
|
6
26
|
## [0.1.0]
|
|
7
27
|
|
|
8
28
|
### Added
|
package/README.md
CHANGED
|
@@ -1,147 +1,292 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Its commands come from the API itself, so every API operation is a command as soon as the API ships it.
|
|
5
|
-
Output is JSON by default, so scripts and agents can read it.
|
|
3
|
+
# lazco
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
Lazco Cloud CLI and local MCP server: manage your Lazco Cloud resources from a terminal, a script or an AI agent.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@lazco/cli) [](https://github.com/Lazco-Corporation/cloud-cli/actions/workflows/ci.yml) [](package.json) [](LICENSE)
|
|
8
|
+
|
|
9
|
+
[Installation](#installation) · [Quick start](#quick-start) · [Sign in](#sign-in) · [Scripts](#use-it-in-a-script) · [Spending](#commands-that-spend) · [MCP](#use-it-from-an-mcp-client)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
`lazco` is the command line of [Lazco Cloud](https://lazco.tw).
|
|
14
|
+
It runs the operations of the Lazco Cloud API, such as servers, DNS, domains, billing, Lazco Pages and Lazco Artifacts.
|
|
15
|
+
|
|
16
|
+
## Highlights
|
|
17
|
+
|
|
18
|
+
- **Every API operation is a command.**
|
|
19
|
+
`lazco` reads the API's spec when it runs, so a new API operation is a command on the day the API ships it, with no update of `lazco`.
|
|
20
|
+
- **Answers in JSON.**
|
|
21
|
+
stdout holds only the answer, and an error is one JSON line on stderr with exit code 1.
|
|
22
|
+
See [Use it in a script](#use-it-in-a-script).
|
|
23
|
+
- **No charge without a quote.**
|
|
24
|
+
A command that spends from your account balance shows the price from the API first, and asks you.
|
|
25
|
+
A retry never charges twice.
|
|
26
|
+
See [Commands that spend](#commands-that-spend).
|
|
27
|
+
- **Credentials in the OS keychain.**
|
|
28
|
+
Sign in in a browser, with a code on another device, or with an API token.
|
|
29
|
+
See [Sign in](#sign-in).
|
|
30
|
+
- **An MCP server for 8 AI clients.**
|
|
31
|
+
`lazco mcp install` adds it to Claude Code, Codex, Cursor and 5 more, with read tools only until you allow writes.
|
|
32
|
+
See [Use it from an MCP client](#use-it-from-an-mcp-client).
|
|
33
|
+
- **Publish a site or a document.**
|
|
34
|
+
Deploy a directory to Lazco Pages and upload only the changed files, or publish an HTML or Markdown file as a Lazco Artifact.
|
|
35
|
+
See [Publish a Lazco Pages site](#publish-a-lazco-pages-site).
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
With Node.js 22 or later, install `lazco` from npm:
|
|
10
40
|
|
|
11
41
|
```sh
|
|
12
|
-
|
|
42
|
+
npm install -g @lazco/cli
|
|
13
43
|
```
|
|
14
44
|
|
|
15
|
-
|
|
45
|
+
Check the install:
|
|
16
46
|
|
|
17
47
|
```sh
|
|
18
|
-
|
|
48
|
+
lazco --version
|
|
19
49
|
```
|
|
20
50
|
|
|
21
|
-
|
|
51
|
+
It prints the version, such as `0.2.0`.
|
|
52
|
+
|
|
53
|
+
To run `lazco` without an install, use `npx -y @lazco/cli <command>`.
|
|
54
|
+
|
|
55
|
+
To install the binary, which needs no Node.js, on macOS or Linux:
|
|
22
56
|
|
|
23
57
|
```sh
|
|
24
|
-
curl -fsSL https://raw.githubusercontent.com/Lazco-Corporation/cloud-cli/main/scripts/install.sh
|
|
58
|
+
curl -fsSL https://raw.githubusercontent.com/Lazco-Corporation/cloud-cli/main/scripts/install.sh \
|
|
59
|
+
| bash -s -- install
|
|
25
60
|
```
|
|
26
61
|
|
|
27
62
|
The installer puts `lazco` in `~/.local/bin`, or in `$LAZCO_INSTALL_DIR`, and checks the archive against `SHA256SUMS`.
|
|
28
|
-
Run it with `update` or `uninstall`
|
|
29
|
-
|
|
63
|
+
Run it again with `update` or `uninstall` in place of `install`.
|
|
64
|
+
When GitHub limits the API rate of your network, the installer fails.
|
|
65
|
+
Set `GH_TOKEN` to a GitHub token and run it again.
|
|
66
|
+
|
|
67
|
+
On Windows, use npm, or download the zip from a [release](https://github.com/Lazco-Corporation/cloud-cli/releases).
|
|
68
|
+
Each release has archives for darwin-arm64, darwin-x64, linux-x64, linux-arm64 and windows-x64.
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
| Use | How | You get |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| In a terminal | Add `--human` to a command | A table or a list of fields |
|
|
75
|
+
| In a script | Run the command as it is | JSON on stdout, errors as JSON on stderr |
|
|
76
|
+
| From an AI agent | `lazco mcp install --client <client>` | The API operations as MCP tools |
|
|
77
|
+
|
|
78
|
+
1. Sign in.
|
|
79
|
+
On a terminal, `lazco` asks which way to sign in:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
lazco auth login
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
2. Check who you are signed in as:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
lazco whoami --fields organization,role
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{"organization":{"id":"<organization id>","name":"<organization name>"},"role":"owner"}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
3. List your servers as a table:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
lazco compute list --human
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Find the command you need:
|
|
102
|
+
|
|
103
|
+
| Command | Prints |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `lazco --help` | The command groups, such as `compute`, `dns` and `billing` |
|
|
106
|
+
| `lazco compute --help` | The commands of one group |
|
|
107
|
+
| `lazco compute create --help` | The arguments and the flags of one command |
|
|
108
|
+
| `lazco --describe` | Every command, one JSON line each |
|
|
109
|
+
| `lazco --describe compute create` | One command and its input as JSON Schema |
|
|
30
110
|
|
|
31
111
|
## Sign in
|
|
32
112
|
|
|
33
|
-
|
|
113
|
+
| Command | Signs in with |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `lazco auth login --sso` | Your Lazco account, in a browser on this machine |
|
|
116
|
+
| `lazco auth login --device` | Your Lazco account, with a URL and a code that you open on another device |
|
|
117
|
+
| `lazco auth login --with-token` | An API token from stdin, which you create on the API page of the Lazco Cloud panel |
|
|
118
|
+
|
|
119
|
+
In a script, pipe the token in:
|
|
34
120
|
|
|
35
121
|
```sh
|
|
36
|
-
lazco auth login --
|
|
122
|
+
printf '%s' "$TOKEN" | lazco auth login --with-token
|
|
37
123
|
```
|
|
38
124
|
|
|
39
|
-
|
|
40
|
-
|
|
125
|
+
With `--sso`, your browser opens the Lazco Cloud sign-in.
|
|
126
|
+
Pick the organization and the access, read only or read and write, then go back to the terminal.
|
|
127
|
+
Over SSH, or on Linux with no display, `--sso` prints a URL and a code, the same as `--device`.
|
|
128
|
+
Enter the code only on the page that the URL opens.
|
|
41
129
|
|
|
42
|
-
|
|
|
130
|
+
| Flag | Does |
|
|
43
131
|
|---|---|
|
|
44
|
-
| `--device` | Prints a URL and a code to sign in on another device, with no browser on this machine |
|
|
45
132
|
| `--org <organization id>` | Preselects that organization |
|
|
46
133
|
| `--read-only` | Asks for read-only access |
|
|
47
|
-
| `--switch` | Picks the organization again, and signs out the previous sign-in of the profile
|
|
134
|
+
| `--switch` | Picks the organization again, and signs out the previous sign-in of the profile |
|
|
48
135
|
|
|
49
|
-
|
|
136
|
+
An SSO sign-in renews itself while you use it, and ends after 30 days without use.
|
|
50
137
|
You can see and end it in the signed-in products of the Cloud panel's settings.
|
|
51
138
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
139
|
+
`lazco` stores the credential in the OS keychain.
|
|
140
|
+
When there is no keychain, it stores the credential in its config file with mode 0600, and says so on stderr.
|
|
141
|
+
|
|
142
|
+
| Command | Does |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `lazco auth status` | Prints the profile, the method, the organization, the role, the access and the expiry |
|
|
145
|
+
| `lazco auth logout` | Signs out and forgets the credential. An API token stays valid until you revoke it in the panel |
|
|
146
|
+
| `lazco profile list` | Prints one line per profile, never its credential |
|
|
147
|
+
| `lazco profile use <name>` | Makes a profile the current one |
|
|
148
|
+
|
|
149
|
+
A profile is one stored sign-in, for example one for each organization.
|
|
150
|
+
Add `--profile <name>` to any command to sign in to or use another profile.
|
|
151
|
+
Without it, `lazco` uses the current profile, which starts as `default`.
|
|
152
|
+
|
|
153
|
+
## Use it in a script
|
|
154
|
+
|
|
155
|
+
- stdout holds only the answer: one JSON object, or one JSON object per line for a list with pages.
|
|
156
|
+
- An error is one line `{"error":{"code":"...","message":"..."}}` on stderr, and the exit code is 1.
|
|
157
|
+
A code that starts with `CLI_` comes from `lazco`.
|
|
158
|
+
Any other code comes from the API.
|
|
159
|
+
- Every other line on stderr starts with `lazco: `, such as a warning or the progress of a task.
|
|
160
|
+
- A command that starts a task, such as a deploy, waits until the task ends.
|
|
161
|
+
|
|
162
|
+
| Flag | Does |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `--json <payload>` | Gives the whole input as JSON. `--json -` reads it from stdin |
|
|
165
|
+
| `--fields <a,b>` | Keeps only these top-level fields of each record |
|
|
166
|
+
| `--page-all` | Follows every page of a list |
|
|
167
|
+
| `--dry-run` | Checks the input and prints the request, and sends nothing |
|
|
168
|
+
| `--yes` | Confirms a command that deletes or spends, with no question |
|
|
169
|
+
| `--no-wait` | Prints the accepted task at once, and does not wait for it |
|
|
170
|
+
| `--timeout <seconds>` | Sets how long to wait for a task. The default is 600 |
|
|
171
|
+
| `--human` | Prints readable text instead of JSON |
|
|
172
|
+
|
|
173
|
+
| Environment variable | Does |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `LAZCO_TOKEN` | Uses this API token, and ignores every profile |
|
|
176
|
+
| `LAZCO_PROFILE` | Picks the profile |
|
|
177
|
+
| `NO_COLOR` | Turns off the color of `--human` |
|
|
178
|
+
|
|
179
|
+
For example, check a new server's input before you send it:
|
|
55
180
|
|
|
56
181
|
```sh
|
|
57
|
-
|
|
182
|
+
lazco compute create --json - --dry-run < server.json
|
|
58
183
|
```
|
|
59
184
|
|
|
60
|
-
|
|
61
|
-
|
|
185
|
+
A command that deletes asks first on a terminal.
|
|
186
|
+
Some delete commands also need a confirm field, such as `--confirm` of `lazco compute delete`, and `--yes` does not replace it.
|
|
62
187
|
|
|
63
|
-
|
|
64
|
-
{"profile":"default","method":"sso","apiUrl":"https://api.lazco.tw/v1","organization":{"id":"<uuid>","name":"<name>"},"scope":"read_write","store":"keychain","current":true}
|
|
65
|
-
```
|
|
188
|
+
## Commands that spend
|
|
66
189
|
|
|
67
|
-
|
|
190
|
+
A command that spends from your account balance, such as `lazco compute create`, follows these steps:
|
|
68
191
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| `lazco profile use <name>` | Makes a profile the current one |
|
|
192
|
+
1. It asks the API for a quote, which charges nothing.
|
|
193
|
+
2. It shows the quote on stderr: the amount, each line, the period, the recurring charge and when the quote expires.
|
|
194
|
+
3. It asks you, or needs `--yes`.
|
|
195
|
+
Without a yes, it exits with `CLI_CONFIRM_REQUIRED`, and `details.quote` of the error holds the quote.
|
|
196
|
+
4. It spends exactly the quoted amount.
|
|
75
197
|
|
|
76
|
-
`--
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
198
|
+
`--dry-run` gets the quote and prints the request that it would send, with the quote in `quote`.
|
|
199
|
+
To spend with a quote that you already have, add `--quote-id <id>` and `--expected-amount <amount>`.
|
|
200
|
+
A quote is good for 10 minutes and for one spend.
|
|
201
|
+
When the API refuses a quote, `lazco` does not get a new one by itself, because nobody saw that price.
|
|
80
202
|
|
|
81
|
-
|
|
203
|
+
Each spend sends an idempotency key, so a retry after a lost answer never charges twice.
|
|
204
|
+
To pick the key, add `--idempotency-key <key>`.
|
|
205
|
+
Run the same command with the same key within 24 hours to get the first result, not a second charge.
|
|
206
|
+
|
|
207
|
+
## Publish a Lazco Pages site
|
|
82
208
|
|
|
83
209
|
```sh
|
|
84
|
-
lazco
|
|
85
|
-
lazco compute get <server-id>
|
|
86
|
-
lazco --describe compute create # the command and its input as JSON Schema
|
|
87
|
-
lazco compute create --json - < server.json --dry-run
|
|
210
|
+
lazco labs pages deployments create <site-id> ./dist
|
|
88
211
|
```
|
|
89
212
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
A quote is good for 10 minutes and for one spend.
|
|
104
|
-
When the API refuses a quote, `lazco` never asks for a new one by itself, because that is a price nobody saw.
|
|
105
|
-
- A command that spends sends an `Idempotency-Key`, so a retry after a lost answer never charges twice.
|
|
106
|
-
`--idempotency-key <key>` chooses the key: run the same command again with the same key, within 24 hours, to get the first result instead of a second charge.
|
|
107
|
-
- A task such as a deploy is waited for. `--no-wait` prints the accepted task at once.
|
|
108
|
-
|
|
109
|
-
### Publish a Lazco Pages site
|
|
213
|
+
The directory becomes the root of the site, and every file in it becomes public.
|
|
214
|
+
`lazco` skips each path with a segment that starts with a dot, `__MACOSX` or `node_modules`, so a `.env` next to your build is never published.
|
|
215
|
+
It refuses a link, a zip archive or a single file.
|
|
216
|
+
Unpack an archive first.
|
|
217
|
+
|
|
218
|
+
The command checks the limits of the API before it sends anything.
|
|
219
|
+
It uploads only the files that changed since the live deploy, and waits until the new deploy is live.
|
|
220
|
+
A failed or stopped upload cancels the deploy, so the next deploy can start at once.
|
|
221
|
+
Add `--dry-run` to print the file list that it would send.
|
|
222
|
+
|
|
223
|
+
## Publish a Lazco Artifact
|
|
224
|
+
|
|
225
|
+
A Lazco Artifact is a document or a web page with a permanent link.
|
|
110
226
|
|
|
111
227
|
```sh
|
|
112
|
-
lazco labs
|
|
113
|
-
lazco labs
|
|
228
|
+
lazco labs artifacts publish ./report.md --region <region> --title "<title>"
|
|
229
|
+
lazco labs artifacts update <artifact-id> ./report.md
|
|
114
230
|
```
|
|
115
231
|
|
|
116
|
-
The
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
232
|
+
The path is one HTML or Markdown file, or a directory with one entry file and the images, CSS and fonts that it uses.
|
|
233
|
+
Anyone with the link in `url` of the answer can open it.
|
|
234
|
+
In a directory, `lazco` skips each file or directory whose name starts with a dot.
|
|
235
|
+
|
|
236
|
+
`lazco` refuses a file type that an artifact cannot hold, more than 100 files, or more than 5 MiB, before it sends anything.
|
|
237
|
+
`warnings` in the answer lists what the page loads from outside, misses or runs, because those parts will not work.
|
|
238
|
+
|
|
239
|
+
An update fails when someone changed the artifact after `lazco` read it, so it never overwrites that change.
|
|
240
|
+
`lazco labs artifacts delete <artifact-id>` stops the link at once.
|
|
122
241
|
|
|
123
242
|
## Use it from an MCP client
|
|
124
243
|
|
|
125
|
-
`lazco mcp` is a local MCP server
|
|
126
|
-
Add it to
|
|
244
|
+
`lazco mcp` is a local MCP server that uses the same sign-in as the commands.
|
|
245
|
+
Add it to your client:
|
|
127
246
|
|
|
128
247
|
```sh
|
|
129
248
|
lazco mcp install --client <client>
|
|
130
249
|
```
|
|
131
250
|
|
|
132
251
|
`<client>` is one of `claude-code`, `claude-desktop`, `codex`, `cursor`, `devin`, `gemini`, `pi` or `vscode`.
|
|
252
|
+
Add `--dry-run` to see the entry without writing it.
|
|
133
253
|
The entry holds the command only, never a credential.
|
|
134
|
-
`--profile <name>`, `--allow-writes` and `--allow-spend` go into the entry, and `--dry-run` shows the entry without writing it.
|
|
135
254
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
255
|
+
| Flag | The server offers |
|
|
256
|
+
|---|---|
|
|
257
|
+
| None | Read tools only |
|
|
258
|
+
| `--allow-writes` | Also the tools that change things, and the delete tools that need the resource's name |
|
|
259
|
+
| `--allow-spend` | With `--allow-writes`, also the tools that spend from the account balance, each with its quote tool |
|
|
260
|
+
|
|
261
|
+
A spend tool needs a quote from its quote tool.
|
|
262
|
+
Its description tells the model to show you the quote and get your yes first.
|
|
263
|
+
When the client can ask you itself (MCP elicitation), the server shows you the quote and spends only after you accept.
|
|
264
|
+
A read-only sign-in gets read tools only.
|
|
265
|
+
`--profile <name>` picks the profile that the server uses.
|
|
266
|
+
|
|
267
|
+
For a desktop client, install `lazco` globally first, so the entry names its full path.
|
|
268
|
+
|
|
269
|
+
## Limitations
|
|
270
|
+
|
|
271
|
+
- **Commands follow your organization.**
|
|
272
|
+
`--help` and `--describe` hide the commands of a product that your organization has not turned on.
|
|
273
|
+
- **It needs the API to start.**
|
|
274
|
+
`lazco` reads the spec from the API and keeps a copy for 10 minutes.
|
|
275
|
+
With no network and no copy, the API commands do not run.
|
|
276
|
+
- **`--human` is for people.**
|
|
277
|
+
Its columns follow the API's answer.
|
|
278
|
+
Parse the JSON in a script.
|
|
279
|
+
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
Report a bug or ask for a feature in [GitHub issues](https://github.com/Lazco-Corporation/cloud-cli/issues).
|
|
283
|
+
For a question about your account or a bill, write to `support@lazco.tw`.
|
|
284
|
+
Report a vulnerability to `security@lazco.tw`, not in an issue.
|
|
142
285
|
|
|
143
|
-
|
|
286
|
+
The changes of each release are in [CHANGELOG.md](CHANGELOG.md).
|
|
287
|
+
The build and test commands are in [AGENTS.md](AGENTS.md), and [docs/](docs/) explains how the code works.
|
|
144
288
|
|
|
145
289
|
## License
|
|
146
290
|
|
|
147
|
-
MIT.
|
|
291
|
+
[MIT](LICENSE).
|
|
292
|
+
Each binary carries the notices of the code compiled into it in `THIRD_PARTY_NOTICES.txt`.
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ async function readAll(stream) {
|
|
|
17
17
|
// package.json
|
|
18
18
|
var package_default = {
|
|
19
19
|
name: "@lazco/cli",
|
|
20
|
-
version: "0.1
|
|
20
|
+
version: "0.2.1",
|
|
21
21
|
description: "Lazco Cloud CLI and local MCP server",
|
|
22
22
|
type: "module",
|
|
23
23
|
license: "MIT",
|
|
@@ -1110,11 +1110,37 @@ var LOOPBACK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
1110
1110
|
var CALLBACK_PATH = "/callback";
|
|
1111
1111
|
var DONE_PAGE = `<!doctype html>
|
|
1112
1112
|
<html lang="en">
|
|
1113
|
-
<head
|
|
1114
|
-
<
|
|
1115
|
-
<
|
|
1113
|
+
<head>
|
|
1114
|
+
<meta charset="utf-8">
|
|
1115
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1116
|
+
<meta name="color-scheme" content="dark">
|
|
1117
|
+
<title>Lazco CLI</title>
|
|
1118
|
+
<style>
|
|
1119
|
+
:root{--ground:#08090a;--panel:#101112;--ink:#e8eaec;--ink-2:#c3c8cc;--faint:#858c92;--line:rgba(255,255,255,.07);--brand-fill:#40b7f3;--shadow-2:inset 0 1px 0 0 rgba(255,255,255,.03),inset 0 0 0 1px rgba(255,255,255,.05),0 1px 1px -.5px rgba(0,0,0,.18)}
|
|
1120
|
+
*{box-sizing:border-box}
|
|
1121
|
+
html,body{margin:0}
|
|
1122
|
+
body{min-height:100vh;display:grid;place-items:center;padding:24px;background:var(--ground);color:var(--ink-2);font-family:'Noto Sans TC','PingFang TC','Microsoft JhengHei',-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
|
1123
|
+
main{width:100%;max-width:26rem;padding:28px;background:var(--panel);border-radius:10px;box-shadow:var(--shadow-2)}
|
|
1124
|
+
.mark{display:flex;align-items:center;gap:8px;margin:0 0 20px;color:var(--faint);font-size:11.5px;font-weight:600}
|
|
1125
|
+
.mark::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--brand-fill)}
|
|
1126
|
+
h1{margin:0 0 6px;color:var(--ink);font-size:24px;line-height:1.25;font-weight:650;letter-spacing:-.01em}
|
|
1127
|
+
.title{margin:0 0 6px;color:var(--ink);font-size:16px;line-height:1.25;font-weight:600}
|
|
1128
|
+
p{margin:0}
|
|
1129
|
+
hr{margin:20px 0;border:0;border-top:1px solid var(--line)}
|
|
1130
|
+
@media (max-width:640px){h1{font-size:26px}.title{font-size:17px}.mark{font-size:12.5px}}
|
|
1131
|
+
</style>
|
|
1132
|
+
</head>
|
|
1133
|
+
<body>
|
|
1134
|
+
<main>
|
|
1135
|
+
<p class="mark">Lazco CLI</p>
|
|
1136
|
+
<h1>Return to your terminal</h1>
|
|
1116
1137
|
<p>The Lazco CLI got the answer from your browser. You can close this tab.</p>
|
|
1117
|
-
<
|
|
1138
|
+
<hr>
|
|
1139
|
+
<div lang="zh-Hant-TW">
|
|
1140
|
+
<p class="title">請回到終端機</p>
|
|
1141
|
+
<p>Lazco CLI 已經收到瀏覽器的回應,你可以關閉這個分頁。</p>
|
|
1142
|
+
</div>
|
|
1143
|
+
</main>
|
|
1118
1144
|
</body>
|
|
1119
1145
|
</html>
|
|
1120
1146
|
`;
|
|
@@ -1148,6 +1174,7 @@ async function startLoopback(hosts = LOOPBACK_HOSTS) {
|
|
|
1148
1174
|
let settle;
|
|
1149
1175
|
let received;
|
|
1150
1176
|
let answered = false;
|
|
1177
|
+
const sockets = new Set;
|
|
1151
1178
|
const server = createServer((request, response) => {
|
|
1152
1179
|
const url = requestUrl(request.url);
|
|
1153
1180
|
if (url === undefined || answered || request.method !== "GET" || url.pathname !== CALLBACK_PATH) {
|
|
@@ -1169,6 +1196,10 @@ async function startLoopback(hosts = LOOPBACK_HOSTS) {
|
|
|
1169
1196
|
settle?.(received);
|
|
1170
1197
|
});
|
|
1171
1198
|
});
|
|
1199
|
+
server.on("connection", (socket) => {
|
|
1200
|
+
sockets.add(socket);
|
|
1201
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1202
|
+
});
|
|
1172
1203
|
let redirectUri;
|
|
1173
1204
|
let lastError;
|
|
1174
1205
|
for (const host of hosts) {
|
|
@@ -1184,15 +1215,16 @@ async function startLoopback(hosts = LOOPBACK_HOSTS) {
|
|
|
1184
1215
|
}
|
|
1185
1216
|
const close = () => {
|
|
1186
1217
|
server.close();
|
|
1187
|
-
|
|
1218
|
+
for (const socket of sockets) {
|
|
1219
|
+
socket.destroy();
|
|
1220
|
+
}
|
|
1188
1221
|
};
|
|
1189
1222
|
return {
|
|
1190
1223
|
redirectUri,
|
|
1191
1224
|
close,
|
|
1192
1225
|
callback: (timeoutMs) => new Promise((resolve, reject) => {
|
|
1193
1226
|
const timer = setTimeout(() => {
|
|
1194
|
-
|
|
1195
|
-
server.closeAllConnections();
|
|
1227
|
+
close();
|
|
1196
1228
|
reject(new CliError("CLI_TIMEOUT", `The browser sign-in did not come back within ${Math.round(timeoutMs / 60000)} minutes`));
|
|
1197
1229
|
}, timeoutMs);
|
|
1198
1230
|
settle = (query) => {
|
|
@@ -2112,8 +2144,28 @@ var PAGES_DEPLOYMENT_INPUT = {
|
|
|
2112
2144
|
"When the call ends before the deployment does, the deployment goes on: read it with the deployment read tool."
|
|
2113
2145
|
]
|
|
2114
2146
|
};
|
|
2147
|
+
var ARTIFACT_PATH_INPUT = "path";
|
|
2148
|
+
var ARTIFACT_FILES_INPUT = "files";
|
|
2149
|
+
var ARTIFACT_INPUT = {
|
|
2150
|
+
properties: {
|
|
2151
|
+
[ARTIFACT_PATH_INPUT]: {
|
|
2152
|
+
type: "string",
|
|
2153
|
+
minLength: 1,
|
|
2154
|
+
description: "The local file or directory to publish, in place of files. A directory is read with every file below it, and every file or directory whose name starts with a dot is skipped. A relative path is resolved against the working directory, and the MCP tool takes an absolute path only."
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2157
|
+
required: [],
|
|
2158
|
+
positionals: [ARTIFACT_PATH_INPUT],
|
|
2159
|
+
toolNotes: [
|
|
2160
|
+
"Publishes content that anyone with the link can open. Only publish what the user asked to publish.",
|
|
2161
|
+
"Give either path, an absolute path of a local file or directory, or files, for content that is not in a file, such as one Markdown or HTML document. Never both.",
|
|
2162
|
+
"The answer holds url, the permanent link, and warnings: what the page loads from outside, misses or runs, which will not work."
|
|
2163
|
+
],
|
|
2164
|
+
fills: [ARTIFACT_FILES_INPUT]
|
|
2165
|
+
};
|
|
2115
2166
|
var KNOWN_UPLOADERS = new Map([
|
|
2116
|
-
["pages-deployment", PAGES_DEPLOYMENT_INPUT]
|
|
2167
|
+
["pages-deployment", PAGES_DEPLOYMENT_INPUT],
|
|
2168
|
+
["artifact", ARTIFACT_INPUT]
|
|
2117
2169
|
]);
|
|
2118
2170
|
var COMMAND_WORD = "[a-z0-9][a-z0-9-]*";
|
|
2119
2171
|
var ANNOTATION_SCHEMA = {
|
|
@@ -2447,7 +2499,7 @@ function buildOperation(document, path, method, source, annotation, pathLevelPar
|
|
|
2447
2499
|
description: "The ETag of the version this change is based on, sent as If-Match. Without it, the current ETag is read first."
|
|
2448
2500
|
};
|
|
2449
2501
|
}
|
|
2450
|
-
const body = uploader === undefined ? mergeBody(document, source, properties, required) : addUploaderInput(annotation.upload, uploader, properties, required);
|
|
2502
|
+
const body = uploader === undefined ? mergeBody(document, source, properties, required) : addUploaderInput(document, source, annotation.upload, uploader, properties, required);
|
|
2451
2503
|
let resolved;
|
|
2452
2504
|
try {
|
|
2453
2505
|
resolved = resolveReferences(document, {
|
|
@@ -2505,7 +2557,23 @@ function withoutQuoteInputs(schema, mapping, parameters) {
|
|
|
2505
2557
|
required: required.filter((name) => !QUOTE_INPUTS.includes(name))
|
|
2506
2558
|
};
|
|
2507
2559
|
}
|
|
2508
|
-
function addUploaderInput(name, uploader, properties, required) {
|
|
2560
|
+
function addUploaderInput(document, source, name, uploader, properties, required) {
|
|
2561
|
+
const bodyInputs = [];
|
|
2562
|
+
if (uploader.fills !== undefined) {
|
|
2563
|
+
const before = new Set(Object.keys(properties));
|
|
2564
|
+
const merged = mergeBody(document, source, properties, required);
|
|
2565
|
+
bodyInputs.push(...Object.keys(properties).filter((property) => !before.has(property)));
|
|
2566
|
+
const missing = uploader.fills.filter((property) => !bodyInputs.includes(property));
|
|
2567
|
+
if (merged.mapping.mode !== "merged" || missing.length > 0) {
|
|
2568
|
+
throw new SkipError("unsupported-body", `the uploader "${name}" needs a JSON object body with ${uploader.fills.join(", ")}`);
|
|
2569
|
+
}
|
|
2570
|
+
for (const property of uploader.fills) {
|
|
2571
|
+
const index = required.indexOf(property);
|
|
2572
|
+
if (index !== -1) {
|
|
2573
|
+
required.splice(index, 1);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2509
2577
|
for (const [property, schema] of Object.entries(uploader.properties)) {
|
|
2510
2578
|
if (Object.hasOwn(properties, property)) {
|
|
2511
2579
|
throw new SkipError("unsupported-parameter", `the parameter "${property}" clashes with an input of the uploader "${name}"`);
|
|
@@ -2513,7 +2581,7 @@ function addUploaderInput(name, uploader, properties, required) {
|
|
|
2513
2581
|
properties[property] = { ...schema };
|
|
2514
2582
|
}
|
|
2515
2583
|
required.push(...uploader.required);
|
|
2516
|
-
return { mapping: { mode: "upload", uploader: name }, additionalProperties: false };
|
|
2584
|
+
return { mapping: { mode: "upload", uploader: name, bodyInputs }, additionalProperties: false };
|
|
2517
2585
|
}
|
|
2518
2586
|
function collectParameters(document, ...lists) {
|
|
2519
2587
|
const byKey = new Map;
|
|
@@ -2992,13 +3060,9 @@ async function walkPages(operation, input, prepare, context, onPage) {
|
|
|
2992
3060
|
}
|
|
2993
3061
|
}
|
|
2994
3062
|
|
|
2995
|
-
// src/uploads/
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
super("Interrupted");
|
|
2999
|
-
this.name = "Interrupted";
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3063
|
+
// src/uploads/artifacts/files.ts
|
|
3064
|
+
import { lstat as lstat2, readdir as readdir2, readFile as readFile4, realpath as realpath3, stat as stat2 } from "node:fs/promises";
|
|
3065
|
+
import nodePath2 from "node:path";
|
|
3002
3066
|
|
|
3003
3067
|
// src/uploads/pages/directory.ts
|
|
3004
3068
|
import { lstat, open, readdir, realpath as realpath2, stat } from "node:fs/promises";
|
|
@@ -3128,6 +3192,256 @@ async function walkSiteDirectory(root) {
|
|
|
3128
3192
|
return { files, skipped };
|
|
3129
3193
|
}
|
|
3130
3194
|
|
|
3195
|
+
// src/uploads/artifacts/files.ts
|
|
3196
|
+
var MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
|
|
3197
|
+
var MAX_ARTIFACT_FILES = 100;
|
|
3198
|
+
var ENCODING_BY_EXTENSION = new Map([
|
|
3199
|
+
[".html", "utf-8"],
|
|
3200
|
+
[".htm", "utf-8"],
|
|
3201
|
+
[".md", "utf-8"],
|
|
3202
|
+
[".css", "utf-8"],
|
|
3203
|
+
[".svg", "utf-8"],
|
|
3204
|
+
[".png", "base64"],
|
|
3205
|
+
[".jpg", "base64"],
|
|
3206
|
+
[".jpeg", "base64"],
|
|
3207
|
+
[".gif", "base64"],
|
|
3208
|
+
[".webp", "base64"],
|
|
3209
|
+
[".avif", "base64"],
|
|
3210
|
+
[".woff2", "base64"],
|
|
3211
|
+
[".woff", "base64"]
|
|
3212
|
+
]);
|
|
3213
|
+
function invalid4(message, details) {
|
|
3214
|
+
return new CliError("CLI_INVALID_INPUT", message, details);
|
|
3215
|
+
}
|
|
3216
|
+
function encodingOf(path) {
|
|
3217
|
+
return ENCODING_BY_EXTENSION.get(nodePath2.posix.extname(path).toLowerCase());
|
|
3218
|
+
}
|
|
3219
|
+
function mebibytes(bytes) {
|
|
3220
|
+
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3221
|
+
}
|
|
3222
|
+
function checkArtifactLimits(files, field) {
|
|
3223
|
+
const unknownType = files.filter((file) => encodingOf(file.path) === undefined);
|
|
3224
|
+
if (unknownType.length > 0) {
|
|
3225
|
+
throw invalid4(`A publish holds only HTML, Markdown, CSS, images and fonts, so ${unknownType.map((file) => file.path).join(", ")} cannot be published`, {
|
|
3226
|
+
field,
|
|
3227
|
+
files: unknownType.map((file) => file.path),
|
|
3228
|
+
allowed: [...ENCODING_BY_EXTENSION.keys()]
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
if (files.length > MAX_ARTIFACT_FILES) {
|
|
3232
|
+
throw invalid4(`A publish holds at most ${MAX_ARTIFACT_FILES} files, and this one has ${files.length}`, { field, files: files.length, limit: MAX_ARTIFACT_FILES });
|
|
3233
|
+
}
|
|
3234
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
3235
|
+
if (totalBytes > MAX_ARTIFACT_BYTES) {
|
|
3236
|
+
throw invalid4(`A publish holds at most ${mebibytes(MAX_ARTIFACT_BYTES)}, and this one has ${mebibytes(totalBytes)}`, { field, bytes: totalBytes, limit: MAX_ARTIFACT_BYTES });
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
function decodedSize(file) {
|
|
3240
|
+
return file.encoding === "base64" ? Buffer.from(file.content, "base64").length : Buffer.byteLength(file.content, "utf8");
|
|
3241
|
+
}
|
|
3242
|
+
async function readLocalArtifact(localPath, options) {
|
|
3243
|
+
const problem = (message) => invalid4(message, [{ field: ARTIFACT_PATH_INPUT, message }]);
|
|
3244
|
+
if (options.requireAbsolute && !nodePath2.isAbsolute(localPath)) {
|
|
3245
|
+
throw problem(`path must be an absolute path, because the working directory of the MCP server is the client's choice: ${localPath}`);
|
|
3246
|
+
}
|
|
3247
|
+
const given = nodePath2.resolve(options.workingDirectory, localPath);
|
|
3248
|
+
let root;
|
|
3249
|
+
let info;
|
|
3250
|
+
try {
|
|
3251
|
+
root = await realpath3(given);
|
|
3252
|
+
info = await stat2(root);
|
|
3253
|
+
} catch {
|
|
3254
|
+
throw problem(`${given} does not exist or cannot be read`);
|
|
3255
|
+
}
|
|
3256
|
+
if (info.isFile()) {
|
|
3257
|
+
const files = [{ path: nodePath2.basename(root), absolutePath: root, size: info.size }];
|
|
3258
|
+
checkArtifactLimits(files, ARTIFACT_PATH_INPUT);
|
|
3259
|
+
return { root, files, skipped: [] };
|
|
3260
|
+
}
|
|
3261
|
+
if (!info.isDirectory()) {
|
|
3262
|
+
throw problem(`${given} is not a file or a directory`);
|
|
3263
|
+
}
|
|
3264
|
+
if (isFilesystemRoot(root)) {
|
|
3265
|
+
throw problem(`${given} is a filesystem root, which is never published`);
|
|
3266
|
+
}
|
|
3267
|
+
const home = await realpath3(options.homeDirectory).catch(() => {
|
|
3268
|
+
return;
|
|
3269
|
+
});
|
|
3270
|
+
if (home !== undefined && (root === home || holds(root, home, nodePath2))) {
|
|
3271
|
+
throw problem(`${given} is or holds the home directory, which is never published`);
|
|
3272
|
+
}
|
|
3273
|
+
const { files, skipped } = await walkArtifactDirectory(root);
|
|
3274
|
+
checkArtifactLimits(files, ARTIFACT_PATH_INPUT);
|
|
3275
|
+
return { root, files, skipped };
|
|
3276
|
+
}
|
|
3277
|
+
async function walkArtifactDirectory(root) {
|
|
3278
|
+
const files = [];
|
|
3279
|
+
const skipped = [];
|
|
3280
|
+
const pathInArtifact = (absolutePath) => nodePath2.relative(root, absolutePath).split(nodePath2.sep).join("/");
|
|
3281
|
+
const unreadable = (path, error) => invalid4(`${path || "."} could not be read`, {
|
|
3282
|
+
path: path || ".",
|
|
3283
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
3284
|
+
});
|
|
3285
|
+
const visit = async (directory) => {
|
|
3286
|
+
let names;
|
|
3287
|
+
try {
|
|
3288
|
+
names = (await readdir2(directory)).sort();
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
throw unreadable(pathInArtifact(directory), error);
|
|
3291
|
+
}
|
|
3292
|
+
for (const name of names) {
|
|
3293
|
+
const absolutePath = nodePath2.join(directory, name);
|
|
3294
|
+
const path = pathInArtifact(absolutePath);
|
|
3295
|
+
if (name.startsWith(".")) {
|
|
3296
|
+
skipped.push(path);
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
let entry;
|
|
3300
|
+
try {
|
|
3301
|
+
entry = await lstat2(absolutePath);
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
throw unreadable(path, error);
|
|
3304
|
+
}
|
|
3305
|
+
if (entry.isSymbolicLink()) {
|
|
3306
|
+
throw invalid4(`${path} is a link. A publish never follows a link, so remove it first`, {
|
|
3307
|
+
path
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
if (entry.isDirectory()) {
|
|
3311
|
+
await visit(absolutePath);
|
|
3312
|
+
} else if (entry.isFile()) {
|
|
3313
|
+
files.push({ path, absolutePath, size: entry.size });
|
|
3314
|
+
if (files.length > MAX_ARTIFACT_FILES) {
|
|
3315
|
+
throw invalid4(`A publish holds at most ${MAX_ARTIFACT_FILES} files, and ${root} has more`, { field: ARTIFACT_PATH_INPUT, files: files.length, limit: MAX_ARTIFACT_FILES });
|
|
3316
|
+
}
|
|
3317
|
+
} else {
|
|
3318
|
+
throw invalid4(`${path} is not a file or a directory, so it cannot be published`, {
|
|
3319
|
+
path
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
await visit(root);
|
|
3325
|
+
if (files.length === 0) {
|
|
3326
|
+
throw invalid4(`${root} holds no file to publish${skipped.length > 0 ? ` (${skipped.length} skipped)` : ""}`, [{ field: ARTIFACT_PATH_INPUT, message: "holds no file to publish" }]);
|
|
3327
|
+
}
|
|
3328
|
+
files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
3329
|
+
return { files, skipped };
|
|
3330
|
+
}
|
|
3331
|
+
async function encodeArtifactFiles(files) {
|
|
3332
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3333
|
+
return Promise.all(files.map(async (file) => {
|
|
3334
|
+
const bytes = await readFile4(file.absolutePath);
|
|
3335
|
+
const encoding = encodingOf(file.path);
|
|
3336
|
+
if (encoding === "base64") {
|
|
3337
|
+
return { path: file.path, encoding, content: bytes.toString("base64") };
|
|
3338
|
+
}
|
|
3339
|
+
try {
|
|
3340
|
+
return { path: file.path, encoding, content: decoder.decode(bytes) };
|
|
3341
|
+
} catch {
|
|
3342
|
+
throw invalid4(`${file.path} is not valid UTF-8, so it cannot be published as text`, {
|
|
3343
|
+
field: ARTIFACT_PATH_INPUT,
|
|
3344
|
+
path: file.path
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
}));
|
|
3348
|
+
}
|
|
3349
|
+
function checkGivenFiles(files) {
|
|
3350
|
+
checkArtifactLimits(files.map((file) => ({ path: file.path, size: decodedSize(file) })), ARTIFACT_FILES_INPUT);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// src/uploads/artifact.ts
|
|
3354
|
+
var SKIPPED_NAMED = 5;
|
|
3355
|
+
function mebibytes2(bytes) {
|
|
3356
|
+
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3357
|
+
}
|
|
3358
|
+
async function artifactFiles(context) {
|
|
3359
|
+
const localPath = context.input[ARTIFACT_PATH_INPUT];
|
|
3360
|
+
const given = context.input[ARTIFACT_FILES_INPUT];
|
|
3361
|
+
if (localPath === undefined === (given === undefined)) {
|
|
3362
|
+
const message = `Give either ${ARTIFACT_PATH_INPUT}, a local file or directory, or ${ARTIFACT_FILES_INPUT}, and not both`;
|
|
3363
|
+
throw new CliError("CLI_INVALID_INPUT", message, [
|
|
3364
|
+
{ field: ARTIFACT_PATH_INPUT, message },
|
|
3365
|
+
{ field: ARTIFACT_FILES_INPUT, message }
|
|
3366
|
+
]);
|
|
3367
|
+
}
|
|
3368
|
+
if (localPath === undefined) {
|
|
3369
|
+
const files = given;
|
|
3370
|
+
checkGivenFiles(files);
|
|
3371
|
+
return files;
|
|
3372
|
+
}
|
|
3373
|
+
const local = await readLocalArtifact(String(localPath), {
|
|
3374
|
+
workingDirectory: context.workingDirectory,
|
|
3375
|
+
homeDirectory: context.homeDirectory,
|
|
3376
|
+
requireAbsolute: context.requireAbsolutePaths
|
|
3377
|
+
});
|
|
3378
|
+
const totalBytes = local.files.reduce((sum, file) => sum + file.size, 0);
|
|
3379
|
+
const named = local.skipped.slice(0, SKIPPED_NAMED).join(", ");
|
|
3380
|
+
const more = local.skipped.length > SKIPPED_NAMED ? ", ..." : "";
|
|
3381
|
+
context.progress(`found ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${mebibytes2(totalBytes)}) in ${local.root}${local.skipped.length > 0 ? `, skipped ${local.skipped.length}: ${named}${more}` : ""}`);
|
|
3382
|
+
return encodeArtifactFiles(local.files);
|
|
3383
|
+
}
|
|
3384
|
+
async function publishArtifact(context) {
|
|
3385
|
+
const { client, operation, signal } = context;
|
|
3386
|
+
if (operation.body.mode !== "upload") {
|
|
3387
|
+
throw new CliError("CLI_INTERNAL", `${operation.operationId} is not an upload`);
|
|
3388
|
+
}
|
|
3389
|
+
const files = await artifactFiles(context);
|
|
3390
|
+
const body = {};
|
|
3391
|
+
for (const property of operation.body.bodyInputs) {
|
|
3392
|
+
if (context.input[property] !== undefined) {
|
|
3393
|
+
body[property] = context.input[property];
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
body[ARTIFACT_FILES_INPUT] = files;
|
|
3397
|
+
const request = { ...context.request, body };
|
|
3398
|
+
if (context.dryRun) {
|
|
3399
|
+
const preview = {
|
|
3400
|
+
...body,
|
|
3401
|
+
files: files.map(({ content, ...file }) => ({ ...file, contentLength: content.length }))
|
|
3402
|
+
};
|
|
3403
|
+
return {
|
|
3404
|
+
dryRun: true,
|
|
3405
|
+
method: request.method,
|
|
3406
|
+
url: client.url(request),
|
|
3407
|
+
headers: client.publicHeaders({ headers: request.headers, body: preview }),
|
|
3408
|
+
body: preview,
|
|
3409
|
+
...operation.annotation.concurrency === "etag" && request.headers?.["If-Match"] === undefined ? {
|
|
3410
|
+
ifMatchFrom: {
|
|
3411
|
+
method: "GET",
|
|
3412
|
+
url: client.url({ path: request.path }),
|
|
3413
|
+
headers: { "Accept-Encoding": ETAG_READ_ENCODING }
|
|
3414
|
+
}
|
|
3415
|
+
} : {}
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
if (signal.aborted) {
|
|
3419
|
+
throw new CliError("CLI_TASK_FAILED", "Interrupted before the publish. Nothing was changed", {
|
|
3420
|
+
interrupted: true
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
context.progress(`sending ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
3424
|
+
const answer = await executeOperation(operation, request, { client, clock: context.clock }, { wait: false, timeoutSeconds: context.timeoutSeconds });
|
|
3425
|
+
if (answer === null || typeof answer !== "object" || Array.isArray(answer)) {
|
|
3426
|
+
throw new CliError("CLI_BAD_RESPONSE", "The API answered the publish with no object", {
|
|
3427
|
+
answer
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
const warnings = answer.warnings;
|
|
3431
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
3432
|
+
context.progress(`warning: ${warnings.length} thing${warnings.length === 1 ? "" : "s"} in the content will not work, see warnings in the answer`);
|
|
3433
|
+
}
|
|
3434
|
+
return answer;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// src/uploads/interrupted.ts
|
|
3438
|
+
class Interrupted extends Error {
|
|
3439
|
+
constructor() {
|
|
3440
|
+
super("Interrupted");
|
|
3441
|
+
this.name = "Interrupted";
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3131
3445
|
// src/uploads/pages/finalize.ts
|
|
3132
3446
|
var POLL_INTERVAL_MS = 5000;
|
|
3133
3447
|
var SUCCEEDED = new Set(["live", "superseded"]);
|
|
@@ -3205,7 +3519,7 @@ async function waitForDeployment(options) {
|
|
|
3205
3519
|
|
|
3206
3520
|
// src/uploads/pages/manifest.ts
|
|
3207
3521
|
import { createHash as createHash3 } from "node:crypto";
|
|
3208
|
-
import { readFile as
|
|
3522
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
3209
3523
|
|
|
3210
3524
|
// src/uploads/pages/rules.ts
|
|
3211
3525
|
var LIMIT_NAMES = ["fileBytes", "totalBytes", "fileCount", "textBytes"];
|
|
@@ -3298,7 +3612,7 @@ function sha256Hex2(bytes) {
|
|
|
3298
3612
|
async function readSiteFile(file) {
|
|
3299
3613
|
let bytes;
|
|
3300
3614
|
try {
|
|
3301
|
-
bytes = await
|
|
3615
|
+
bytes = await readFile5(file.absolutePath);
|
|
3302
3616
|
} catch (error) {
|
|
3303
3617
|
throw new CliError("CLI_INVALID_INPUT", `${file.path} could not be read`, {
|
|
3304
3618
|
path: file.path,
|
|
@@ -3662,7 +3976,7 @@ async function uploadFiles(options) {
|
|
|
3662
3976
|
|
|
3663
3977
|
// src/uploads/pagesDeployment.ts
|
|
3664
3978
|
var PROGRESS_INTERVAL_MS = 1000;
|
|
3665
|
-
var
|
|
3979
|
+
var SKIPPED_NAMED2 = 5;
|
|
3666
3980
|
function badResponse2(message) {
|
|
3667
3981
|
return new CliError("CLI_BAD_RESPONSE", message);
|
|
3668
3982
|
}
|
|
@@ -3694,7 +4008,7 @@ function parseStart(body) {
|
|
|
3694
4008
|
}
|
|
3695
4009
|
};
|
|
3696
4010
|
}
|
|
3697
|
-
function
|
|
4011
|
+
function mebibytes3(bytes) {
|
|
3698
4012
|
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3699
4013
|
}
|
|
3700
4014
|
function throttled(context) {
|
|
@@ -3740,9 +4054,9 @@ async function deployPagesSite(context) {
|
|
|
3740
4054
|
const walk = await walkSiteDirectory(root);
|
|
3741
4055
|
files = walk.files;
|
|
3742
4056
|
totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
3743
|
-
const named = walk.skipped.slice(0,
|
|
3744
|
-
const more = walk.skipped.length >
|
|
3745
|
-
context.progress(`found ${files.length} files (${
|
|
4057
|
+
const named = walk.skipped.slice(0, SKIPPED_NAMED2).join(", ");
|
|
4058
|
+
const more = walk.skipped.length > SKIPPED_NAMED2 ? ", ..." : "";
|
|
4059
|
+
context.progress(`found ${files.length} files (${mebibytes3(totalBytes)}) in ${root}${walk.skipped.length > 0 ? `, skipped ${walk.skipped.length}: ${named}${more}` : ""}`);
|
|
3746
4060
|
checkLimits(files, rules);
|
|
3747
4061
|
const hashProgress = throttled(context);
|
|
3748
4062
|
manifest = await buildManifest(files, rules, {
|
|
@@ -3799,7 +4113,7 @@ async function deployPagesSite(context) {
|
|
|
3799
4113
|
context.progress("warning: the API sent no Date header, so the upload window is timed by the local clock");
|
|
3800
4114
|
}
|
|
3801
4115
|
const uploadBytes = start.uploads.reduce((sum, upload) => sum + (byPath.get(upload.path)?.size ?? 0), 0);
|
|
3802
|
-
context.progress(`uploading ${start.uploads.length} files (${
|
|
4116
|
+
context.progress(`uploading ${start.uploads.length} files (${mebibytes3(uploadBytes)}) to the deployment ${deployment.id}, ${files.length - start.uploads.length} unchanged`);
|
|
3803
4117
|
const uploadProgress = throttled(context);
|
|
3804
4118
|
const uploaded = await uploadFiles({
|
|
3805
4119
|
uploads: start.uploads,
|
|
@@ -3824,7 +4138,7 @@ async function deployPagesSite(context) {
|
|
|
3824
4138
|
fetch: context.storageFetch,
|
|
3825
4139
|
clock: context.clock,
|
|
3826
4140
|
signal,
|
|
3827
|
-
onUploaded: (progress) => uploadProgress(`uploaded ${progress.files} of ${start.uploads.length} files (${
|
|
4141
|
+
onUploaded: (progress) => uploadProgress(`uploaded ${progress.files} of ${start.uploads.length} files (${mebibytes3(progress.bytes)})`, progress.files === start.uploads.length)
|
|
3828
4142
|
});
|
|
3829
4143
|
signal.throwIfAborted();
|
|
3830
4144
|
context.progress(`finalizing the deployment ${deployment.id}`);
|
|
@@ -3894,7 +4208,8 @@ async function cancelDeployment(context, deploymentPath, deploymentId) {
|
|
|
3894
4208
|
|
|
3895
4209
|
// src/uploads/registry.ts
|
|
3896
4210
|
var UPLOADERS = new Map([
|
|
3897
|
-
["pages-deployment", deployPagesSite]
|
|
4211
|
+
["pages-deployment", deployPagesSite],
|
|
4212
|
+
["artifact", publishArtifact]
|
|
3898
4213
|
]);
|
|
3899
4214
|
|
|
3900
4215
|
// src/mcp/tools.ts
|
|
@@ -4943,7 +5258,7 @@ var BY_NAME = new Map(GLOBAL_FLAGS.map((flag) => [flag.name, flag]));
|
|
|
4943
5258
|
function isGlobalFlagName(name) {
|
|
4944
5259
|
return BY_NAME.has(name);
|
|
4945
5260
|
}
|
|
4946
|
-
function
|
|
5261
|
+
function invalid5(message, field) {
|
|
4947
5262
|
return new CliError("CLI_INVALID_INPUT", message, [{ field, message }]);
|
|
4948
5263
|
}
|
|
4949
5264
|
function extractGlobalFlags(argv) {
|
|
@@ -4969,17 +5284,17 @@ function extractGlobalFlags(argv) {
|
|
|
4969
5284
|
const inline = match[2];
|
|
4970
5285
|
if (!spec.takesValue) {
|
|
4971
5286
|
if (inline !== undefined) {
|
|
4972
|
-
throw
|
|
5287
|
+
throw invalid5(`--${spec.name} takes no value`, spec.name);
|
|
4973
5288
|
}
|
|
4974
5289
|
switches.add(spec.name);
|
|
4975
5290
|
continue;
|
|
4976
5291
|
}
|
|
4977
5292
|
if (values.has(spec.name)) {
|
|
4978
|
-
throw
|
|
5293
|
+
throw invalid5(`--${spec.name} is given twice`, spec.name);
|
|
4979
5294
|
}
|
|
4980
5295
|
const value = inline ?? argv[index + 1];
|
|
4981
5296
|
if (value === undefined) {
|
|
4982
|
-
throw
|
|
5297
|
+
throw invalid5(`--${spec.name} needs a value`, spec.name);
|
|
4983
5298
|
}
|
|
4984
5299
|
if (inline === undefined) {
|
|
4985
5300
|
index += 1;
|
|
@@ -4987,14 +5302,14 @@ function extractGlobalFlags(argv) {
|
|
|
4987
5302
|
values.set(spec.name, value);
|
|
4988
5303
|
}
|
|
4989
5304
|
if (switches.has("wait") && switches.has("no-wait")) {
|
|
4990
|
-
throw
|
|
5305
|
+
throw invalid5("--wait and --no-wait cannot both be given", "wait");
|
|
4991
5306
|
}
|
|
4992
5307
|
let timeoutSeconds = DEFAULT_WAIT_SECONDS;
|
|
4993
5308
|
const timeout = values.get("timeout");
|
|
4994
5309
|
if (timeout !== undefined) {
|
|
4995
5310
|
timeoutSeconds = Number(timeout);
|
|
4996
5311
|
if (!/^\d+$/.test(timeout) || timeoutSeconds < 1) {
|
|
4997
|
-
throw
|
|
5312
|
+
throw invalid5("--timeout must be a whole number of seconds, 1 or more", "timeout");
|
|
4998
5313
|
}
|
|
4999
5314
|
}
|
|
5000
5315
|
const fields = values.get("fields");
|
|
@@ -5106,7 +5421,7 @@ function coerce(kind, text) {
|
|
|
5106
5421
|
}
|
|
5107
5422
|
return text;
|
|
5108
5423
|
}
|
|
5109
|
-
function
|
|
5424
|
+
function invalid6(message, problems) {
|
|
5110
5425
|
return new CliError("CLI_INVALID_INPUT", message, problems);
|
|
5111
5426
|
}
|
|
5112
5427
|
function parsePayload(text) {
|
|
@@ -5114,12 +5429,12 @@ function parsePayload(text) {
|
|
|
5114
5429
|
try {
|
|
5115
5430
|
value = JSON.parse(text);
|
|
5116
5431
|
} catch (error) {
|
|
5117
|
-
throw
|
|
5432
|
+
throw invalid6("--json is not valid JSON", [
|
|
5118
5433
|
{ field: "json", message: error instanceof Error ? error.message : "is not valid JSON" }
|
|
5119
5434
|
]);
|
|
5120
5435
|
}
|
|
5121
5436
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
5122
|
-
throw
|
|
5437
|
+
throw invalid6("--json must be one JSON object", [
|
|
5123
5438
|
{ field: "json", message: "must be an object" }
|
|
5124
5439
|
]);
|
|
5125
5440
|
}
|
|
@@ -5201,7 +5516,7 @@ function buildInput(options) {
|
|
|
5201
5516
|
}
|
|
5202
5517
|
}
|
|
5203
5518
|
if (problems.length > 0) {
|
|
5204
|
-
throw
|
|
5519
|
+
throw invalid6("The arguments are not valid", problems);
|
|
5205
5520
|
}
|
|
5206
5521
|
return { ...options.payload, ...fromArguments };
|
|
5207
5522
|
}
|