@lazco/cli 0.1.0 → 0.2.0
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 +13 -0
- package/README.md +15 -0
- package/dist/cli.js +345 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `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.
|
|
11
|
+
In a directory, every file or directory whose name starts with a dot is skipped, so a `.env` is never published.
|
|
12
|
+
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.
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- The browser page that ends an SSO sign-in uses the Lazco design, in the dark theme.
|
|
18
|
+
|
|
6
19
|
## [0.1.0]
|
|
7
20
|
|
|
8
21
|
### Added
|
package/README.md
CHANGED
|
@@ -120,6 +120,21 @@ The command checks the API's limits before it sends anything, uploads only the f
|
|
|
120
120
|
A failed or interrupted upload cancels the deployment, so the next deploy can start at once.
|
|
121
121
|
`--dry-run` reads the limits and the site, and prints the manifest it would send.
|
|
122
122
|
|
|
123
|
+
### Publish a Lazco Artifact
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
lazco labs artifacts publish ./report.md --region <region> --title "<title>"
|
|
127
|
+
lazco labs artifacts publish ./report --region <region> --title "<title>"
|
|
128
|
+
lazco labs artifacts update <artifact-id> ./report
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The path is one HTML or Markdown file, or a directory with one entry file and the images, CSS and fonts it uses.
|
|
132
|
+
Anyone with the link the answer holds in `url` can open it.
|
|
133
|
+
In a directory, every file or directory whose name starts with a dot is skipped, so a `.env` is never published.
|
|
134
|
+
The command refuses a file type a publish may not hold, more than 100 files and more than 5 MiB before it sends anything.
|
|
135
|
+
The answer's `warnings` lists what the page loads from outside, misses or runs, which will not work.
|
|
136
|
+
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.
|
|
137
|
+
|
|
123
138
|
## Use it from an MCP client
|
|
124
139
|
|
|
125
140
|
`lazco mcp` is a local MCP server with the same sign-in, and it renews an SSO sign-in as it runs.
|
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.
|
|
20
|
+
version: "0.2.0",
|
|
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
|
`;
|
|
@@ -2112,8 +2138,28 @@ var PAGES_DEPLOYMENT_INPUT = {
|
|
|
2112
2138
|
"When the call ends before the deployment does, the deployment goes on: read it with the deployment read tool."
|
|
2113
2139
|
]
|
|
2114
2140
|
};
|
|
2141
|
+
var ARTIFACT_PATH_INPUT = "path";
|
|
2142
|
+
var ARTIFACT_FILES_INPUT = "files";
|
|
2143
|
+
var ARTIFACT_INPUT = {
|
|
2144
|
+
properties: {
|
|
2145
|
+
[ARTIFACT_PATH_INPUT]: {
|
|
2146
|
+
type: "string",
|
|
2147
|
+
minLength: 1,
|
|
2148
|
+
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."
|
|
2149
|
+
}
|
|
2150
|
+
},
|
|
2151
|
+
required: [],
|
|
2152
|
+
positionals: [ARTIFACT_PATH_INPUT],
|
|
2153
|
+
toolNotes: [
|
|
2154
|
+
"Publishes content that anyone with the link can open. Only publish what the user asked to publish.",
|
|
2155
|
+
"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.",
|
|
2156
|
+
"The answer holds url, the permanent link, and warnings: what the page loads from outside, misses or runs, which will not work."
|
|
2157
|
+
],
|
|
2158
|
+
fills: [ARTIFACT_FILES_INPUT]
|
|
2159
|
+
};
|
|
2115
2160
|
var KNOWN_UPLOADERS = new Map([
|
|
2116
|
-
["pages-deployment", PAGES_DEPLOYMENT_INPUT]
|
|
2161
|
+
["pages-deployment", PAGES_DEPLOYMENT_INPUT],
|
|
2162
|
+
["artifact", ARTIFACT_INPUT]
|
|
2117
2163
|
]);
|
|
2118
2164
|
var COMMAND_WORD = "[a-z0-9][a-z0-9-]*";
|
|
2119
2165
|
var ANNOTATION_SCHEMA = {
|
|
@@ -2447,7 +2493,7 @@ function buildOperation(document, path, method, source, annotation, pathLevelPar
|
|
|
2447
2493
|
description: "The ETag of the version this change is based on, sent as If-Match. Without it, the current ETag is read first."
|
|
2448
2494
|
};
|
|
2449
2495
|
}
|
|
2450
|
-
const body = uploader === undefined ? mergeBody(document, source, properties, required) : addUploaderInput(annotation.upload, uploader, properties, required);
|
|
2496
|
+
const body = uploader === undefined ? mergeBody(document, source, properties, required) : addUploaderInput(document, source, annotation.upload, uploader, properties, required);
|
|
2451
2497
|
let resolved;
|
|
2452
2498
|
try {
|
|
2453
2499
|
resolved = resolveReferences(document, {
|
|
@@ -2505,7 +2551,23 @@ function withoutQuoteInputs(schema, mapping, parameters) {
|
|
|
2505
2551
|
required: required.filter((name) => !QUOTE_INPUTS.includes(name))
|
|
2506
2552
|
};
|
|
2507
2553
|
}
|
|
2508
|
-
function addUploaderInput(name, uploader, properties, required) {
|
|
2554
|
+
function addUploaderInput(document, source, name, uploader, properties, required) {
|
|
2555
|
+
const bodyInputs = [];
|
|
2556
|
+
if (uploader.fills !== undefined) {
|
|
2557
|
+
const before = new Set(Object.keys(properties));
|
|
2558
|
+
const merged = mergeBody(document, source, properties, required);
|
|
2559
|
+
bodyInputs.push(...Object.keys(properties).filter((property) => !before.has(property)));
|
|
2560
|
+
const missing = uploader.fills.filter((property) => !bodyInputs.includes(property));
|
|
2561
|
+
if (merged.mapping.mode !== "merged" || missing.length > 0) {
|
|
2562
|
+
throw new SkipError("unsupported-body", `the uploader "${name}" needs a JSON object body with ${uploader.fills.join(", ")}`);
|
|
2563
|
+
}
|
|
2564
|
+
for (const property of uploader.fills) {
|
|
2565
|
+
const index = required.indexOf(property);
|
|
2566
|
+
if (index !== -1) {
|
|
2567
|
+
required.splice(index, 1);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2509
2571
|
for (const [property, schema] of Object.entries(uploader.properties)) {
|
|
2510
2572
|
if (Object.hasOwn(properties, property)) {
|
|
2511
2573
|
throw new SkipError("unsupported-parameter", `the parameter "${property}" clashes with an input of the uploader "${name}"`);
|
|
@@ -2513,7 +2575,7 @@ function addUploaderInput(name, uploader, properties, required) {
|
|
|
2513
2575
|
properties[property] = { ...schema };
|
|
2514
2576
|
}
|
|
2515
2577
|
required.push(...uploader.required);
|
|
2516
|
-
return { mapping: { mode: "upload", uploader: name }, additionalProperties: false };
|
|
2578
|
+
return { mapping: { mode: "upload", uploader: name, bodyInputs }, additionalProperties: false };
|
|
2517
2579
|
}
|
|
2518
2580
|
function collectParameters(document, ...lists) {
|
|
2519
2581
|
const byKey = new Map;
|
|
@@ -2992,13 +3054,9 @@ async function walkPages(operation, input, prepare, context, onPage) {
|
|
|
2992
3054
|
}
|
|
2993
3055
|
}
|
|
2994
3056
|
|
|
2995
|
-
// src/uploads/
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
super("Interrupted");
|
|
2999
|
-
this.name = "Interrupted";
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3057
|
+
// src/uploads/artifacts/files.ts
|
|
3058
|
+
import { lstat as lstat2, readdir as readdir2, readFile as readFile4, realpath as realpath3, stat as stat2 } from "node:fs/promises";
|
|
3059
|
+
import nodePath2 from "node:path";
|
|
3002
3060
|
|
|
3003
3061
|
// src/uploads/pages/directory.ts
|
|
3004
3062
|
import { lstat, open, readdir, realpath as realpath2, stat } from "node:fs/promises";
|
|
@@ -3128,6 +3186,256 @@ async function walkSiteDirectory(root) {
|
|
|
3128
3186
|
return { files, skipped };
|
|
3129
3187
|
}
|
|
3130
3188
|
|
|
3189
|
+
// src/uploads/artifacts/files.ts
|
|
3190
|
+
var MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
|
|
3191
|
+
var MAX_ARTIFACT_FILES = 100;
|
|
3192
|
+
var ENCODING_BY_EXTENSION = new Map([
|
|
3193
|
+
[".html", "utf-8"],
|
|
3194
|
+
[".htm", "utf-8"],
|
|
3195
|
+
[".md", "utf-8"],
|
|
3196
|
+
[".css", "utf-8"],
|
|
3197
|
+
[".svg", "utf-8"],
|
|
3198
|
+
[".png", "base64"],
|
|
3199
|
+
[".jpg", "base64"],
|
|
3200
|
+
[".jpeg", "base64"],
|
|
3201
|
+
[".gif", "base64"],
|
|
3202
|
+
[".webp", "base64"],
|
|
3203
|
+
[".avif", "base64"],
|
|
3204
|
+
[".woff2", "base64"],
|
|
3205
|
+
[".woff", "base64"]
|
|
3206
|
+
]);
|
|
3207
|
+
function invalid4(message, details) {
|
|
3208
|
+
return new CliError("CLI_INVALID_INPUT", message, details);
|
|
3209
|
+
}
|
|
3210
|
+
function encodingOf(path) {
|
|
3211
|
+
return ENCODING_BY_EXTENSION.get(nodePath2.posix.extname(path).toLowerCase());
|
|
3212
|
+
}
|
|
3213
|
+
function mebibytes(bytes) {
|
|
3214
|
+
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3215
|
+
}
|
|
3216
|
+
function checkArtifactLimits(files, field) {
|
|
3217
|
+
const unknownType = files.filter((file) => encodingOf(file.path) === undefined);
|
|
3218
|
+
if (unknownType.length > 0) {
|
|
3219
|
+
throw invalid4(`A publish holds only HTML, Markdown, CSS, images and fonts, so ${unknownType.map((file) => file.path).join(", ")} cannot be published`, {
|
|
3220
|
+
field,
|
|
3221
|
+
files: unknownType.map((file) => file.path),
|
|
3222
|
+
allowed: [...ENCODING_BY_EXTENSION.keys()]
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
if (files.length > MAX_ARTIFACT_FILES) {
|
|
3226
|
+
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 });
|
|
3227
|
+
}
|
|
3228
|
+
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
3229
|
+
if (totalBytes > MAX_ARTIFACT_BYTES) {
|
|
3230
|
+
throw invalid4(`A publish holds at most ${mebibytes(MAX_ARTIFACT_BYTES)}, and this one has ${mebibytes(totalBytes)}`, { field, bytes: totalBytes, limit: MAX_ARTIFACT_BYTES });
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
function decodedSize(file) {
|
|
3234
|
+
return file.encoding === "base64" ? Buffer.from(file.content, "base64").length : Buffer.byteLength(file.content, "utf8");
|
|
3235
|
+
}
|
|
3236
|
+
async function readLocalArtifact(localPath, options) {
|
|
3237
|
+
const problem = (message) => invalid4(message, [{ field: ARTIFACT_PATH_INPUT, message }]);
|
|
3238
|
+
if (options.requireAbsolute && !nodePath2.isAbsolute(localPath)) {
|
|
3239
|
+
throw problem(`path must be an absolute path, because the working directory of the MCP server is the client's choice: ${localPath}`);
|
|
3240
|
+
}
|
|
3241
|
+
const given = nodePath2.resolve(options.workingDirectory, localPath);
|
|
3242
|
+
let root;
|
|
3243
|
+
let info;
|
|
3244
|
+
try {
|
|
3245
|
+
root = await realpath3(given);
|
|
3246
|
+
info = await stat2(root);
|
|
3247
|
+
} catch {
|
|
3248
|
+
throw problem(`${given} does not exist or cannot be read`);
|
|
3249
|
+
}
|
|
3250
|
+
if (info.isFile()) {
|
|
3251
|
+
const files = [{ path: nodePath2.basename(root), absolutePath: root, size: info.size }];
|
|
3252
|
+
checkArtifactLimits(files, ARTIFACT_PATH_INPUT);
|
|
3253
|
+
return { root, files, skipped: [] };
|
|
3254
|
+
}
|
|
3255
|
+
if (!info.isDirectory()) {
|
|
3256
|
+
throw problem(`${given} is not a file or a directory`);
|
|
3257
|
+
}
|
|
3258
|
+
if (isFilesystemRoot(root)) {
|
|
3259
|
+
throw problem(`${given} is a filesystem root, which is never published`);
|
|
3260
|
+
}
|
|
3261
|
+
const home = await realpath3(options.homeDirectory).catch(() => {
|
|
3262
|
+
return;
|
|
3263
|
+
});
|
|
3264
|
+
if (home !== undefined && (root === home || holds(root, home, nodePath2))) {
|
|
3265
|
+
throw problem(`${given} is or holds the home directory, which is never published`);
|
|
3266
|
+
}
|
|
3267
|
+
const { files, skipped } = await walkArtifactDirectory(root);
|
|
3268
|
+
checkArtifactLimits(files, ARTIFACT_PATH_INPUT);
|
|
3269
|
+
return { root, files, skipped };
|
|
3270
|
+
}
|
|
3271
|
+
async function walkArtifactDirectory(root) {
|
|
3272
|
+
const files = [];
|
|
3273
|
+
const skipped = [];
|
|
3274
|
+
const pathInArtifact = (absolutePath) => nodePath2.relative(root, absolutePath).split(nodePath2.sep).join("/");
|
|
3275
|
+
const unreadable = (path, error) => invalid4(`${path || "."} could not be read`, {
|
|
3276
|
+
path: path || ".",
|
|
3277
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
3278
|
+
});
|
|
3279
|
+
const visit = async (directory) => {
|
|
3280
|
+
let names;
|
|
3281
|
+
try {
|
|
3282
|
+
names = (await readdir2(directory)).sort();
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
throw unreadable(pathInArtifact(directory), error);
|
|
3285
|
+
}
|
|
3286
|
+
for (const name of names) {
|
|
3287
|
+
const absolutePath = nodePath2.join(directory, name);
|
|
3288
|
+
const path = pathInArtifact(absolutePath);
|
|
3289
|
+
if (name.startsWith(".")) {
|
|
3290
|
+
skipped.push(path);
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
let entry;
|
|
3294
|
+
try {
|
|
3295
|
+
entry = await lstat2(absolutePath);
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
throw unreadable(path, error);
|
|
3298
|
+
}
|
|
3299
|
+
if (entry.isSymbolicLink()) {
|
|
3300
|
+
throw invalid4(`${path} is a link. A publish never follows a link, so remove it first`, {
|
|
3301
|
+
path
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
if (entry.isDirectory()) {
|
|
3305
|
+
await visit(absolutePath);
|
|
3306
|
+
} else if (entry.isFile()) {
|
|
3307
|
+
files.push({ path, absolutePath, size: entry.size });
|
|
3308
|
+
if (files.length > MAX_ARTIFACT_FILES) {
|
|
3309
|
+
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 });
|
|
3310
|
+
}
|
|
3311
|
+
} else {
|
|
3312
|
+
throw invalid4(`${path} is not a file or a directory, so it cannot be published`, {
|
|
3313
|
+
path
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
await visit(root);
|
|
3319
|
+
if (files.length === 0) {
|
|
3320
|
+
throw invalid4(`${root} holds no file to publish${skipped.length > 0 ? ` (${skipped.length} skipped)` : ""}`, [{ field: ARTIFACT_PATH_INPUT, message: "holds no file to publish" }]);
|
|
3321
|
+
}
|
|
3322
|
+
files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
3323
|
+
return { files, skipped };
|
|
3324
|
+
}
|
|
3325
|
+
async function encodeArtifactFiles(files) {
|
|
3326
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
3327
|
+
return Promise.all(files.map(async (file) => {
|
|
3328
|
+
const bytes = await readFile4(file.absolutePath);
|
|
3329
|
+
const encoding = encodingOf(file.path);
|
|
3330
|
+
if (encoding === "base64") {
|
|
3331
|
+
return { path: file.path, encoding, content: bytes.toString("base64") };
|
|
3332
|
+
}
|
|
3333
|
+
try {
|
|
3334
|
+
return { path: file.path, encoding, content: decoder.decode(bytes) };
|
|
3335
|
+
} catch {
|
|
3336
|
+
throw invalid4(`${file.path} is not valid UTF-8, so it cannot be published as text`, {
|
|
3337
|
+
field: ARTIFACT_PATH_INPUT,
|
|
3338
|
+
path: file.path
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}));
|
|
3342
|
+
}
|
|
3343
|
+
function checkGivenFiles(files) {
|
|
3344
|
+
checkArtifactLimits(files.map((file) => ({ path: file.path, size: decodedSize(file) })), ARTIFACT_FILES_INPUT);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// src/uploads/artifact.ts
|
|
3348
|
+
var SKIPPED_NAMED = 5;
|
|
3349
|
+
function mebibytes2(bytes) {
|
|
3350
|
+
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3351
|
+
}
|
|
3352
|
+
async function artifactFiles(context) {
|
|
3353
|
+
const localPath = context.input[ARTIFACT_PATH_INPUT];
|
|
3354
|
+
const given = context.input[ARTIFACT_FILES_INPUT];
|
|
3355
|
+
if (localPath === undefined === (given === undefined)) {
|
|
3356
|
+
const message = `Give either ${ARTIFACT_PATH_INPUT}, a local file or directory, or ${ARTIFACT_FILES_INPUT}, and not both`;
|
|
3357
|
+
throw new CliError("CLI_INVALID_INPUT", message, [
|
|
3358
|
+
{ field: ARTIFACT_PATH_INPUT, message },
|
|
3359
|
+
{ field: ARTIFACT_FILES_INPUT, message }
|
|
3360
|
+
]);
|
|
3361
|
+
}
|
|
3362
|
+
if (localPath === undefined) {
|
|
3363
|
+
const files = given;
|
|
3364
|
+
checkGivenFiles(files);
|
|
3365
|
+
return files;
|
|
3366
|
+
}
|
|
3367
|
+
const local = await readLocalArtifact(String(localPath), {
|
|
3368
|
+
workingDirectory: context.workingDirectory,
|
|
3369
|
+
homeDirectory: context.homeDirectory,
|
|
3370
|
+
requireAbsolute: context.requireAbsolutePaths
|
|
3371
|
+
});
|
|
3372
|
+
const totalBytes = local.files.reduce((sum, file) => sum + file.size, 0);
|
|
3373
|
+
const named = local.skipped.slice(0, SKIPPED_NAMED).join(", ");
|
|
3374
|
+
const more = local.skipped.length > SKIPPED_NAMED ? ", ..." : "";
|
|
3375
|
+
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}` : ""}`);
|
|
3376
|
+
return encodeArtifactFiles(local.files);
|
|
3377
|
+
}
|
|
3378
|
+
async function publishArtifact(context) {
|
|
3379
|
+
const { client, operation, signal } = context;
|
|
3380
|
+
if (operation.body.mode !== "upload") {
|
|
3381
|
+
throw new CliError("CLI_INTERNAL", `${operation.operationId} is not an upload`);
|
|
3382
|
+
}
|
|
3383
|
+
const files = await artifactFiles(context);
|
|
3384
|
+
const body = {};
|
|
3385
|
+
for (const property of operation.body.bodyInputs) {
|
|
3386
|
+
if (context.input[property] !== undefined) {
|
|
3387
|
+
body[property] = context.input[property];
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
body[ARTIFACT_FILES_INPUT] = files;
|
|
3391
|
+
const request = { ...context.request, body };
|
|
3392
|
+
if (context.dryRun) {
|
|
3393
|
+
const preview = {
|
|
3394
|
+
...body,
|
|
3395
|
+
files: files.map(({ content, ...file }) => ({ ...file, contentLength: content.length }))
|
|
3396
|
+
};
|
|
3397
|
+
return {
|
|
3398
|
+
dryRun: true,
|
|
3399
|
+
method: request.method,
|
|
3400
|
+
url: client.url(request),
|
|
3401
|
+
headers: client.publicHeaders({ headers: request.headers, body: preview }),
|
|
3402
|
+
body: preview,
|
|
3403
|
+
...operation.annotation.concurrency === "etag" && request.headers?.["If-Match"] === undefined ? {
|
|
3404
|
+
ifMatchFrom: {
|
|
3405
|
+
method: "GET",
|
|
3406
|
+
url: client.url({ path: request.path }),
|
|
3407
|
+
headers: { "Accept-Encoding": ETAG_READ_ENCODING }
|
|
3408
|
+
}
|
|
3409
|
+
} : {}
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
if (signal.aborted) {
|
|
3413
|
+
throw new CliError("CLI_TASK_FAILED", "Interrupted before the publish. Nothing was changed", {
|
|
3414
|
+
interrupted: true
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
context.progress(`sending ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
3418
|
+
const answer = await executeOperation(operation, request, { client, clock: context.clock }, { wait: false, timeoutSeconds: context.timeoutSeconds });
|
|
3419
|
+
if (answer === null || typeof answer !== "object" || Array.isArray(answer)) {
|
|
3420
|
+
throw new CliError("CLI_BAD_RESPONSE", "The API answered the publish with no object", {
|
|
3421
|
+
answer
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
const warnings = answer.warnings;
|
|
3425
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
3426
|
+
context.progress(`warning: ${warnings.length} thing${warnings.length === 1 ? "" : "s"} in the content will not work, see warnings in the answer`);
|
|
3427
|
+
}
|
|
3428
|
+
return answer;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// src/uploads/interrupted.ts
|
|
3432
|
+
class Interrupted extends Error {
|
|
3433
|
+
constructor() {
|
|
3434
|
+
super("Interrupted");
|
|
3435
|
+
this.name = "Interrupted";
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3131
3439
|
// src/uploads/pages/finalize.ts
|
|
3132
3440
|
var POLL_INTERVAL_MS = 5000;
|
|
3133
3441
|
var SUCCEEDED = new Set(["live", "superseded"]);
|
|
@@ -3205,7 +3513,7 @@ async function waitForDeployment(options) {
|
|
|
3205
3513
|
|
|
3206
3514
|
// src/uploads/pages/manifest.ts
|
|
3207
3515
|
import { createHash as createHash3 } from "node:crypto";
|
|
3208
|
-
import { readFile as
|
|
3516
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
3209
3517
|
|
|
3210
3518
|
// src/uploads/pages/rules.ts
|
|
3211
3519
|
var LIMIT_NAMES = ["fileBytes", "totalBytes", "fileCount", "textBytes"];
|
|
@@ -3298,7 +3606,7 @@ function sha256Hex2(bytes) {
|
|
|
3298
3606
|
async function readSiteFile(file) {
|
|
3299
3607
|
let bytes;
|
|
3300
3608
|
try {
|
|
3301
|
-
bytes = await
|
|
3609
|
+
bytes = await readFile5(file.absolutePath);
|
|
3302
3610
|
} catch (error) {
|
|
3303
3611
|
throw new CliError("CLI_INVALID_INPUT", `${file.path} could not be read`, {
|
|
3304
3612
|
path: file.path,
|
|
@@ -3662,7 +3970,7 @@ async function uploadFiles(options) {
|
|
|
3662
3970
|
|
|
3663
3971
|
// src/uploads/pagesDeployment.ts
|
|
3664
3972
|
var PROGRESS_INTERVAL_MS = 1000;
|
|
3665
|
-
var
|
|
3973
|
+
var SKIPPED_NAMED2 = 5;
|
|
3666
3974
|
function badResponse2(message) {
|
|
3667
3975
|
return new CliError("CLI_BAD_RESPONSE", message);
|
|
3668
3976
|
}
|
|
@@ -3694,7 +4002,7 @@ function parseStart(body) {
|
|
|
3694
4002
|
}
|
|
3695
4003
|
};
|
|
3696
4004
|
}
|
|
3697
|
-
function
|
|
4005
|
+
function mebibytes3(bytes) {
|
|
3698
4006
|
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
3699
4007
|
}
|
|
3700
4008
|
function throttled(context) {
|
|
@@ -3740,9 +4048,9 @@ async function deployPagesSite(context) {
|
|
|
3740
4048
|
const walk = await walkSiteDirectory(root);
|
|
3741
4049
|
files = walk.files;
|
|
3742
4050
|
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 (${
|
|
4051
|
+
const named = walk.skipped.slice(0, SKIPPED_NAMED2).join(", ");
|
|
4052
|
+
const more = walk.skipped.length > SKIPPED_NAMED2 ? ", ..." : "";
|
|
4053
|
+
context.progress(`found ${files.length} files (${mebibytes3(totalBytes)}) in ${root}${walk.skipped.length > 0 ? `, skipped ${walk.skipped.length}: ${named}${more}` : ""}`);
|
|
3746
4054
|
checkLimits(files, rules);
|
|
3747
4055
|
const hashProgress = throttled(context);
|
|
3748
4056
|
manifest = await buildManifest(files, rules, {
|
|
@@ -3799,7 +4107,7 @@ async function deployPagesSite(context) {
|
|
|
3799
4107
|
context.progress("warning: the API sent no Date header, so the upload window is timed by the local clock");
|
|
3800
4108
|
}
|
|
3801
4109
|
const uploadBytes = start.uploads.reduce((sum, upload) => sum + (byPath.get(upload.path)?.size ?? 0), 0);
|
|
3802
|
-
context.progress(`uploading ${start.uploads.length} files (${
|
|
4110
|
+
context.progress(`uploading ${start.uploads.length} files (${mebibytes3(uploadBytes)}) to the deployment ${deployment.id}, ${files.length - start.uploads.length} unchanged`);
|
|
3803
4111
|
const uploadProgress = throttled(context);
|
|
3804
4112
|
const uploaded = await uploadFiles({
|
|
3805
4113
|
uploads: start.uploads,
|
|
@@ -3824,7 +4132,7 @@ async function deployPagesSite(context) {
|
|
|
3824
4132
|
fetch: context.storageFetch,
|
|
3825
4133
|
clock: context.clock,
|
|
3826
4134
|
signal,
|
|
3827
|
-
onUploaded: (progress) => uploadProgress(`uploaded ${progress.files} of ${start.uploads.length} files (${
|
|
4135
|
+
onUploaded: (progress) => uploadProgress(`uploaded ${progress.files} of ${start.uploads.length} files (${mebibytes3(progress.bytes)})`, progress.files === start.uploads.length)
|
|
3828
4136
|
});
|
|
3829
4137
|
signal.throwIfAborted();
|
|
3830
4138
|
context.progress(`finalizing the deployment ${deployment.id}`);
|
|
@@ -3894,7 +4202,8 @@ async function cancelDeployment(context, deploymentPath, deploymentId) {
|
|
|
3894
4202
|
|
|
3895
4203
|
// src/uploads/registry.ts
|
|
3896
4204
|
var UPLOADERS = new Map([
|
|
3897
|
-
["pages-deployment", deployPagesSite]
|
|
4205
|
+
["pages-deployment", deployPagesSite],
|
|
4206
|
+
["artifact", publishArtifact]
|
|
3898
4207
|
]);
|
|
3899
4208
|
|
|
3900
4209
|
// src/mcp/tools.ts
|
|
@@ -4943,7 +5252,7 @@ var BY_NAME = new Map(GLOBAL_FLAGS.map((flag) => [flag.name, flag]));
|
|
|
4943
5252
|
function isGlobalFlagName(name) {
|
|
4944
5253
|
return BY_NAME.has(name);
|
|
4945
5254
|
}
|
|
4946
|
-
function
|
|
5255
|
+
function invalid5(message, field) {
|
|
4947
5256
|
return new CliError("CLI_INVALID_INPUT", message, [{ field, message }]);
|
|
4948
5257
|
}
|
|
4949
5258
|
function extractGlobalFlags(argv) {
|
|
@@ -4969,17 +5278,17 @@ function extractGlobalFlags(argv) {
|
|
|
4969
5278
|
const inline = match[2];
|
|
4970
5279
|
if (!spec.takesValue) {
|
|
4971
5280
|
if (inline !== undefined) {
|
|
4972
|
-
throw
|
|
5281
|
+
throw invalid5(`--${spec.name} takes no value`, spec.name);
|
|
4973
5282
|
}
|
|
4974
5283
|
switches.add(spec.name);
|
|
4975
5284
|
continue;
|
|
4976
5285
|
}
|
|
4977
5286
|
if (values.has(spec.name)) {
|
|
4978
|
-
throw
|
|
5287
|
+
throw invalid5(`--${spec.name} is given twice`, spec.name);
|
|
4979
5288
|
}
|
|
4980
5289
|
const value = inline ?? argv[index + 1];
|
|
4981
5290
|
if (value === undefined) {
|
|
4982
|
-
throw
|
|
5291
|
+
throw invalid5(`--${spec.name} needs a value`, spec.name);
|
|
4983
5292
|
}
|
|
4984
5293
|
if (inline === undefined) {
|
|
4985
5294
|
index += 1;
|
|
@@ -4987,14 +5296,14 @@ function extractGlobalFlags(argv) {
|
|
|
4987
5296
|
values.set(spec.name, value);
|
|
4988
5297
|
}
|
|
4989
5298
|
if (switches.has("wait") && switches.has("no-wait")) {
|
|
4990
|
-
throw
|
|
5299
|
+
throw invalid5("--wait and --no-wait cannot both be given", "wait");
|
|
4991
5300
|
}
|
|
4992
5301
|
let timeoutSeconds = DEFAULT_WAIT_SECONDS;
|
|
4993
5302
|
const timeout = values.get("timeout");
|
|
4994
5303
|
if (timeout !== undefined) {
|
|
4995
5304
|
timeoutSeconds = Number(timeout);
|
|
4996
5305
|
if (!/^\d+$/.test(timeout) || timeoutSeconds < 1) {
|
|
4997
|
-
throw
|
|
5306
|
+
throw invalid5("--timeout must be a whole number of seconds, 1 or more", "timeout");
|
|
4998
5307
|
}
|
|
4999
5308
|
}
|
|
5000
5309
|
const fields = values.get("fields");
|
|
@@ -5106,7 +5415,7 @@ function coerce(kind, text) {
|
|
|
5106
5415
|
}
|
|
5107
5416
|
return text;
|
|
5108
5417
|
}
|
|
5109
|
-
function
|
|
5418
|
+
function invalid6(message, problems) {
|
|
5110
5419
|
return new CliError("CLI_INVALID_INPUT", message, problems);
|
|
5111
5420
|
}
|
|
5112
5421
|
function parsePayload(text) {
|
|
@@ -5114,12 +5423,12 @@ function parsePayload(text) {
|
|
|
5114
5423
|
try {
|
|
5115
5424
|
value = JSON.parse(text);
|
|
5116
5425
|
} catch (error) {
|
|
5117
|
-
throw
|
|
5426
|
+
throw invalid6("--json is not valid JSON", [
|
|
5118
5427
|
{ field: "json", message: error instanceof Error ? error.message : "is not valid JSON" }
|
|
5119
5428
|
]);
|
|
5120
5429
|
}
|
|
5121
5430
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
5122
|
-
throw
|
|
5431
|
+
throw invalid6("--json must be one JSON object", [
|
|
5123
5432
|
{ field: "json", message: "must be an object" }
|
|
5124
5433
|
]);
|
|
5125
5434
|
}
|
|
@@ -5201,7 +5510,7 @@ function buildInput(options) {
|
|
|
5201
5510
|
}
|
|
5202
5511
|
}
|
|
5203
5512
|
if (problems.length > 0) {
|
|
5204
|
-
throw
|
|
5513
|
+
throw invalid6("The arguments are not valid", problems);
|
|
5205
5514
|
}
|
|
5206
5515
|
return { ...options.payload, ...fromArguments };
|
|
5207
5516
|
}
|