@neta-art/cohub-cli 6.6.0 → 6.7.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.
@@ -1,3 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { basename } from "node:path";
1
4
  import { HttpError } from "@neta-art/cohub";
2
5
  import { createClient, createClientWithAccessToken } from "../client.js";
3
6
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
@@ -6,6 +9,7 @@ import { downloadApp } from "../app-download.js";
6
9
  import { getAppByRef, parseAppRef } from "../app-ref.js";
7
10
  import { checkAppTarget } from "../app-target.js";
8
11
  import { registerAppCommerce } from "./app-commerce.js";
12
+ import { collectPublicUpload } from "./public.js";
9
13
  const APP_STATUSES = ["published", "disabled"];
10
14
  const APP_VISIBILITIES = ["public", "space"];
11
15
  const collectOption = (value, previous = []) => [...previous, value];
@@ -57,6 +61,69 @@ function withCohubBarMeta(input) {
57
61
  delete meta.presentation;
58
62
  return Object.keys(meta).length > 0 ? meta : null;
59
63
  }
64
+ const MAX_APP_SOURCE_FILES = 1000;
65
+ const MAX_APP_SOURCE_BYTES = 1024 * 1024 * 1024;
66
+ const APP_SOURCE_UPLOAD_CONCURRENCY = 4;
67
+ async function uploadLocalAppSource(client, spaceId, source) {
68
+ if (source.targetType === "port")
69
+ return null;
70
+ const upload = await collectPublicUpload(source.targetRef);
71
+ const totalBytes = upload.files.reduce((sum, file) => sum + file.size, 0);
72
+ if (upload.files.length > MAX_APP_SOURCE_FILES)
73
+ return error("App source is too large", `Use no more than ${MAX_APP_SOURCE_FILES} files.`);
74
+ if (totalBytes > MAX_APP_SOURCE_BYTES)
75
+ return error("App source is too large", "The total source size must not exceed 1 GiB.");
76
+ const uploadId = randomUUID();
77
+ const directoryPrefix = source.targetType === "directory" ? upload.destination.replace(/\/$/, "") : "";
78
+ const files = new Array(upload.files.length);
79
+ let nextIndex = 0;
80
+ const workers = Array.from({ length: Math.min(APP_SOURCE_UPLOAD_CONCURRENCY, upload.files.length) }, async () => {
81
+ while (nextIndex < upload.files.length) {
82
+ const index = nextIndex++;
83
+ const file = upload.files[index];
84
+ if (!file)
85
+ return;
86
+ const plan = await client.publicAssets.createUpload({
87
+ purpose: "app_source",
88
+ uploadProtocol: "presigned_put_v1",
89
+ spaceId,
90
+ sessionId: uploadId,
91
+ file: { size: file.size, mimeType: file.mimeType, filename: basename(file.publicPath) },
92
+ });
93
+ const response = await fetch(plan.asset.uploadUrl, {
94
+ method: "PUT",
95
+ headers: plan.asset.uploadHeaders,
96
+ body: createReadStream(file.localPath),
97
+ duplex: "half",
98
+ });
99
+ if (!response.ok)
100
+ throw new Error(`Failed to upload ${file.publicPath}: HTTP ${response.status}`);
101
+ const path = directoryPrefix && file.publicPath.startsWith(`${directoryPrefix}/`)
102
+ ? file.publicPath.slice(directoryPrefix.length + 1)
103
+ : basename(file.publicPath);
104
+ files[index] = { path, objectKey: plan.asset.objectKey, size: file.size, mimeType: file.mimeType };
105
+ }
106
+ });
107
+ await Promise.all(workers);
108
+ const manifest = JSON.stringify({ kind: "cohub.app-source", version: 1, targetType: source.targetType, files });
109
+ const manifestBlob = new Blob([manifest], { type: "application/json" });
110
+ const manifestPlan = await client.publicAssets.createUpload({
111
+ purpose: "app_source",
112
+ uploadProtocol: "presigned_put_v1",
113
+ spaceId,
114
+ sessionId: uploadId,
115
+ file: { size: manifestBlob.size, mimeType: "application/json", filename: "manifest.json" },
116
+ });
117
+ const manifestResponse = await fetch(manifestPlan.asset.uploadUrl, {
118
+ method: "PUT",
119
+ headers: manifestPlan.asset.uploadHeaders,
120
+ body: manifestBlob,
121
+ });
122
+ if (!manifestResponse.ok)
123
+ throw new Error(`Failed to upload app source manifest: HTTP ${manifestResponse.status}`);
124
+ const manifestAsset = manifestPlan.asset;
125
+ return { sourceRef: manifestAsset.objectKey, targetRef: source.targetType === "file" ? files[0]?.path ?? "" : "." };
126
+ }
60
127
  function resolveTarget(opts) {
61
128
  const targets = [
62
129
  opts.file ? { targetType: "file", targetRef: opts.file } : null,
@@ -321,8 +388,9 @@ export function registerApps(program) {
321
388
  appsCmd
322
389
  .command("publish <slug>")
323
390
  .description("Create or publish an app in the target space")
324
- .option("--file <path>", "Publish a file (HTML page, board, or any other file) from the Space workspace")
325
- .option("--dir <path>", "Publish a directory site from the Space workspace")
391
+ .option("--source <source>", "Source: workspace (default) or local")
392
+ .option("--file <path>", "Publish a file from the selected source")
393
+ .option("--dir <path>", "Publish a directory site from the selected source")
326
394
  .option("--port <port>", "Publish a public sandbox port")
327
395
  .option("--disabled", "Create as disabled")
328
396
  .option("--status <status>", "App status: published, disabled")
@@ -339,27 +407,42 @@ export function registerApps(program) {
339
407
  const target = resolveTarget(opts);
340
408
  if (!target)
341
409
  return error("Missing target", "Use one of --file, --dir, or --port.");
410
+ const source = opts.source ? parseChoice(opts.source, "source", ["workspace", "local"]) : "workspace";
411
+ if (target.targetType === "port" && opts.source)
412
+ return error("Invalid source", "--source applies only to --file and --dir.");
342
413
  const spaceId = resolveSpace(appsCmd);
343
414
  const client = createClient();
344
- const { targetType, targetRef } = target;
345
- if (targetType !== "port")
415
+ let { targetType, targetRef } = target;
416
+ let sourceRef = null;
417
+ if (source === "local") {
418
+ const uploaded = await uploadLocalAppSource(client, spaceId, target);
419
+ if (!uploaded)
420
+ return error("Invalid source", "--source local applies only to --file and --dir.");
421
+ targetRef = uploaded.targetRef;
422
+ sourceRef = uploaded.sourceRef;
423
+ }
424
+ else if (targetType !== "port") {
346
425
  await guardAppTarget(client, spaceId, { targetType, targetRef });
426
+ }
347
427
  const status = resolveStatus(opts);
348
428
  const meta = withCohubBarMeta({
349
429
  meta: parseJsonObject(opts.meta, "meta"),
350
430
  hideCohubBar: opts.hideCohubBar,
351
431
  showCohubBar: opts.showCohubBar,
352
432
  });
433
+ const publishMeta = source === "local"
434
+ ? { ...(meta ?? {}), runtime: { source: { type: "upload", ref: sourceRef } } }
435
+ : meta;
353
436
  const input = {
354
437
  spaceId,
355
438
  slug,
356
439
  status,
357
440
  visibility: resolveVisibility(opts.visibility),
358
441
  targetType: target.targetType,
359
- targetRef: target.targetRef,
442
+ targetRef,
360
443
  appScopes: opts.appScope,
361
444
  allowedViewerScopes: opts.viewerScope,
362
- meta,
445
+ meta: publishMeta,
363
446
  };
364
447
  try {
365
448
  const result = await client.apps.create(input);
@@ -381,10 +464,10 @@ export function registerApps(program) {
381
464
  status: status === "published" && existingApp.status !== "published" ? existingApp.status : status,
382
465
  visibility: resolveVisibility(opts.visibility),
383
466
  targetType: target.targetType,
384
- targetRef: target.targetRef,
467
+ targetRef,
385
468
  appScopes: opts.appScope,
386
469
  allowedViewerScopes: opts.viewerScope,
387
- meta,
470
+ meta: publishMeta,
388
471
  });
389
472
  const publishedVersion = status === "published"
390
473
  ? await client.apps.publishVersion(app.id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.6.0",
3
+ "version": "6.7.0",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,7 @@
19
19
  "commander": "^15.0.0",
20
20
  "pixi.js": "^8.20.1",
21
21
  "sharp": "^0.35.4",
22
- "@neta-art/cohub": "8.9.0"
22
+ "@neta-art/cohub": "8.10.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"