@neta-art/cohub-cli 1.11.1 → 1.12.1
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/commands/generations.js +21 -8
- package/dist/commands/spaces.js +206 -2
- package/package.json +2 -2
|
@@ -57,23 +57,22 @@ async function saveOutputs(output, outputPath) {
|
|
|
57
57
|
if (outputs.length === 0)
|
|
58
58
|
return [];
|
|
59
59
|
const info = await stat(outputPath).catch(() => null);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await mkdir(outputPath, { recursive: true });
|
|
60
|
+
const isSingleFile = outputs.length === 1 && !(info?.isDirectory() ?? false);
|
|
61
|
+
const targetPath = isSingleFile ? outputPath : await resolveOutputDirectory(outputPath, info);
|
|
62
|
+
if (isSingleFile)
|
|
63
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
65
64
|
else
|
|
66
|
-
await mkdir(
|
|
65
|
+
await mkdir(targetPath, { recursive: true });
|
|
67
66
|
const savedPaths = [];
|
|
68
67
|
for (const [i, block] of outputs.entries()) {
|
|
69
68
|
if (block.type === "text") {
|
|
70
|
-
const target =
|
|
69
|
+
const target = isSingleFile ? targetPath : join(targetPath, `generation-${i + 1}.txt`);
|
|
71
70
|
await writeFile(target, block.text, "utf-8");
|
|
72
71
|
savedPaths.push(target);
|
|
73
72
|
continue;
|
|
74
73
|
}
|
|
75
74
|
const source = block.source;
|
|
76
|
-
const target =
|
|
75
|
+
const target = isSingleFile ? targetPath : join(targetPath, outputName(block, source.type === "url" ? source.url : undefined, i));
|
|
77
76
|
if (source.type === "url") {
|
|
78
77
|
const response = await fetch(source.url);
|
|
79
78
|
if (!response.ok)
|
|
@@ -88,6 +87,20 @@ async function saveOutputs(output, outputPath) {
|
|
|
88
87
|
}
|
|
89
88
|
return savedPaths;
|
|
90
89
|
}
|
|
90
|
+
async function resolveOutputDirectory(outputPath, info) {
|
|
91
|
+
if (info?.isDirectory() || (!info && !extname(outputPath)))
|
|
92
|
+
return outputPath;
|
|
93
|
+
const ext = extname(outputPath);
|
|
94
|
+
const stem = ext ? basename(outputPath, ext) : basename(outputPath);
|
|
95
|
+
const parent = dirname(outputPath);
|
|
96
|
+
const base = join(parent, `${stem}-outputs`);
|
|
97
|
+
for (let i = 0;; i += 1) {
|
|
98
|
+
const candidate = i === 0 ? base : `${base}-${i + 1}`;
|
|
99
|
+
const candidateInfo = await stat(candidate).catch(() => null);
|
|
100
|
+
if (!candidateInfo || candidateInfo.isDirectory())
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
91
104
|
function outputName(block, url, index) {
|
|
92
105
|
const fromUrl = url ? basename(new URL(url).pathname) : "";
|
|
93
106
|
const label = slugOutputLabel(block);
|
package/dist/commands/spaces.js
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveSpace } from "../space.js";
|
|
|
10
10
|
const cliEnv = resolveCohubEnvironment();
|
|
11
11
|
const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
|
|
12
12
|
const SPACE_ROLES = ["host", "builder", "guest"];
|
|
13
|
+
const LABEL_RESOURCE_TYPES = ["session", "checkpoint", "file"];
|
|
13
14
|
function parseInteger(value, name, options = {}) {
|
|
14
15
|
if (!/^-?\d+$/.test(value.trim()))
|
|
15
16
|
return error(`Invalid ${name}`, `${name} must be an integer`);
|
|
@@ -22,6 +23,9 @@ function parseInteger(value, name, options = {}) {
|
|
|
22
23
|
return error(`Invalid ${name}`, `${name} must be at most ${options.max}`);
|
|
23
24
|
return parsed;
|
|
24
25
|
}
|
|
26
|
+
function collectOption(value, previous = []) {
|
|
27
|
+
return [...previous, value];
|
|
28
|
+
}
|
|
25
29
|
function parseChoice(value, name, choices) {
|
|
26
30
|
if (choices.includes(value))
|
|
27
31
|
return value;
|
|
@@ -189,6 +193,7 @@ async function sendPrompt(command, words, opts) {
|
|
|
189
193
|
provider: opts.provider,
|
|
190
194
|
accessMode: opts.readOnly ? "read_only" : "full_access",
|
|
191
195
|
schedule,
|
|
196
|
+
labelRefs: opts.label?.length ? opts.label : undefined,
|
|
192
197
|
});
|
|
193
198
|
if (jsonRequested(opts))
|
|
194
199
|
return outJson(result);
|
|
@@ -215,6 +220,7 @@ export function registerPrompt(program) {
|
|
|
215
220
|
.option("--at <iso>", "Send once at an ISO 8601 time with timezone")
|
|
216
221
|
.option("--cron <expression>", "Repeat using a 5-field cron expression")
|
|
217
222
|
.option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
|
|
223
|
+
.option("--label <ref>", "Attach a label, e.g. Bug or Area/Frontend", collectOption, [])
|
|
218
224
|
.option("--json", "Output as JSON")
|
|
219
225
|
.action((words, opts) => sendPrompt(program, words, opts));
|
|
220
226
|
}
|
|
@@ -373,6 +379,7 @@ export function registerSpaces(program) {
|
|
|
373
379
|
.option("--at <iso>", "Send once at an ISO 8601 time with timezone")
|
|
374
380
|
.option("--cron <expression>", "Repeat using a 5-field cron expression")
|
|
375
381
|
.option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
|
|
382
|
+
.option("--label <ref>", "Attach a label, e.g. Bug or Area/Frontend", collectOption, [])
|
|
376
383
|
.option("--json", "Output as JSON")
|
|
377
384
|
.action((words, opts) => sendPrompt(spacesCmd, words, opts));
|
|
378
385
|
// ── spaces files ──
|
|
@@ -387,6 +394,8 @@ export function registerSpaces(program) {
|
|
|
387
394
|
registerCheckpoints(spacesCmd);
|
|
388
395
|
// ── spaces mods ──
|
|
389
396
|
registerMods(spacesCmd);
|
|
397
|
+
// ── spaces labels ──
|
|
398
|
+
registerLabels(spacesCmd);
|
|
390
399
|
// ── spaces usage ──
|
|
391
400
|
spacesCmd
|
|
392
401
|
.command("usage [days]")
|
|
@@ -413,6 +422,197 @@ export function registerSpaces(program) {
|
|
|
413
422
|
}
|
|
414
423
|
});
|
|
415
424
|
}
|
|
425
|
+
function flattenLabels(items, prefix = "") {
|
|
426
|
+
return items.flatMap((label) => {
|
|
427
|
+
const path = prefix ? `${prefix}/${label.name}` : label.name;
|
|
428
|
+
return [{ ...label, path }, ...flattenLabels(label.children ?? [], path)];
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function parseLabelResourceType(value) {
|
|
432
|
+
return parseChoice(value, "resource type", LABEL_RESOURCE_TYPES);
|
|
433
|
+
}
|
|
434
|
+
function parseLabelRefs(value) {
|
|
435
|
+
if (!value?.trim())
|
|
436
|
+
return [];
|
|
437
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
438
|
+
}
|
|
439
|
+
function registerLabels(spacesCmd) {
|
|
440
|
+
const labelsCmd = spacesCmd
|
|
441
|
+
.command("labels")
|
|
442
|
+
.description("Manage labels")
|
|
443
|
+
.hook("preAction", () => { resolveSpace(spacesCmd); });
|
|
444
|
+
labelsCmd
|
|
445
|
+
.command("ls")
|
|
446
|
+
.alias("list")
|
|
447
|
+
.description("List labels")
|
|
448
|
+
.option("--json", "Output as JSON")
|
|
449
|
+
.action(async (opts) => {
|
|
450
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
451
|
+
const client = createClient();
|
|
452
|
+
try {
|
|
453
|
+
const result = await client.space(spaceId).labels.list();
|
|
454
|
+
if (jsonRequested(opts))
|
|
455
|
+
return outJson(result);
|
|
456
|
+
table(flattenLabels(result.labels), [
|
|
457
|
+
{ key: "path", label: "Label" },
|
|
458
|
+
{ key: "rank", label: "Rank" },
|
|
459
|
+
]);
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
handleHttp(e);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
labelsCmd
|
|
466
|
+
.command("create <labelRef>")
|
|
467
|
+
.description("Create a label")
|
|
468
|
+
.option("--json", "Output as JSON")
|
|
469
|
+
.action(async (labelRef, opts) => {
|
|
470
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
471
|
+
const client = createClient();
|
|
472
|
+
try {
|
|
473
|
+
const result = await client.space(spaceId).labels.create(labelRef);
|
|
474
|
+
if (jsonRequested(opts))
|
|
475
|
+
return outJson(result);
|
|
476
|
+
ok("Label created");
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
handleHttp(e);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
labelsCmd
|
|
483
|
+
.command("update <labelRef>")
|
|
484
|
+
.description("Update a label")
|
|
485
|
+
.option("--name <name>", "Label name")
|
|
486
|
+
.option("--parent <ref>", "Parent label; use null for root")
|
|
487
|
+
.option("--rank <n>", "Sort rank")
|
|
488
|
+
.option("--json", "Output as JSON")
|
|
489
|
+
.action(async (labelRef, opts) => {
|
|
490
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
491
|
+
const client = createClient();
|
|
492
|
+
try {
|
|
493
|
+
const result = await client.space(spaceId).labels.update(labelRef, {
|
|
494
|
+
name: opts.name,
|
|
495
|
+
parentRef: opts.parent === undefined ? undefined : opts.parent === "null" ? null : opts.parent,
|
|
496
|
+
rank: opts.rank === undefined ? undefined : parseInteger(opts.rank, "rank", { min: -1_000_000, max: 1_000_000 }),
|
|
497
|
+
});
|
|
498
|
+
if (jsonRequested(opts))
|
|
499
|
+
return outJson(result);
|
|
500
|
+
ok("Label updated");
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
handleHttp(e);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
labelsCmd
|
|
507
|
+
.command("rm <labelRef>")
|
|
508
|
+
.alias("delete")
|
|
509
|
+
.description("Delete a label")
|
|
510
|
+
.action(async (labelRef) => {
|
|
511
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
512
|
+
const client = createClient();
|
|
513
|
+
try {
|
|
514
|
+
await client.space(spaceId).labels.delete(labelRef);
|
|
515
|
+
ok("Label deleted");
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
handleHttp(e);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
labelsCmd
|
|
522
|
+
.command("reorder <labelRefs...>")
|
|
523
|
+
.description("Reorder labels")
|
|
524
|
+
.option("--json", "Output as JSON")
|
|
525
|
+
.action(async (labelRefs, opts) => {
|
|
526
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
527
|
+
const client = createClient();
|
|
528
|
+
try {
|
|
529
|
+
const result = await client.space(spaceId).labels.reorder(labelRefs);
|
|
530
|
+
if (jsonRequested(opts))
|
|
531
|
+
return outJson(result);
|
|
532
|
+
ok("Labels reordered");
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
handleHttp(e);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
labelsCmd
|
|
539
|
+
.command("items <labelRef>")
|
|
540
|
+
.description("List label items")
|
|
541
|
+
.option("--limit <n>", "Page size")
|
|
542
|
+
.option("--cursor <cursor>", "Page cursor")
|
|
543
|
+
.option("--json", "Output as JSON")
|
|
544
|
+
.action(async (labelRef, opts) => {
|
|
545
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
546
|
+
const client = createClient();
|
|
547
|
+
try {
|
|
548
|
+
const result = await client.space(spaceId).labels.listItems(labelRef, {
|
|
549
|
+
limit: opts.limit ? parseInteger(opts.limit, "limit", { min: 1 }) : undefined,
|
|
550
|
+
cursor: opts.cursor,
|
|
551
|
+
});
|
|
552
|
+
if (jsonRequested(opts))
|
|
553
|
+
return outJson(result);
|
|
554
|
+
table(result.items, [
|
|
555
|
+
{ key: "id", label: "ID" },
|
|
556
|
+
{ key: "resourceType", label: "Type" },
|
|
557
|
+
{ key: "resourceRef", label: "Resource" },
|
|
558
|
+
{ key: "rank", label: "Rank" },
|
|
559
|
+
]);
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
handleHttp(e);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
labelsCmd
|
|
566
|
+
.command("attach <labelRef> <resourceType> <resourceRef>")
|
|
567
|
+
.description("Attach a label")
|
|
568
|
+
.option("--json", "Output as JSON")
|
|
569
|
+
.action(async (labelRef, resourceType, resourceRef, opts) => {
|
|
570
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
571
|
+
const client = createClient();
|
|
572
|
+
try {
|
|
573
|
+
const result = await client.space(spaceId).labels.attach(labelRef, { resourceType: parseLabelResourceType(resourceType), resourceRef });
|
|
574
|
+
if (jsonRequested(opts))
|
|
575
|
+
return outJson(result);
|
|
576
|
+
ok("Label attached");
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
handleHttp(e);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
labelsCmd
|
|
583
|
+
.command("detach <labelRef> <resourceType> <resourceRef>")
|
|
584
|
+
.description("Detach a label")
|
|
585
|
+
.action(async (labelRef, resourceType, resourceRef) => {
|
|
586
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
587
|
+
const client = createClient();
|
|
588
|
+
try {
|
|
589
|
+
await client.space(spaceId).labels.detach(labelRef, { resourceType: parseLabelResourceType(resourceType), resourceRef });
|
|
590
|
+
ok("Label detached");
|
|
591
|
+
}
|
|
592
|
+
catch (e) {
|
|
593
|
+
handleHttp(e);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
labelsCmd
|
|
597
|
+
.command("set <resourceType> <resourceRef> [labelRefs...]")
|
|
598
|
+
.description("Set resource labels")
|
|
599
|
+
.option("--labels <refs>", "Comma-separated label refs")
|
|
600
|
+
.option("--json", "Output as JSON")
|
|
601
|
+
.action(async (resourceType, resourceRef, labelRefs, opts) => {
|
|
602
|
+
const spaceId = resolveSpace(spacesCmd);
|
|
603
|
+
const client = createClient();
|
|
604
|
+
try {
|
|
605
|
+
const refs = [...parseLabelRefs(opts.labels), ...labelRefs];
|
|
606
|
+
const result = await client.space(spaceId).labels.setResourceLabels(parseLabelResourceType(resourceType), resourceRef, refs);
|
|
607
|
+
if (jsonRequested(opts))
|
|
608
|
+
return outJson(result);
|
|
609
|
+
ok("Resource labels updated");
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
handleHttp(e);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
416
616
|
function registerMods(spacesCmd) {
|
|
417
617
|
const modsCmd = spacesCmd
|
|
418
618
|
.command("mods")
|
|
@@ -652,7 +852,6 @@ function registerFiles(spacesCmd) {
|
|
|
652
852
|
}
|
|
653
853
|
});
|
|
654
854
|
}
|
|
655
|
-
// ── Session operations ──
|
|
656
855
|
function registerSessions(spacesCmd) {
|
|
657
856
|
const sessionsCmd = spacesCmd
|
|
658
857
|
.command("sessions")
|
|
@@ -688,12 +887,17 @@ function registerSessions(spacesCmd) {
|
|
|
688
887
|
sessionsCmd
|
|
689
888
|
.command("create [title]")
|
|
690
889
|
.description("Create a session")
|
|
890
|
+
.option("--label <ref>", "Attach a label, e.g. Bug or Area/Frontend", collectOption, [])
|
|
691
891
|
.option("--json", "Output as JSON")
|
|
692
892
|
.action(async (title, opts) => {
|
|
693
893
|
const spaceId = resolveSpace(spacesCmd);
|
|
694
894
|
const client = createClient();
|
|
695
895
|
try {
|
|
696
|
-
const result = await client.space(spaceId).sessions.create({
|
|
896
|
+
const result = await client.space(spaceId).sessions.create({
|
|
897
|
+
title,
|
|
898
|
+
source: "cli",
|
|
899
|
+
labelRefs: opts.label?.length ? opts.label : undefined,
|
|
900
|
+
});
|
|
697
901
|
if (jsonRequested(opts))
|
|
698
902
|
return outJson(result);
|
|
699
903
|
ok(`Session created: ${result.session.id}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@neta-art/generation": "^0.1.2",
|
|
17
17
|
"commander": "^14.0.3",
|
|
18
18
|
"sharp": "^0.34.5",
|
|
19
|
-
"@neta-art/cohub": "1.
|
|
19
|
+
"@neta-art/cohub": "1.22.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|