@neta-art/cohub-cli 3.5.2 → 3.6.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/README.md +3 -2
- package/dist/commands/works.js +24 -4
- package/dist/work-download.d.ts +12 -0
- package/dist/work-download.js +254 -0
- package/dist/work-ref.d.ts +13 -0
- package/dist/work-ref.js +61 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -275,7 +275,8 @@ Publish and manage Work entries from a Space workspace. Public Work URLs require
|
|
|
275
275
|
cohub profile update --username <username>
|
|
276
276
|
cohub spaces update <spaceId> --slug <space-slug>
|
|
277
277
|
cohub -s <spaceId> works ls --json
|
|
278
|
-
cohub works get <workId> --json
|
|
278
|
+
cohub works get <workId|url|username/space/work> --json
|
|
279
|
+
cohub works download <workId|url|username/space/work> --output <path>
|
|
279
280
|
cohub -s <spaceId> works publish demo --file dist/index.html
|
|
280
281
|
cohub -s <spaceId> works publish site --dir dist
|
|
281
282
|
cohub -s <spaceId> works publish app --port 3000
|
|
@@ -290,7 +291,7 @@ Resolve a published Work by public identity:
|
|
|
290
291
|
cohub works resolve <workSlug> --owner <username> --space-slug <spaceSlug>
|
|
291
292
|
```
|
|
292
293
|
|
|
293
|
-
Use `--json` for machine-readable output.
|
|
294
|
+
Use `--json` for machine-readable output. `works get` and `works download` also accept `cohub://works/<username>/<space>/<work>` mention URIs. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port Works are not downloadable. The resolve command remains available for explicit slug-based lookup.
|
|
294
295
|
|
|
295
296
|
Realtime rooms use a published Work's runtime identity, so they are available
|
|
296
297
|
through `client.work.realtime` in the SDK rather than as CLI commands.
|
package/dist/commands/works.js
CHANGED
|
@@ -2,6 +2,8 @@ import { HttpError } from "@neta-art/cohub";
|
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
3
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
4
4
|
import { resolveSpace } from "../space.js";
|
|
5
|
+
import { downloadWork } from "../work-download.js";
|
|
6
|
+
import { getWorkByRef } from "../work-ref.js";
|
|
5
7
|
import { registerWorkCommerce } from "./work-commerce.js";
|
|
6
8
|
const WORK_STATUSES = ["published", "disabled"];
|
|
7
9
|
const WORK_VISIBILITIES = ["public", "space"];
|
|
@@ -143,13 +145,13 @@ export function registerWorks(program) {
|
|
|
143
145
|
}
|
|
144
146
|
});
|
|
145
147
|
worksCmd
|
|
146
|
-
.command("get <
|
|
147
|
-
.description("Show work details")
|
|
148
|
+
.command("get <work>")
|
|
149
|
+
.description("Show work details by id, URL, mention URI, or username/space/work")
|
|
148
150
|
.option("--json", "Output as JSON")
|
|
149
|
-
.action(async (
|
|
151
|
+
.action(async (work, opts) => {
|
|
150
152
|
const client = createClient();
|
|
151
153
|
try {
|
|
152
|
-
const result = await client
|
|
154
|
+
const result = await getWorkByRef(client, work);
|
|
153
155
|
if (jsonRequested(opts))
|
|
154
156
|
return outJson(result);
|
|
155
157
|
printWork(result.work);
|
|
@@ -159,6 +161,24 @@ export function registerWorks(program) {
|
|
|
159
161
|
handleHttp(e);
|
|
160
162
|
}
|
|
161
163
|
});
|
|
164
|
+
worksCmd
|
|
165
|
+
.command("download <work>")
|
|
166
|
+
.description("Download a published file or directory Work")
|
|
167
|
+
.option("-o, --output <path>", "Output file or directory")
|
|
168
|
+
.option("--json", "Output as JSON")
|
|
169
|
+
.action(async (work, opts) => {
|
|
170
|
+
const client = createClient();
|
|
171
|
+
try {
|
|
172
|
+
const detail = await getWorkByRef(client, work);
|
|
173
|
+
const result = await downloadWork(detail, opts.output);
|
|
174
|
+
if (jsonRequested(opts))
|
|
175
|
+
return outJson(result);
|
|
176
|
+
ok(`Downloaded ${result.files} file${result.files === 1 ? "" : "s"} to ${result.output}`);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
handleHttp(e);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
162
182
|
worksCmd
|
|
163
183
|
.command("resolve <workSlug>")
|
|
164
184
|
.description("Resolve a published work by owner and space slug")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkGetResponse } from "@neta-art/cohub";
|
|
2
|
+
type DownloadResult = {
|
|
3
|
+
workId: string;
|
|
4
|
+
version: number;
|
|
5
|
+
kind: "file" | "directory";
|
|
6
|
+
output: string;
|
|
7
|
+
files: number;
|
|
8
|
+
bytes: number;
|
|
9
|
+
verified: true;
|
|
10
|
+
};
|
|
11
|
+
export declare function downloadWork(detail: WorkGetResponse, outputOption?: string, fetcher?: typeof fetch): Promise<DownloadResult>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { link, lstat, mkdir, mkdtemp, rename, rm, rmdir } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, posix, resolve } from "node:path";
|
|
5
|
+
import { Readable, Transform } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
const MANIFEST_MAX_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const DOWNLOAD_CONCURRENCY = 4;
|
|
9
|
+
function safeRelativePath(value, label) {
|
|
10
|
+
if (!value || value.includes("\\") || value.includes("\0") || posix.isAbsolute(value)) {
|
|
11
|
+
throw new Error(`Invalid ${label} in Work manifest`);
|
|
12
|
+
}
|
|
13
|
+
const segments = value.split("/");
|
|
14
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
15
|
+
throw new Error(`Invalid ${label} in Work manifest`);
|
|
16
|
+
}
|
|
17
|
+
return posix.normalize(value);
|
|
18
|
+
}
|
|
19
|
+
function isManifestFile(value) {
|
|
20
|
+
if (!value || typeof value !== "object")
|
|
21
|
+
return false;
|
|
22
|
+
const file = value;
|
|
23
|
+
return typeof file.artifactPath === "string"
|
|
24
|
+
&& typeof file.outputPath === "string"
|
|
25
|
+
&& (typeof file.mimeType === "string" || file.mimeType === null)
|
|
26
|
+
&& Number.isSafeInteger(file.sizeBytes)
|
|
27
|
+
&& Number(file.sizeBytes) >= 0
|
|
28
|
+
&& typeof file.sha256 === "string"
|
|
29
|
+
&& /^[0-9a-f]{64}$/i.test(file.sha256);
|
|
30
|
+
}
|
|
31
|
+
function parseManifest(value) {
|
|
32
|
+
if (!value || typeof value !== "object")
|
|
33
|
+
throw new Error("Work download manifest is invalid");
|
|
34
|
+
const manifest = value;
|
|
35
|
+
if (manifest.kind !== "cohub.work.artifact-manifest"
|
|
36
|
+
|| manifest.version !== 1
|
|
37
|
+
|| (manifest.targetType !== "file" && manifest.targetType !== "directory")
|
|
38
|
+
|| typeof manifest.targetRef !== "string"
|
|
39
|
+
|| typeof manifest.entrypoint !== "string"
|
|
40
|
+
|| !Number.isSafeInteger(manifest.fileCount)
|
|
41
|
+
|| Number(manifest.fileCount) < 1
|
|
42
|
+
|| !Number.isSafeInteger(manifest.sizeBytes)
|
|
43
|
+
|| Number(manifest.sizeBytes) < 0
|
|
44
|
+
|| !Array.isArray(manifest.files)
|
|
45
|
+
|| !manifest.files.every(isManifestFile)
|
|
46
|
+
|| manifest.files.length !== manifest.fileCount) {
|
|
47
|
+
throw new Error("Work download manifest is invalid");
|
|
48
|
+
}
|
|
49
|
+
const seenArtifactPaths = new Set();
|
|
50
|
+
const seenOutputPaths = new Set();
|
|
51
|
+
let sizeBytes = 0;
|
|
52
|
+
for (const file of manifest.files) {
|
|
53
|
+
file.artifactPath = safeRelativePath(file.artifactPath, "artifact path");
|
|
54
|
+
file.outputPath = safeRelativePath(file.outputPath, "output path");
|
|
55
|
+
if (seenArtifactPaths.has(file.artifactPath) || seenOutputPaths.has(file.outputPath)) {
|
|
56
|
+
throw new Error("Work download manifest contains duplicate paths");
|
|
57
|
+
}
|
|
58
|
+
seenArtifactPaths.add(file.artifactPath);
|
|
59
|
+
seenOutputPaths.add(file.outputPath);
|
|
60
|
+
sizeBytes += file.sizeBytes;
|
|
61
|
+
}
|
|
62
|
+
if (sizeBytes !== manifest.sizeBytes)
|
|
63
|
+
throw new Error("Work download manifest size is invalid");
|
|
64
|
+
safeRelativePath(manifest.entrypoint, "entrypoint");
|
|
65
|
+
return manifest;
|
|
66
|
+
}
|
|
67
|
+
async function readManifest(url, expectedSha256, fetcher) {
|
|
68
|
+
const response = await fetcher(url);
|
|
69
|
+
if (!response.ok)
|
|
70
|
+
throw new Error(`Failed to download Work manifest (${response.status})`);
|
|
71
|
+
if (!response.body)
|
|
72
|
+
throw new Error("Work download manifest is invalid");
|
|
73
|
+
const contentLength = Number(response.headers.get("content-length") ?? 0);
|
|
74
|
+
if (Number.isFinite(contentLength) && contentLength > MANIFEST_MAX_BYTES) {
|
|
75
|
+
throw new Error("Work download manifest is too large");
|
|
76
|
+
}
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let sizeBytes = 0;
|
|
79
|
+
for await (const chunk of Readable.fromWeb(response.body)) {
|
|
80
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
81
|
+
sizeBytes += bytes.byteLength;
|
|
82
|
+
if (sizeBytes > MANIFEST_MAX_BYTES)
|
|
83
|
+
throw new Error("Work download manifest is too large");
|
|
84
|
+
chunks.push(bytes);
|
|
85
|
+
}
|
|
86
|
+
const bytes = Buffer.concat(chunks, sizeBytes);
|
|
87
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
88
|
+
if (sha256 !== expectedSha256)
|
|
89
|
+
throw new Error("Work download manifest checksum mismatch");
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error("Work download manifest is invalid");
|
|
96
|
+
}
|
|
97
|
+
return parseManifest(parsed);
|
|
98
|
+
}
|
|
99
|
+
function artifactUrl(contentUrl, artifactPath) {
|
|
100
|
+
const base = new URL("./", contentUrl);
|
|
101
|
+
const encodedPath = safeRelativePath(artifactPath, "artifact path")
|
|
102
|
+
.split("/")
|
|
103
|
+
.map(encodeURIComponent)
|
|
104
|
+
.join("/");
|
|
105
|
+
return new URL(encodedPath, base).toString();
|
|
106
|
+
}
|
|
107
|
+
async function downloadFile(url, output, expected, fetcher) {
|
|
108
|
+
const response = await fetcher(url);
|
|
109
|
+
if (!response.ok || !response.body)
|
|
110
|
+
throw new Error(`Failed to download ${expected.outputPath} (${response.status})`);
|
|
111
|
+
await mkdir(dirname(output), { recursive: true });
|
|
112
|
+
let sizeBytes = 0;
|
|
113
|
+
const hash = createHash("sha256");
|
|
114
|
+
const verifier = new Transform({
|
|
115
|
+
transform(chunk, _encoding, callback) {
|
|
116
|
+
sizeBytes += chunk.byteLength;
|
|
117
|
+
if (sizeBytes > expected.sizeBytes) {
|
|
118
|
+
callback(new Error(`Downloaded file verification failed: ${expected.outputPath}`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
hash.update(chunk);
|
|
122
|
+
callback(null, chunk);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
await pipeline(Readable.fromWeb(response.body), verifier, createWriteStream(output, { flags: "wx" }));
|
|
126
|
+
if (sizeBytes !== expected.sizeBytes || hash.digest("hex") !== expected.sha256) {
|
|
127
|
+
throw new Error(`Downloaded file verification failed: ${expected.outputPath}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function outputExistsError(output) {
|
|
131
|
+
return new Error(`Output already exists: ${output}`);
|
|
132
|
+
}
|
|
133
|
+
async function outputExists(output) {
|
|
134
|
+
return Boolean(await lstat(output).catch((cause) => {
|
|
135
|
+
if (cause.code === "ENOENT")
|
|
136
|
+
return null;
|
|
137
|
+
throw cause;
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
async function assertOutputMissing(output) {
|
|
141
|
+
if (await outputExists(output))
|
|
142
|
+
throw outputExistsError(output);
|
|
143
|
+
}
|
|
144
|
+
async function installFileNoReplace(stagedFile, output) {
|
|
145
|
+
try {
|
|
146
|
+
await link(stagedFile, output);
|
|
147
|
+
}
|
|
148
|
+
catch (cause) {
|
|
149
|
+
if (cause.code === "EEXIST")
|
|
150
|
+
throw outputExistsError(output);
|
|
151
|
+
throw cause;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function installDirectoryNoReplace(stage, output) {
|
|
155
|
+
if (process.platform === "win32") {
|
|
156
|
+
try {
|
|
157
|
+
// Windows directory renames already fail when the destination exists.
|
|
158
|
+
await rename(stage, output);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
catch (cause) {
|
|
162
|
+
if (await outputExists(output))
|
|
163
|
+
throw outputExistsError(output);
|
|
164
|
+
throw cause;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await mkdir(output);
|
|
169
|
+
}
|
|
170
|
+
catch (cause) {
|
|
171
|
+
if (cause.code === "EEXIST")
|
|
172
|
+
throw outputExistsError(output);
|
|
173
|
+
throw cause;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await rename(stage, output);
|
|
177
|
+
}
|
|
178
|
+
catch (cause) {
|
|
179
|
+
await rmdir(output).catch(() => undefined);
|
|
180
|
+
throw cause;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function downloadFiles(input) {
|
|
184
|
+
let next = 0;
|
|
185
|
+
let failed = false;
|
|
186
|
+
let failure;
|
|
187
|
+
const workers = Array.from({ length: Math.min(DOWNLOAD_CONCURRENCY, input.files.length) }, async () => {
|
|
188
|
+
while (!failed && next < input.files.length) {
|
|
189
|
+
const index = next++;
|
|
190
|
+
const file = input.files[index];
|
|
191
|
+
if (!file)
|
|
192
|
+
continue;
|
|
193
|
+
try {
|
|
194
|
+
await downloadFile(artifactUrl(input.contentUrl, file.artifactPath), join(input.stage, ...file.outputPath.split("/")), file, input.fetcher);
|
|
195
|
+
}
|
|
196
|
+
catch (cause) {
|
|
197
|
+
if (!failed)
|
|
198
|
+
failure = cause;
|
|
199
|
+
failed = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
await Promise.all(workers);
|
|
204
|
+
if (failed)
|
|
205
|
+
throw failure;
|
|
206
|
+
}
|
|
207
|
+
export async function downloadWork(detail, outputOption, fetcher = fetch) {
|
|
208
|
+
const { work, content } = detail;
|
|
209
|
+
if (!content)
|
|
210
|
+
throw new Error("This Work has no published downloadable artifact");
|
|
211
|
+
if (content.kind === "port")
|
|
212
|
+
throw new Error("Port Works do not have a downloadable artifact");
|
|
213
|
+
if (content.kind === "board")
|
|
214
|
+
throw new Error("Board Works do not have a restorable file or directory artifact");
|
|
215
|
+
if (!content.download)
|
|
216
|
+
throw new Error("This Work version does not support download");
|
|
217
|
+
const manifest = await readManifest(content.download.manifestUrl, content.download.manifestSha256, fetcher);
|
|
218
|
+
if (manifest.targetType !== content.targetType || manifest.targetRef !== content.path) {
|
|
219
|
+
throw new Error("Work download manifest does not match the published artifact");
|
|
220
|
+
}
|
|
221
|
+
const entry = manifest.files.find((file) => file.artifactPath === manifest.entrypoint);
|
|
222
|
+
if (!entry)
|
|
223
|
+
throw new Error("Work download manifest entrypoint is missing");
|
|
224
|
+
const hasDirectoryOutput = manifest.targetType === "directory" || manifest.files.length > 1;
|
|
225
|
+
const output = resolve(outputOption ?? (hasDirectoryOutput ? work.slug : basename(manifest.targetRef)));
|
|
226
|
+
await assertOutputMissing(output);
|
|
227
|
+
await mkdir(dirname(output), { recursive: true });
|
|
228
|
+
const stage = await mkdtemp(join(dirname(output), `.${basename(output)}.cohub-download-`));
|
|
229
|
+
try {
|
|
230
|
+
const files = hasDirectoryOutput ? manifest.files : [entry];
|
|
231
|
+
await downloadFiles({ files, contentUrl: content.url, stage, fetcher });
|
|
232
|
+
if (hasDirectoryOutput) {
|
|
233
|
+
await installDirectoryNoReplace(stage, output);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const stagedFile = join(stage, ...entry.outputPath.split("/"));
|
|
237
|
+
await installFileNoReplace(stagedFile, output);
|
|
238
|
+
await rm(stage, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
workId: work.id,
|
|
242
|
+
version: work.latestVersion,
|
|
243
|
+
kind: hasDirectoryOutput ? "directory" : "file",
|
|
244
|
+
output,
|
|
245
|
+
files: files.length,
|
|
246
|
+
bytes: files.reduce((sum, file) => sum + file.sizeBytes, 0),
|
|
247
|
+
verified: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (cause) {
|
|
251
|
+
await rm(stage, { recursive: true, force: true }).catch(() => undefined);
|
|
252
|
+
throw cause;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CohubHttpClient, WorkGetResponse } from "@neta-art/cohub";
|
|
2
|
+
type WorkPublicRef = {
|
|
3
|
+
username: string;
|
|
4
|
+
spaceSlug: string;
|
|
5
|
+
workSlug: string;
|
|
6
|
+
};
|
|
7
|
+
export type ParsedWorkRef = {
|
|
8
|
+
id: string;
|
|
9
|
+
} | WorkPublicRef;
|
|
10
|
+
export declare function parseWorkRef(input: string): ParsedWorkRef;
|
|
11
|
+
export declare function formatWorkRef(ref: ParsedWorkRef): string;
|
|
12
|
+
export declare function getWorkByRef(client: CohubHttpClient, input: string): Promise<WorkGetResponse>;
|
|
13
|
+
export {};
|
package/dist/work-ref.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2
|
+
const USERNAME_PATTERN = /^(?!-)(?!.*--)[a-z0-9-]{1,39}(?<!-)$/;
|
|
3
|
+
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,78}[a-z0-9])?$/;
|
|
4
|
+
function decodePart(value) {
|
|
5
|
+
try {
|
|
6
|
+
return decodeURIComponent(value).trim();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function publicRef(parts) {
|
|
13
|
+
if (parts.length !== 3)
|
|
14
|
+
return null;
|
|
15
|
+
const [username = "", spaceSlug = "", workSlug = ""] = parts.map(decodePart);
|
|
16
|
+
return USERNAME_PATTERN.test(username) && SLUG_PATTERN.test(spaceSlug) && SLUG_PATTERN.test(workSlug)
|
|
17
|
+
? { username, spaceSlug, workSlug }
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function parseUrlRef(value) {
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
29
|
+
if (url.protocol === "cohub:" && url.hostname === "works")
|
|
30
|
+
return publicRef(parts) ?? null;
|
|
31
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
32
|
+
return null;
|
|
33
|
+
if (parts.length === 4 && parts[0] === "spaces" && UUID_PATTERN.test(parts[1] ?? "") && parts[2] === "works" && UUID_PATTERN.test(parts[3] ?? "")) {
|
|
34
|
+
return { id: parts[3] };
|
|
35
|
+
}
|
|
36
|
+
if (parts.length === 4 && parts[2] === "w")
|
|
37
|
+
return publicRef([parts[0], parts[1], parts[3]]);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function parseWorkRef(input) {
|
|
41
|
+
const value = input.trim();
|
|
42
|
+
if (UUID_PATTERN.test(value))
|
|
43
|
+
return { id: value };
|
|
44
|
+
const parsedUrl = parseUrlRef(value.includes("://") ? value : value.startsWith("/") ? `https://cohub.invalid${value}` : value);
|
|
45
|
+
if (parsedUrl)
|
|
46
|
+
return parsedUrl;
|
|
47
|
+
const parts = value.split("/").filter(Boolean);
|
|
48
|
+
const parsedPublic = parts.length === 3 ? publicRef(parts) : null;
|
|
49
|
+
if (parsedPublic)
|
|
50
|
+
return parsedPublic;
|
|
51
|
+
throw new Error("Work must be an id, public URL, cohub://works URI, or username/space/work reference");
|
|
52
|
+
}
|
|
53
|
+
export function formatWorkRef(ref) {
|
|
54
|
+
return "id" in ref ? ref.id : `${ref.username}/${ref.spaceSlug}/${ref.workSlug}`;
|
|
55
|
+
}
|
|
56
|
+
export function getWorkByRef(client, input) {
|
|
57
|
+
const ref = parseWorkRef(input);
|
|
58
|
+
return "id" in ref
|
|
59
|
+
? client.works.get(ref.id)
|
|
60
|
+
: client.works.getBySlug(ref.username, ref.spaceSlug, ref.workSlug);
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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.19.0",
|
|
21
21
|
"sharp": "^0.35.3",
|
|
22
|
-
"@neta-art/cohub": "5.
|
|
22
|
+
"@neta-art/cohub": "5.1.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|