@neta-art/cohub-cli 6.5.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.
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { CohubClient, CohubHttpClient } from "@neta-art/cohub";
2
2
  export declare function createClient(): CohubHttpClient;
3
+ export declare function createClientWithAccessToken(token: string): CohubHttpClient;
3
4
  export declare function createRealtimeClient(): CohubClient;
package/dist/client.js CHANGED
@@ -10,6 +10,9 @@ const clientOptions = () => ({
10
10
  export function createClient() {
11
11
  return new CohubHttpClient(clientOptions());
12
12
  }
13
+ export function createClientWithAccessToken(token) {
14
+ return new CohubHttpClient({ ...clientOptions(), getAccessToken: () => token });
15
+ }
13
16
  export function createRealtimeClient() {
14
17
  return new CohubClient(clientOptions());
15
18
  }
@@ -1,3 +1,4 @@
1
+ import { resolveExecutionAppId, } from "@neta-art/cohub";
1
2
  import { createClient } from "../client.js";
2
3
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
3
4
  function collectOption(value, previous = []) {
@@ -9,6 +10,9 @@ function requireText(value, label, flag) {
9
10
  return text;
10
11
  return error(`Missing ${label}`, `Pass ${flag}.`);
11
12
  }
13
+ function requireAppId(value) {
14
+ return requireText(value ?? resolveExecutionAppId() ?? undefined, "app ID", "--app-id <id>");
15
+ }
12
16
  function requireList(values, label, flag) {
13
17
  const items = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
14
18
  if (items.length > 0)
@@ -89,7 +93,7 @@ Examples:
89
93
  .option("--product-key <key>", "Product key", collectOption)
90
94
  .option("--json", "Output as JSON")
91
95
  .action(async (opts) => {
92
- const appId = requireText(opts.appId, "app ID", "--app-id <id>");
96
+ const appId = requireAppId(opts.appId);
93
97
  const productKeys = requireList(opts.productKey, "product key", "--product-key <key>");
94
98
  const client = createClient();
95
99
  try {
@@ -108,7 +112,7 @@ Examples:
108
112
  .option("--app-id <id>", "App ID")
109
113
  .option("--json", "Output as JSON")
110
114
  .action(async (opts) => {
111
- const appId = requireText(opts.appId, "app ID", "--app-id <id>");
115
+ const appId = requireAppId(opts.appId);
112
116
  const client = createClient();
113
117
  try {
114
118
  const result = await client.appCommerce.getEntitlements(appId);
@@ -132,7 +136,7 @@ Examples:
132
136
  .option("--reason <text>", "Reason for the consumption")
133
137
  .option("--json", "Output as JSON")
134
138
  .action(async (opts) => {
135
- const appId = requireText(opts.appId, "app ID", "--app-id <id>");
139
+ const appId = requireAppId(opts.appId);
136
140
  const amountText = requireText(opts.amount, "amount", "--amount <n>");
137
141
  const amount = Number.parseInt(amountText, 10);
138
142
  if (!Number.isSafeInteger(amount) || amount <= 0) {
@@ -172,7 +176,7 @@ Examples:
172
176
  .option("--product-key <key>", "Product key")
173
177
  .option("--json", "Output as JSON")
174
178
  .action(async (opts) => {
175
- const appId = requireText(opts.appId, "app ID", "--app-id <id>");
179
+ const appId = requireAppId(opts.appId);
176
180
  const productKey = requireText(opts.productKey, "product key", "--product-key <key>");
177
181
  const client = createClient();
178
182
  try {
@@ -210,7 +214,7 @@ Examples:
210
214
  .option("--order-id <id>", "Order ID")
211
215
  .option("--json", "Output as JSON")
212
216
  .action(async (opts) => {
213
- const appId = requireText(opts.appId, "app ID", "--app-id <id>");
217
+ const appId = requireAppId(opts.appId);
214
218
  const orderId = requireText(opts.orderId, "order ID", "--order-id <id>");
215
219
  const client = createClient();
216
220
  try {
@@ -1,11 +1,15 @@
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
- import { createClient } from "../client.js";
5
+ import { createClient, createClientWithAccessToken } from "../client.js";
3
6
  import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
4
7
  import { resolveSpace } from "../space.js";
5
8
  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];
@@ -27,6 +31,16 @@ function parseJsonObject(value, name) {
27
31
  }
28
32
  return error(`Invalid ${name}`, `${name} must be a JSON object`);
29
33
  }
34
+ function parseJsonValue(value, name) {
35
+ if (!value)
36
+ return null;
37
+ try {
38
+ return JSON.parse(value);
39
+ }
40
+ catch {
41
+ return error(`Invalid ${name}`, `${name} must be valid JSON`);
42
+ }
43
+ }
30
44
  function compactObject(input) {
31
45
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
32
46
  }
@@ -47,6 +61,69 @@ function withCohubBarMeta(input) {
47
61
  delete meta.presentation;
48
62
  return Object.keys(meta).length > 0 ? meta : null;
49
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
+ }
50
127
  function resolveTarget(opts) {
51
128
  const targets = [
52
129
  opts.file ? { targetType: "file", targetRef: opts.file } : null,
@@ -259,6 +336,32 @@ export function registerApps(program) {
259
336
  handleHttp(e);
260
337
  }
261
338
  });
339
+ const actionsCmd = appsCmd
340
+ .command("actions")
341
+ .description("Run published App Actions");
342
+ actionsCmd
343
+ .command("run <app> <action>")
344
+ .description("Run an App Action")
345
+ .option("--input <json>", "JSON input", "null")
346
+ .option("--json", "Output as JSON")
347
+ .action(async (appRef, action, opts) => {
348
+ const client = createClient();
349
+ try {
350
+ const ref = parseAppRef(appRef);
351
+ const detail = "id" in ref
352
+ ? await client.apps.getPublicById(ref.id)
353
+ : await client.apps.getBySlug(ref.username, ref.spaceSlug, ref.appSlug);
354
+ const session = await client.apps.createSession(detail.app.id);
355
+ const appClient = createClientWithAccessToken(session.token);
356
+ const result = await appClient.apps.runAction(detail.app.id, action, parseJsonValue(opts.input, "input"));
357
+ if (jsonRequested(opts))
358
+ return outJson(result);
359
+ ok(`App Action queued: ${result.taskRunId}`);
360
+ }
361
+ catch (e) {
362
+ handleHttp(e);
363
+ }
364
+ });
262
365
  appsCmd
263
366
  .command("resolve <appSlug>")
264
367
  .description("Resolve a published app by owner and space slug")
@@ -285,8 +388,9 @@ export function registerApps(program) {
285
388
  appsCmd
286
389
  .command("publish <slug>")
287
390
  .description("Create or publish an app in the target space")
288
- .option("--file <path>", "Publish a file (HTML page, board, or any other file) from the Space workspace")
289
- .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")
290
394
  .option("--port <port>", "Publish a public sandbox port")
291
395
  .option("--disabled", "Create as disabled")
292
396
  .option("--status <status>", "App status: published, disabled")
@@ -303,27 +407,42 @@ export function registerApps(program) {
303
407
  const target = resolveTarget(opts);
304
408
  if (!target)
305
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.");
306
413
  const spaceId = resolveSpace(appsCmd);
307
414
  const client = createClient();
308
- const { targetType, targetRef } = target;
309
- 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") {
310
425
  await guardAppTarget(client, spaceId, { targetType, targetRef });
426
+ }
311
427
  const status = resolveStatus(opts);
312
428
  const meta = withCohubBarMeta({
313
429
  meta: parseJsonObject(opts.meta, "meta"),
314
430
  hideCohubBar: opts.hideCohubBar,
315
431
  showCohubBar: opts.showCohubBar,
316
432
  });
433
+ const publishMeta = source === "local"
434
+ ? { ...(meta ?? {}), runtime: { source: { type: "upload", ref: sourceRef } } }
435
+ : meta;
317
436
  const input = {
318
437
  spaceId,
319
438
  slug,
320
439
  status,
321
440
  visibility: resolveVisibility(opts.visibility),
322
441
  targetType: target.targetType,
323
- targetRef: target.targetRef,
442
+ targetRef,
324
443
  appScopes: opts.appScope,
325
444
  allowedViewerScopes: opts.viewerScope,
326
- meta,
445
+ meta: publishMeta,
327
446
  };
328
447
  try {
329
448
  const result = await client.apps.create(input);
@@ -345,10 +464,10 @@ export function registerApps(program) {
345
464
  status: status === "published" && existingApp.status !== "published" ? existingApp.status : status,
346
465
  visibility: resolveVisibility(opts.visibility),
347
466
  targetType: target.targetType,
348
- targetRef: target.targetRef,
467
+ targetRef,
349
468
  appScopes: opts.appScope,
350
469
  allowedViewerScopes: opts.viewerScope,
351
- meta,
470
+ meta: publishMeta,
352
471
  });
353
472
  const publishedVersion = status === "published"
354
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.5.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",
@@ -15,11 +15,11 @@
15
15
  "NOTICE"
16
16
  ],
17
17
  "dependencies": {
18
- "@neta-art/generation": "^0.1.23",
18
+ "@neta-art/generation": "^0.1.25",
19
19
  "commander": "^15.0.0",
20
20
  "pixi.js": "^8.20.1",
21
21
  "sharp": "^0.35.4",
22
- "@neta-art/cohub": "8.8.0"
22
+ "@neta-art/cohub": "8.10.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"