@neta-art/cohub-cli 1.20.1 → 1.20.2

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
@@ -61,7 +61,9 @@ cohub spaces prompt -h
61
61
  cohub spaces ls --json
62
62
  cohub spaces get <spaceId> --json
63
63
  cohub spaces create --name "<name>" --description "<description>" --json
64
+ cohub spaces update <spaceId> --slug <space-slug>
64
65
  cohub spaces rename <spaceId> "<new name>"
66
+ cohub -s <spaceId> run -- git status
65
67
  ```
66
68
 
67
69
  Many space-scoped commands need a target Space:
@@ -73,6 +75,7 @@ cohub -s <spaceId> spaces prompt "message" --json
73
75
  ## Chats and prompts
74
76
 
75
77
  Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
78
+ Use `run` for a one-off shell command in the current Space workspace.
76
79
 
77
80
  ```bash
78
81
  # Send now
@@ -175,9 +178,11 @@ Confirm before deleting files or directories.
175
178
 
176
179
  ## Works
177
180
 
178
- Publish and manage Work entries from a Space workspace.
181
+ Publish and manage Work entries from a Space workspace. Public Work URLs require a username and a Space slug.
179
182
 
180
183
  ```bash
184
+ cohub profile update --username <username>
185
+ cohub spaces update <spaceId> --slug <space-slug>
181
186
  cohub -s <spaceId> works ls --json
182
187
  cohub works get <workId> --json
183
188
  cohub -s <spaceId> works publish demo --file dist/index.html
@@ -204,6 +209,16 @@ cohub -s <spaceId> spaces checkpoints get <checkpointId> --json
204
209
  cohub -s <spaceId> spaces checkpoints create "<description>" --json
205
210
  ```
206
211
 
212
+ ## Run commands
213
+
214
+ ```bash
215
+ cohub -s <spaceId> run -- git status
216
+ cohub -s <spaceId> run --command "pnpm test"
217
+ cohub -s <spaceId> run --async --command "pnpm build"
218
+ ```
219
+
220
+ Use `run` for one-off shell commands in a Space workspace. Use `--async` to queue and return immediately.
221
+
207
222
  ## Tasks
208
223
 
209
224
  ```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
+ }
@@ -348,6 +348,37 @@ export function registerSpaces(program) {
348
348
  handleHttp(e);
349
349
  }
350
350
  });
351
+ // ── spaces update ──
352
+ spacesCmd
353
+ .command("update <id>")
354
+ .description("Update a space")
355
+ .option("--name <name>", "Space name")
356
+ .option("--slug <slug>", "Public space slug")
357
+ .option("--json", "Output as JSON")
358
+ .action(async (id, opts) => {
359
+ const input = {
360
+ name: opts.name,
361
+ slug: opts.slug,
362
+ };
363
+ if (input.name === undefined && input.slug === undefined) {
364
+ return error("Nothing to update", "Pass --name or --slug.");
365
+ }
366
+ const client = createClient();
367
+ try {
368
+ const result = await client.space(id).update(input);
369
+ if (jsonRequested(opts))
370
+ return outJson(result);
371
+ ok("Space updated");
372
+ table([result.space], [
373
+ { key: "id", label: "ID" },
374
+ { key: "name", label: "Name" },
375
+ { key: "slug", label: "Slug" },
376
+ ]);
377
+ }
378
+ catch (e) {
379
+ handleHttp(e);
380
+ }
381
+ });
351
382
  // ── spaces rename ──
352
383
  spacesCmd
353
384
  .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.2",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",