@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 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
@@ -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
+ }
@@ -296,18 +296,20 @@ export function registerSpaces(program) {
296
296
  });
297
297
  // ── spaces get ──
298
298
  spacesCmd
299
- .command("get <id>")
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(id);
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
- if (!isVersionRequest(process.argv.slice(2))) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "1.20.1",
3
+ "version": "1.20.3",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",