@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 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.1.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><meta charset="utf-8"><title>Lazco CLI</title></head>
1114
- <body style="font-family: system-ui, sans-serif; margin: 3rem auto; max-width: 32rem; line-height: 1.6">
1115
- <h1 style="font-size: 1.25rem">Return to your terminal</h1>
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
- <p lang="zh-Hant-TW">請回到終端機。Lazco CLI 已經收到瀏覽器的回應,你可以關閉這個分頁。</p>
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/interrupted.ts
2996
- class Interrupted extends Error {
2997
- constructor() {
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 readFile4 } from "node:fs/promises";
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 readFile4(file.absolutePath);
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 SKIPPED_NAMED = 5;
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 mebibytes(bytes) {
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, SKIPPED_NAMED).join(", ");
3744
- const more = walk.skipped.length > SKIPPED_NAMED ? ", ..." : "";
3745
- context.progress(`found ${files.length} files (${mebibytes(totalBytes)}) in ${root}${walk.skipped.length > 0 ? `, skipped ${walk.skipped.length}: ${named}${more}` : ""}`);
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 (${mebibytes(uploadBytes)}) to the deployment ${deployment.id}, ${files.length - start.uploads.length} unchanged`);
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 (${mebibytes(progress.bytes)})`, progress.files === start.uploads.length)
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 invalid4(message, field) {
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 invalid4(`--${spec.name} takes no value`, spec.name);
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 invalid4(`--${spec.name} is given twice`, spec.name);
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 invalid4(`--${spec.name} needs a value`, spec.name);
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 invalid4("--wait and --no-wait cannot both be given", "wait");
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 invalid4("--timeout must be a whole number of seconds, 1 or more", "timeout");
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 invalid5(message, problems) {
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 invalid5("--json is not valid JSON", [
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 invalid5("--json must be one JSON object", [
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 invalid5("The arguments are not valid", problems);
5513
+ throw invalid6("The arguments are not valid", problems);
5205
5514
  }
5206
5515
  return { ...options.payload, ...fromArguments };
5207
5516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazco/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lazco Cloud CLI and local MCP server",
5
5
  "type": "module",
6
6
  "license": "MIT",