@neta-art/cohub-cli 1.20.1 → 1.20.3
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 +19 -1
- package/dist/commands/profile.js +26 -1
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +233 -0
- package/dist/commands/spaces.js +35 -2
- package/dist/index.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,19 +60,25 @@ cohub spaces prompt -h
|
|
|
60
60
|
```bash
|
|
61
61
|
cohub spaces ls --json
|
|
62
62
|
cohub spaces get <spaceId> --json
|
|
63
|
+
cohub -s <spaceId> spaces get
|
|
64
|
+
COHUB_SPACE_ID=<spaceId> cohub spaces get
|
|
63
65
|
cohub spaces create --name "<name>" --description "<description>" --json
|
|
66
|
+
cohub spaces update <spaceId> --slug <space-slug>
|
|
64
67
|
cohub spaces rename <spaceId> "<new name>"
|
|
68
|
+
cohub -s <spaceId> run -- git status
|
|
65
69
|
```
|
|
66
70
|
|
|
67
71
|
Many space-scoped commands need a target Space:
|
|
68
72
|
|
|
69
73
|
```bash
|
|
70
74
|
cohub -s <spaceId> spaces prompt "message" --json
|
|
75
|
+
COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
71
76
|
```
|
|
72
77
|
|
|
73
78
|
## Chats and prompts
|
|
74
79
|
|
|
75
80
|
Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
|
|
81
|
+
Use `run` for a one-off shell command in the current Space workspace.
|
|
76
82
|
|
|
77
83
|
```bash
|
|
78
84
|
# Send now
|
|
@@ -175,9 +181,11 @@ Confirm before deleting files or directories.
|
|
|
175
181
|
|
|
176
182
|
## Works
|
|
177
183
|
|
|
178
|
-
Publish and manage Work entries from a Space workspace.
|
|
184
|
+
Publish and manage Work entries from a Space workspace. Public Work URLs require a username and a Space slug.
|
|
179
185
|
|
|
180
186
|
```bash
|
|
187
|
+
cohub profile update --username <username>
|
|
188
|
+
cohub spaces update <spaceId> --slug <space-slug>
|
|
181
189
|
cohub -s <spaceId> works ls --json
|
|
182
190
|
cohub works get <workId> --json
|
|
183
191
|
cohub -s <spaceId> works publish demo --file dist/index.html
|
|
@@ -204,6 +212,16 @@ cohub -s <spaceId> spaces checkpoints get <checkpointId> --json
|
|
|
204
212
|
cohub -s <spaceId> spaces checkpoints create "<description>" --json
|
|
205
213
|
```
|
|
206
214
|
|
|
215
|
+
## Run commands
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
cohub -s <spaceId> run -- git status
|
|
219
|
+
cohub -s <spaceId> run --command "pnpm test"
|
|
220
|
+
cohub -s <spaceId> run --async --command "pnpm build"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Use `run` for one-off shell commands in a Space workspace. Use `--async` to queue and return immediately.
|
|
224
|
+
|
|
207
225
|
## Tasks
|
|
208
226
|
|
|
209
227
|
```bash
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { uploadAvatarAsset } from "../avatar.js";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { json as outJson, jsonRequested, ok, handleHttp } from "../output.js";
|
|
3
|
+
import { json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
|
|
4
4
|
export function registerProfile(program) {
|
|
5
5
|
const profileCmd = program.command("profile").description("Manage your profile");
|
|
6
|
+
profileCmd
|
|
7
|
+
.command("update")
|
|
8
|
+
.description("Update your profile")
|
|
9
|
+
.option("--username <username>", "Public username")
|
|
10
|
+
.option("--display-name <name>", "Display name")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const input = {
|
|
14
|
+
username: opts.username,
|
|
15
|
+
displayName: opts.displayName,
|
|
16
|
+
};
|
|
17
|
+
if (input.username === undefined && input.displayName === undefined) {
|
|
18
|
+
return error("Nothing to update", "Pass --username or --display-name.");
|
|
19
|
+
}
|
|
20
|
+
const client = createClient();
|
|
21
|
+
try {
|
|
22
|
+
const result = await client.user.updateProfile(input);
|
|
23
|
+
if (jsonRequested(opts))
|
|
24
|
+
return outJson(result);
|
|
25
|
+
ok("Profile updated");
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
handleHttp(e);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
6
31
|
profileCmd
|
|
7
32
|
.command("avatar <path>")
|
|
8
33
|
.description("Upload your avatar")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function maybeHandleRunCommand(argv: string[]): Promise<boolean>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createClient } from "../client.js";
|
|
2
|
+
import { error, handleHttp, json as outJson, spinner } from "../output.js";
|
|
3
|
+
const DEFAULT_WAIT_TIMEOUT_MS = (6 * 60 * 60 + 60) * 1000;
|
|
4
|
+
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
5
|
+
function shellQuote(value) {
|
|
6
|
+
if (value.length === 0)
|
|
7
|
+
return "''";
|
|
8
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value))
|
|
9
|
+
return value;
|
|
10
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
11
|
+
}
|
|
12
|
+
function shellJoin(values) {
|
|
13
|
+
return values.map((value) => shellQuote(value)).join(" ");
|
|
14
|
+
}
|
|
15
|
+
function topLevelRunIndex(argv) {
|
|
16
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
17
|
+
const token = argv[index];
|
|
18
|
+
if (token === "--")
|
|
19
|
+
return -1;
|
|
20
|
+
if (token === "-s" || token === "--space") {
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (token.startsWith("--space=") || token === "--json")
|
|
25
|
+
continue;
|
|
26
|
+
if (token.startsWith("-"))
|
|
27
|
+
continue;
|
|
28
|
+
return token === "run" ? index : -1;
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
function parseSpaceId(tokens) {
|
|
33
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
34
|
+
const token = tokens[index];
|
|
35
|
+
if (token === "-s" || token === "--space") {
|
|
36
|
+
const value = tokens[index + 1];
|
|
37
|
+
if (!value)
|
|
38
|
+
return error("Missing space", `${token} requires a value`);
|
|
39
|
+
return value.trim() || undefined;
|
|
40
|
+
}
|
|
41
|
+
if (token.startsWith("--space="))
|
|
42
|
+
return token.slice("--space=".length).trim() || undefined;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function parseRunCliOptions(argv) {
|
|
47
|
+
const runIndex = topLevelRunIndex(argv);
|
|
48
|
+
if (runIndex < 0)
|
|
49
|
+
return error("Invalid invocation", "Use `cohub run [options] <command>`");
|
|
50
|
+
const beforeRun = argv.slice(0, runIndex);
|
|
51
|
+
const afterRun = argv.slice(runIndex + 1);
|
|
52
|
+
let spaceId = parseSpaceId(beforeRun) ?? process.env.COHUB_SPACE_ID?.trim() ?? "";
|
|
53
|
+
let json = beforeRun.includes("--json");
|
|
54
|
+
let async = false;
|
|
55
|
+
let commandOption = null;
|
|
56
|
+
let positionalTokens = [];
|
|
57
|
+
for (let index = 0; index < afterRun.length; index += 1) {
|
|
58
|
+
const token = afterRun[index];
|
|
59
|
+
if (token === "-h" || token === "--help") {
|
|
60
|
+
printRunHelp();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
if (token === "--async") {
|
|
64
|
+
async = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (token === "--json") {
|
|
68
|
+
json = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (token === "-c" || token === "--command") {
|
|
72
|
+
const value = afterRun[index + 1];
|
|
73
|
+
if (!value)
|
|
74
|
+
return error("Missing command", `${token} requires a value`);
|
|
75
|
+
if (commandOption !== null || positionalTokens.length > 0) {
|
|
76
|
+
return error("Conflicting command input", "Use either --command or positional command tokens, not both.");
|
|
77
|
+
}
|
|
78
|
+
commandOption = value;
|
|
79
|
+
index += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (token.startsWith("--command=")) {
|
|
83
|
+
if (commandOption !== null || positionalTokens.length > 0) {
|
|
84
|
+
return error("Conflicting command input", "Use either --command or positional command tokens, not both.");
|
|
85
|
+
}
|
|
86
|
+
commandOption = token.slice("--command=".length);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token === "--") {
|
|
90
|
+
if (commandOption !== null) {
|
|
91
|
+
return error("Conflicting command input", "Use either --command or positional command tokens, not both.");
|
|
92
|
+
}
|
|
93
|
+
positionalTokens = afterRun.slice(index + 1);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (commandOption !== null) {
|
|
97
|
+
return error("Conflicting command input", "Use either --command or positional command tokens, not both.");
|
|
98
|
+
}
|
|
99
|
+
positionalTokens = afterRun.slice(index);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const command = commandOption?.trim() || shellJoin(positionalTokens).trim();
|
|
103
|
+
if (!command) {
|
|
104
|
+
return error("No command", "Pass --command <shell command>, or use `--` followed by the command.");
|
|
105
|
+
}
|
|
106
|
+
if (!spaceId) {
|
|
107
|
+
return error("Missing required space", "Add -s, --space <id> before `run` or set COHUB_SPACE_ID.");
|
|
108
|
+
}
|
|
109
|
+
return { spaceId, json, async, command };
|
|
110
|
+
}
|
|
111
|
+
function printRunHelp() {
|
|
112
|
+
process.stdout.write(`
|
|
113
|
+
Usage:
|
|
114
|
+
cohub [-s <spaceId>] run [options] -- <shell command>
|
|
115
|
+
cohub [-s <spaceId>] run --command <shell command>
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
-c, --command <command> Shell command to execute in the space workspace
|
|
119
|
+
--async Queue the command and return immediately
|
|
120
|
+
--json Output as JSON
|
|
121
|
+
-h, --help Show this help
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
cohub -s <spaceId> run --command "git status"
|
|
125
|
+
cohub -s <spaceId> run --async --command "pnpm test"
|
|
126
|
+
cohub -s <spaceId> run -- git status -sb
|
|
127
|
+
|
|
128
|
+
Notes:
|
|
129
|
+
- Use --command for commands that contain leading flags, or use -- before the shell command.
|
|
130
|
+
- The command runs in /workspace.
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
function sleep(ms) {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
136
|
+
function formatElapsed(ms) {
|
|
137
|
+
if (ms < 1000)
|
|
138
|
+
return `${ms}ms`;
|
|
139
|
+
const seconds = Math.floor(ms / 1000);
|
|
140
|
+
if (seconds < 60)
|
|
141
|
+
return `${seconds}s`;
|
|
142
|
+
const minutes = Math.floor(seconds / 60);
|
|
143
|
+
const restSeconds = seconds % 60;
|
|
144
|
+
if (minutes < 60)
|
|
145
|
+
return restSeconds > 0 ? `${minutes}m ${restSeconds}s` : `${minutes}m`;
|
|
146
|
+
const hours = Math.floor(minutes / 60);
|
|
147
|
+
const restMinutes = minutes % 60;
|
|
148
|
+
return restMinutes > 0 ? `${hours}h ${restMinutes}m` : `${hours}h`;
|
|
149
|
+
}
|
|
150
|
+
function asRecord(value) {
|
|
151
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
152
|
+
}
|
|
153
|
+
function parseRunResult(detail) {
|
|
154
|
+
const result = asRecord(detail.run.result);
|
|
155
|
+
const terminationRecord = asRecord(result?.termination);
|
|
156
|
+
return {
|
|
157
|
+
exitCode: typeof result?.exitCode === "number" ? result.exitCode : null,
|
|
158
|
+
termination: terminationRecord && (terminationRecord.reason === "exited" || terminationRecord.reason === "timed_out" || terminationRecord.reason === "aborted")
|
|
159
|
+
? {
|
|
160
|
+
reason: terminationRecord.reason,
|
|
161
|
+
exitCode: typeof terminationRecord.exitCode === "number" ? terminationRecord.exitCode : null,
|
|
162
|
+
...(typeof terminationRecord.timeoutSecs === "number" ? { timeoutSecs: terminationRecord.timeoutSecs } : {}),
|
|
163
|
+
...(typeof terminationRecord.message === "string" ? { message: terminationRecord.message } : {}),
|
|
164
|
+
}
|
|
165
|
+
: undefined,
|
|
166
|
+
durationMs: typeof result?.durationMs === "number" ? result.durationMs : 0,
|
|
167
|
+
output: typeof result?.output === "string" ? result.output : "",
|
|
168
|
+
truncated: Boolean(result?.truncated),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function writeOutput(output) {
|
|
172
|
+
if (!output)
|
|
173
|
+
return;
|
|
174
|
+
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
|
|
175
|
+
}
|
|
176
|
+
async function waitForRunCompletion(taskRunId, showSpinner) {
|
|
177
|
+
const client = createClient();
|
|
178
|
+
const startedAt = Date.now();
|
|
179
|
+
const spin = showSpinner ? spinner() : null;
|
|
180
|
+
if (spin)
|
|
181
|
+
spin.start(`Waiting for command task ${taskRunId}`);
|
|
182
|
+
while (true) {
|
|
183
|
+
const detail = await client.tasks.get(taskRunId);
|
|
184
|
+
if (detail.run.status === "completed" || detail.run.status === "failed") {
|
|
185
|
+
if (spin)
|
|
186
|
+
spin.stop(`Finished in ${formatElapsed(Date.now() - startedAt)}`);
|
|
187
|
+
return detail;
|
|
188
|
+
}
|
|
189
|
+
const elapsedMs = Date.now() - startedAt;
|
|
190
|
+
if (elapsedMs > DEFAULT_WAIT_TIMEOUT_MS) {
|
|
191
|
+
if (spin)
|
|
192
|
+
spin.stop(`Timed out after ${formatElapsed(DEFAULT_WAIT_TIMEOUT_MS)}`);
|
|
193
|
+
return error("Timed out waiting for run command", `taskRunId: ${taskRunId}`);
|
|
194
|
+
}
|
|
195
|
+
if (spin)
|
|
196
|
+
spin.update(`Running... ${formatElapsed(elapsedMs)}`);
|
|
197
|
+
await sleep(DEFAULT_POLL_INTERVAL_MS);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function handleRunCli(argv) {
|
|
201
|
+
const opts = parseRunCliOptions(argv);
|
|
202
|
+
const client = createClient();
|
|
203
|
+
try {
|
|
204
|
+
const { taskRunId } = await client.space(opts.spaceId).runCommand({ command: opts.command });
|
|
205
|
+
if (opts.async) {
|
|
206
|
+
if (opts.json)
|
|
207
|
+
return outJson({ taskRunId });
|
|
208
|
+
process.stderr.write(` Command queued — taskRunId: ${taskRunId}\n`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const detail = await waitForRunCompletion(taskRunId, !opts.json);
|
|
212
|
+
if (detail.run.status === "failed") {
|
|
213
|
+
const message = detail.run.errorMessage?.trim() || `Command task failed: ${taskRunId}`;
|
|
214
|
+
return error(message);
|
|
215
|
+
}
|
|
216
|
+
const result = parseRunResult(detail);
|
|
217
|
+
if (opts.json) {
|
|
218
|
+
return outJson({ taskRunId, run: detail.run, progress: detail.progress, ...result });
|
|
219
|
+
}
|
|
220
|
+
writeOutput(result.output);
|
|
221
|
+
process.exitCode = result.exitCode ?? 0;
|
|
222
|
+
process.stderr.write(` Command finished — taskRunId: ${taskRunId}, exitCode: ${result.exitCode ?? "unknown"}, duration: ${formatElapsed(result.durationMs)}${result.truncated ? ", output truncated" : ""}\n`);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
handleHttp(e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export async function maybeHandleRunCommand(argv) {
|
|
229
|
+
if (topLevelRunIndex(argv) < 0)
|
|
230
|
+
return false;
|
|
231
|
+
await handleRunCli(argv);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
package/dist/commands/spaces.js
CHANGED
|
@@ -296,18 +296,20 @@ export function registerSpaces(program) {
|
|
|
296
296
|
});
|
|
297
297
|
// ── spaces get ──
|
|
298
298
|
spacesCmd
|
|
299
|
-
.command("get
|
|
299
|
+
.command("get [id]")
|
|
300
300
|
.description("Show space details")
|
|
301
301
|
.option("--json", "Output as JSON")
|
|
302
302
|
.action(async (id, opts) => {
|
|
303
|
+
const spaceId = id?.trim() || resolveSpace(spacesCmd);
|
|
303
304
|
const client = createClient();
|
|
304
305
|
try {
|
|
305
|
-
const space = await client.spaces.get(
|
|
306
|
+
const space = await client.spaces.get(spaceId);
|
|
306
307
|
if (jsonRequested(opts))
|
|
307
308
|
return outJson(space);
|
|
308
309
|
table([space], [
|
|
309
310
|
{ key: "id", label: "ID" },
|
|
310
311
|
{ key: "name", label: "Name" },
|
|
312
|
+
{ key: "slug", label: "Slug" },
|
|
311
313
|
{ key: "description", label: "Description" },
|
|
312
314
|
{ key: "status", label: "Status" },
|
|
313
315
|
{ key: "createdAt", label: "Created" },
|
|
@@ -348,6 +350,37 @@ export function registerSpaces(program) {
|
|
|
348
350
|
handleHttp(e);
|
|
349
351
|
}
|
|
350
352
|
});
|
|
353
|
+
// ── spaces update ──
|
|
354
|
+
spacesCmd
|
|
355
|
+
.command("update <id>")
|
|
356
|
+
.description("Update a space")
|
|
357
|
+
.option("--name <name>", "Space name")
|
|
358
|
+
.option("--slug <slug>", "Public space slug")
|
|
359
|
+
.option("--json", "Output as JSON")
|
|
360
|
+
.action(async (id, opts) => {
|
|
361
|
+
const input = {
|
|
362
|
+
name: opts.name,
|
|
363
|
+
slug: opts.slug,
|
|
364
|
+
};
|
|
365
|
+
if (input.name === undefined && input.slug === undefined) {
|
|
366
|
+
return error("Nothing to update", "Pass --name or --slug.");
|
|
367
|
+
}
|
|
368
|
+
const client = createClient();
|
|
369
|
+
try {
|
|
370
|
+
const result = await client.space(id).update(input);
|
|
371
|
+
if (jsonRequested(opts))
|
|
372
|
+
return outJson(result);
|
|
373
|
+
ok("Space updated");
|
|
374
|
+
table([result.space], [
|
|
375
|
+
{ key: "id", label: "ID" },
|
|
376
|
+
{ key: "name", label: "Name" },
|
|
377
|
+
{ key: "slug", label: "Slug" },
|
|
378
|
+
]);
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
handleHttp(e);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
351
384
|
// ── spaces rename ──
|
|
352
385
|
spacesCmd
|
|
353
386
|
.command("rename <id> <name>")
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerModels } from "./commands/models.js";
|
|
|
10
10
|
import { registerProfile } from "./commands/profile.js";
|
|
11
11
|
import { registerSearch } from "./commands/search.js";
|
|
12
12
|
import { registerPrompt, registerSpaces } from "./commands/spaces.js";
|
|
13
|
+
import { maybeHandleRunCommand } from "./commands/run.js";
|
|
13
14
|
import { registerTasks } from "./commands/tasks.js";
|
|
14
15
|
import { registerWorks } from "./commands/works.js";
|
|
15
16
|
import { ensureCliSelfUpdated } from "./self-update.js";
|
|
@@ -38,6 +39,7 @@ Common commands:
|
|
|
38
39
|
cohub profile avatar ./avatar.png
|
|
39
40
|
cohub spaces ls
|
|
40
41
|
cohub -s <space-id> prompt "Fix the failing tests"
|
|
42
|
+
cohub -s <space-id> run -- git status
|
|
41
43
|
cohub search "release notes"
|
|
42
44
|
cohub -s <space-id> spaces sessions turns ls <session-id>
|
|
43
45
|
cohub -s <space-id> spaces files ls
|
|
@@ -64,7 +66,11 @@ registerCronJobs(program);
|
|
|
64
66
|
registerWorks(program);
|
|
65
67
|
const isVersionRequest = (argv) => argv.some((arg) => arg === "-v" || arg === "--version");
|
|
66
68
|
try {
|
|
67
|
-
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
if (await maybeHandleRunCommand(argv)) {
|
|
71
|
+
process.exit();
|
|
72
|
+
}
|
|
73
|
+
if (!isVersionRequest(argv)) {
|
|
68
74
|
await ensureCliSelfUpdated();
|
|
69
75
|
}
|
|
70
76
|
}
|